Version 0.7.0
This commit is contained in:
parent
64ad8498f5
commit
6b6bbdc142
112 changed files with 3828 additions and 188 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pulsar-web",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { StartView } from "./views/StartView";
|
|||
import { AppView } from "./views/AppView";
|
||||
import { LoginView } from "./views/LoginView";
|
||||
import { RegisterView } from "./views/RegisterView";
|
||||
import { InviteView } from "./views/InviteView";
|
||||
|
||||
const App: Component = () => {
|
||||
return (
|
||||
|
|
@ -12,6 +13,7 @@ const App: Component = () => {
|
|||
<Route path="/app" component={AppView} />
|
||||
<Route path="/login" component={LoginView} />
|
||||
<Route path="/register" component={RegisterView} />
|
||||
<Route path="/invite/:id" component={InviteView} />
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ interface IFetchChannel {
|
|||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
order: number;
|
||||
communityId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
|
@ -25,6 +27,7 @@ interface IUpdateChannelRequest {
|
|||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
category?: string | null;
|
||||
}
|
||||
|
||||
interface IUpdateChannelResponse extends IResponseSuccess, IFetchChannel {}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ import {
|
|||
IFetchCommunityMembersResponse,
|
||||
IFetchCommunityInvitesRequest,
|
||||
IFetchCommunityInvitesResponse,
|
||||
IUpdateCommunityChannelOrderRequest,
|
||||
IUpdateCommunityChannelOrderResponse,
|
||||
IUpdateCommunityRoleOrderRequest,
|
||||
IUpdateCommunityRoleOrderResponse,
|
||||
IRemoveCommunityMemberRequest,
|
||||
IRemoveCommunityMemberResponse,
|
||||
ICreateCommunityInviteRequest,
|
||||
ICreateCommunityInviteResponse,
|
||||
} from "./types";
|
||||
|
||||
const fetchCommunityApi = async (
|
||||
|
|
@ -67,6 +75,45 @@ const fetchCommunityInvitesApi = async (
|
|||
return await callApi(HTTP.GET, `community/${request.id}/invites`);
|
||||
};
|
||||
|
||||
const updateCommunityChannelOrderApi = async (
|
||||
request: IUpdateCommunityChannelOrderRequest,
|
||||
): Promise<IUpdateCommunityChannelOrderResponse | IResponseError> => {
|
||||
return await callApi(
|
||||
HTTP.PATCH,
|
||||
`community/${request.id}/channels/order`,
|
||||
request,
|
||||
);
|
||||
};
|
||||
|
||||
const updateCommunityRoleOrderApi = async (
|
||||
request: IUpdateCommunityRoleOrderRequest,
|
||||
): Promise<IUpdateCommunityRoleOrderResponse | IResponseError> => {
|
||||
return await callApi(
|
||||
HTTP.PATCH,
|
||||
`community/${request.id}/roles/order`,
|
||||
request,
|
||||
);
|
||||
};
|
||||
|
||||
const removeCommunityMemberApi = async (
|
||||
request: IRemoveCommunityMemberRequest,
|
||||
): Promise<IRemoveCommunityMemberResponse | IResponseError> => {
|
||||
return await callApi(
|
||||
HTTP.DELETE,
|
||||
`community/${request.id}/members/${request.memberId}`,
|
||||
);
|
||||
};
|
||||
|
||||
const createCommunityInviteApi = async (
|
||||
request: ICreateCommunityInviteRequest,
|
||||
): Promise<ICreateCommunityInviteResponse | IResponseError> => {
|
||||
return await callApi(
|
||||
HTTP.POST,
|
||||
`community/${request.communityId}/invite`,
|
||||
request,
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
fetchCommunityApi,
|
||||
createCommunityApi,
|
||||
|
|
@ -76,4 +123,8 @@ export {
|
|||
fetchCommunityRolesApi,
|
||||
fetchCommunityMembersApi,
|
||||
fetchCommunityInvitesApi,
|
||||
updateCommunityChannelOrderApi,
|
||||
updateCommunityRoleOrderApi,
|
||||
removeCommunityMemberApi,
|
||||
createCommunityInviteApi,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ interface IFetchCommunityChannelsResponse extends IResponseSuccess {
|
|||
interface IFetchCommunityChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface IFetchCommunityRolesRequest {
|
||||
|
|
@ -62,6 +65,9 @@ interface IFetchCommunityRolesResponse extends IResponseSuccess {
|
|||
interface IFetchCommunityRole {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
order: number;
|
||||
showInMembers: boolean;
|
||||
}
|
||||
|
||||
interface IFetchCommunityMembersRequest {
|
||||
|
|
@ -92,6 +98,45 @@ interface IFetchCommunityInvite {
|
|||
id: string;
|
||||
}
|
||||
|
||||
interface IUpdateCommunityChannelOrderRequest {
|
||||
id: string;
|
||||
order: string[];
|
||||
}
|
||||
|
||||
interface IUpdateCommunityChannelOrderResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IUpdateCommunityRoleOrderRequest {
|
||||
id: string;
|
||||
order: string[];
|
||||
}
|
||||
|
||||
interface IUpdateCommunityRoleOrderResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IRemoveCommunityMemberRequest {
|
||||
id: string;
|
||||
memberId: string;
|
||||
}
|
||||
|
||||
interface IRemoveCommunityMemberResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface ICreateCommunityInviteRequest {
|
||||
communityId: string;
|
||||
totalInvites?: number;
|
||||
expirationDate?: number;
|
||||
}
|
||||
|
||||
interface ICreateCommunityInviteResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
inviteId: string;
|
||||
}
|
||||
|
||||
export {
|
||||
type IFetchCommunity,
|
||||
type IFetchCommunityRequest,
|
||||
|
|
@ -114,4 +159,12 @@ export {
|
|||
type IFetchCommunityInvitesRequest,
|
||||
type IFetchCommunityInvitesResponse,
|
||||
type IFetchCommunityInvite,
|
||||
type IUpdateCommunityChannelOrderRequest,
|
||||
type IUpdateCommunityChannelOrderResponse,
|
||||
type IUpdateCommunityRoleOrderRequest,
|
||||
type IUpdateCommunityRoleOrderResponse,
|
||||
type IRemoveCommunityMemberRequest,
|
||||
type IRemoveCommunityMemberResponse,
|
||||
type ICreateCommunityInviteRequest,
|
||||
type ICreateCommunityInviteResponse,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface IUploadCommunityAvatarResponse extends IResponseSuccess {}
|
|||
|
||||
interface IFetchAttachment {
|
||||
id: string;
|
||||
iv: string;
|
||||
filename: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
|
|
@ -33,6 +34,7 @@ interface IFetchAttachmentRequest {
|
|||
interface IFetchAttachmentResponse extends IResponseSuccess, IFetchAttachment {}
|
||||
|
||||
interface ICreateAttachmentRequest {
|
||||
iv: string;
|
||||
filename: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface ICreateMessageResponse extends IResponseSuccess, IFetchMessage {}
|
|||
|
||||
interface IUpdateMessageRequest {
|
||||
id: string;
|
||||
iv: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import {
|
|||
IUpdateRoleResponse,
|
||||
IRemoveRoleRequest,
|
||||
IRemoveRoleResponse,
|
||||
IAssignRoleRequest,
|
||||
IAssignRoleResponse,
|
||||
IUnssignRoleRequest,
|
||||
IUnssignRoleResponse,
|
||||
IFetchPermissionsResponse,
|
||||
} from "./types";
|
||||
|
||||
const fetchRoleApi = async (
|
||||
|
|
@ -35,4 +40,30 @@ const removeRoleApi = async (
|
|||
return await callApi(HTTP.DELETE, `role/${request.id}`);
|
||||
};
|
||||
|
||||
export { fetchRoleApi, createRoleApi, updateRoleApi, removeRoleApi };
|
||||
const assignRoleApi = async (
|
||||
request: IAssignRoleRequest,
|
||||
): Promise<IAssignRoleResponse | IResponseError> => {
|
||||
return await callApi(HTTP.POST, `role/${request.id}/assign`, request);
|
||||
};
|
||||
|
||||
const unassignRoleApi = async (
|
||||
request: IUnssignRoleRequest,
|
||||
): Promise<IUnssignRoleResponse | IResponseError> => {
|
||||
return await callApi(HTTP.POST, `role/${request.id}/unassign`, request);
|
||||
};
|
||||
|
||||
const fetchPermissionsApi = async (): Promise<
|
||||
IFetchPermissionsResponse | IResponseError
|
||||
> => {
|
||||
return await callApi(HTTP.GET, `role/permissions`);
|
||||
};
|
||||
|
||||
export {
|
||||
fetchRoleApi,
|
||||
createRoleApi,
|
||||
updateRoleApi,
|
||||
removeRoleApi,
|
||||
assignRoleApi,
|
||||
unassignRoleApi,
|
||||
fetchPermissionsApi,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ interface IFetchRole {
|
|||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color: string;
|
||||
order: number;
|
||||
showInMembers: boolean;
|
||||
communityId: string;
|
||||
permissions: string[];
|
||||
creationDate: number;
|
||||
|
|
@ -18,6 +21,7 @@ interface IFetchRoleResponse extends IResponseSuccess, IFetchRole {}
|
|||
interface ICreateRoleRequest {
|
||||
name: string;
|
||||
communityId: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface ICreateRoleResponse extends IResponseSuccess, IFetchRole {}
|
||||
|
|
@ -25,6 +29,10 @@ interface ICreateRoleResponse extends IResponseSuccess, IFetchRole {}
|
|||
interface IUpdateRoleRequest {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
showInMembers?: boolean;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
interface IUpdateRoleResponse extends IResponseSuccess, IFetchRole {}
|
||||
|
|
@ -38,6 +46,34 @@ interface IRemoveRoleResponse extends IResponseSuccess {
|
|||
communityId: string;
|
||||
}
|
||||
|
||||
interface IAssignRoleRequest {
|
||||
id: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface IAssignRoleResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
name: string;
|
||||
communityId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface IUnssignRoleRequest {
|
||||
id: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface IUnssignRoleResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
name: string;
|
||||
communityId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface IFetchPermissionsResponse extends IResponseSuccess {
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export {
|
||||
type IFetchRole,
|
||||
type IFetchRoleRequest,
|
||||
|
|
@ -48,4 +84,9 @@ export {
|
|||
type IUpdateRoleResponse,
|
||||
type IRemoveRoleResponse,
|
||||
type IRemoveRoleRequest,
|
||||
type IAssignRoleRequest,
|
||||
type IAssignRoleResponse,
|
||||
type IUnssignRoleRequest,
|
||||
type IUnssignRoleResponse,
|
||||
type IFetchPermissionsResponse,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,6 +53,23 @@ interface IFetchUserCommunity {
|
|||
name: string;
|
||||
}
|
||||
|
||||
interface IFetchUserCommunityRolesRequest {
|
||||
id: string;
|
||||
communityId: string;
|
||||
}
|
||||
|
||||
interface IFetchUserCommunityRolesResponse extends IResponseSuccess {
|
||||
id: string;
|
||||
communityId: string;
|
||||
roles: IFetchUserCommunityRole[];
|
||||
}
|
||||
|
||||
interface IFetchUserCommunityRole {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface IUpdateUserRequest {
|
||||
id: string;
|
||||
nickname?: string;
|
||||
|
|
@ -73,6 +90,9 @@ export {
|
|||
type IFetchUserCommunitiesRequest,
|
||||
type IFetchUserCommunitiesResponse,
|
||||
type IFetchUserCommunity,
|
||||
type IFetchUserCommunityRolesRequest,
|
||||
type IFetchUserCommunityRolesResponse,
|
||||
type IFetchUserCommunityRole,
|
||||
type IUpdateUserRequest,
|
||||
type IUpdateUserResponse,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
IFetchUserSessionsResponse,
|
||||
IFetchUserCommunitiesRequest,
|
||||
IFetchUserCommunitiesResponse,
|
||||
IFetchUserCommunityRolesRequest,
|
||||
IFetchUserCommunityRolesResponse,
|
||||
IUpdateUserRequest,
|
||||
IUpdateUserResponse,
|
||||
} from "./types";
|
||||
|
|
@ -36,6 +38,15 @@ const fetchUserCommunitiesApi = async (
|
|||
return await callApi(HTTP.GET, `user/${request.id}/communities`);
|
||||
};
|
||||
|
||||
const fetchUserCommunityRolesApi = async (
|
||||
request: IFetchUserCommunityRolesRequest,
|
||||
): Promise<IFetchUserCommunityRolesResponse | IResponseError> => {
|
||||
return await callApi(
|
||||
HTTP.GET,
|
||||
`user/${request.id}/community/${request.communityId}/roles`,
|
||||
);
|
||||
};
|
||||
|
||||
const updateUserApi = async (
|
||||
request: IUpdateUserRequest,
|
||||
): Promise<IUpdateUserResponse | IResponseError> => {
|
||||
|
|
@ -47,5 +58,6 @@ export {
|
|||
fetchUserApi,
|
||||
fetchUserSessionsApi,
|
||||
fetchUserCommunitiesApi,
|
||||
fetchUserCommunityRolesApi,
|
||||
updateUserApi,
|
||||
};
|
||||
|
|
|
|||
23
src/components/BadgeItem/BadgeItem.tsx
Normal file
23
src/components/BadgeItem/BadgeItem.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { createSignal, type Component } from "solid-js";
|
||||
import { IBadgeItemProps } from "./types";
|
||||
import { RemoveIcon, RemoveStrokeIcon } from "../../icons";
|
||||
|
||||
const BadgeItem: Component<IBadgeItemProps> = (props: IBadgeItemProps) => {
|
||||
const [getHover, setHover] = createSignal<boolean>(false);
|
||||
|
||||
return (
|
||||
<div class={`badge bg-stone-700 h-8 pl-3 p-1 rounded-full`}>
|
||||
{props.text}
|
||||
<div
|
||||
class="w-6 cursor-pointer"
|
||||
onClick={() => props.onRemove?.(props.id)}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
{getHover() ? <RemoveIcon /> : <RemoveStrokeIcon />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BadgeItem };
|
||||
2
src/components/BadgeItem/index.ts
Normal file
2
src/components/BadgeItem/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./BadgeItem";
|
||||
export * from "./types";
|
||||
8
src/components/BadgeItem/types.ts
Normal file
8
src/components/BadgeItem/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
interface IBadgeItemProps {
|
||||
id: string;
|
||||
text: string;
|
||||
removable: boolean;
|
||||
onRemove?: (id: string) => void;
|
||||
}
|
||||
|
||||
export { type IBadgeItemProps };
|
||||
|
|
@ -6,6 +6,7 @@ const Channel: Component<IChannelProps> = (props: IChannelProps) => {
|
|||
<li
|
||||
class={`flex flex-row gap-2 items-center p-1 cursor-pointer rounded-lg ${props.active ? "bg-stone-700 hover:bg-stone-700" : "hover:bg-stone-800"}`}
|
||||
onClick={() => props.onChannelClick?.(props.id)}
|
||||
onContextMenu={(e) => props.onChannelRightClick?.(props.id, e)}
|
||||
>
|
||||
<div class="font-bold text-xl"> #</div>
|
||||
<div class="font-bold">{props.name}</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ interface IChannelProps {
|
|||
name: string;
|
||||
active: boolean;
|
||||
onChannelClick?: (id: string) => void;
|
||||
onChannelRightClick?: (id: string, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export { type IChannelProps };
|
||||
|
|
|
|||
15
src/components/CheckItem/CheckItem.tsx
Normal file
15
src/components/CheckItem/CheckItem.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Component } from "solid-js";
|
||||
import { ICheckItemProps } from "./types";
|
||||
|
||||
const CheckItem: Component<ICheckItemProps> = (props: ICheckItemProps) => {
|
||||
return (
|
||||
<div
|
||||
class={`w-48 py-2 cursor-pointer rounded-xl text-center text-md font-semibold transition-all border-2 border-stone-700 ${props.checked ? "bg-stone-700 hover:bg-stone-700" : "bg-stone-900 hover:bg-stone-800"}`}
|
||||
onClick={() => props.onClick?.(props.id, !props.checked)}
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CheckItem };
|
||||
2
src/components/CheckItem/index.ts
Normal file
2
src/components/CheckItem/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./CheckItem";
|
||||
export * from "./types";
|
||||
8
src/components/CheckItem/types.ts
Normal file
8
src/components/CheckItem/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
interface ICheckItemProps {
|
||||
id: string;
|
||||
text: string;
|
||||
checked: boolean;
|
||||
onClick?: (id: string, checked: boolean) => void;
|
||||
}
|
||||
|
||||
export { type ICheckItemProps };
|
||||
|
|
@ -7,6 +7,7 @@ const Community: Component<ICommunityProps> = (props: ICommunityProps) => {
|
|||
<div
|
||||
class="avatar cursor-pointer"
|
||||
onClick={() => props.onCommunityClick?.(props.id)}
|
||||
onContextMenu={(e) => props.onCommunityRightClick?.(props.id, e)}
|
||||
>
|
||||
<div
|
||||
class={`w-full transition-all duration-300 hover:outline-stone-300 ${props.active ? "rounded-lg outline-3 outline-stone-300 hover:outline-3" : "outline-transparent rounded-4xl outline-2"}`}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface ICommunityProps {
|
|||
avatar?: string;
|
||||
active: boolean;
|
||||
onCommunityClick?: (id: string) => void;
|
||||
onCommunityRightClick?: (id: string, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export { type ICommunityProps };
|
||||
|
|
|
|||
54
src/components/ContextMenu/ContextMenu.tsx
Normal file
54
src/components/ContextMenu/ContextMenu.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { createMemo, type Component, type JSXElement } from "solid-js";
|
||||
import { IContextMenuItem, IContextMenuProps } from "./types";
|
||||
import { Dynamic, Portal } from "solid-js/web";
|
||||
|
||||
const ContextMenu: Component<IContextMenuProps> = (
|
||||
props: IContextMenuProps,
|
||||
) => {
|
||||
let menuRef: HTMLUListElement | undefined;
|
||||
|
||||
const isRight = createMemo<boolean>(() => {
|
||||
const offsetWidth = props.offset ?? 0;
|
||||
|
||||
return props.position.x + offsetWidth >= window.innerWidth;
|
||||
});
|
||||
|
||||
const mapItem = (item: IContextMenuItem): JSXElement => {
|
||||
return (
|
||||
<li
|
||||
class="flex flex-row items-center gap-3 hover:bg-stone-800 w-full h-10 p-3 rounded-xl cursor-pointer z-50"
|
||||
style={{ color: item.color }}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div class="w-5">
|
||||
<Dynamic component={item.icon} />
|
||||
</div>
|
||||
<span>{item.label}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.visible && props.items.length > 0 ? (
|
||||
<Portal>
|
||||
<ul
|
||||
ref={menuRef}
|
||||
class="fixed flex flex-col gap-1 bg-stone-900 w-fit border-2 border-stone-800 shadow-xl rounded-2xl p-1 z-50"
|
||||
style={{
|
||||
top: `${props.position.y}px`,
|
||||
left: isRight()
|
||||
? undefined
|
||||
: `${props.position.x}px`,
|
||||
right: isRight() ? "16px" : undefined,
|
||||
}}
|
||||
>
|
||||
{props.items.map(mapItem)}
|
||||
</ul>
|
||||
</Portal>
|
||||
) : undefined}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { ContextMenu };
|
||||
3
src/components/ContextMenu/index.ts
Normal file
3
src/components/ContextMenu/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./ContextMenu";
|
||||
export * from "./types";
|
||||
export * from "./useContextMenu";
|
||||
23
src/components/ContextMenu/types.ts
Normal file
23
src/components/ContextMenu/types.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { JSXElement } from "solid-js";
|
||||
import { IconParameters } from "../../icons";
|
||||
|
||||
interface IContextMenuProps {
|
||||
visible: boolean;
|
||||
position: IPosition;
|
||||
offset?: number;
|
||||
items: IContextMenuItem[];
|
||||
}
|
||||
|
||||
interface IPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface IContextMenuItem {
|
||||
label: string;
|
||||
icon: (props: IconParameters) => JSXElement;
|
||||
color?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export { type IContextMenuProps, type IPosition, type IContextMenuItem };
|
||||
44
src/components/ContextMenu/useContextMenu.ts
Normal file
44
src/components/ContextMenu/useContextMenu.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createSignal, onCleanup } from "solid-js";
|
||||
import { resetContextMenuOpenId, setContextMenuOpenId } from "../../store/app";
|
||||
import { state } from "../../store/state";
|
||||
|
||||
const useContextMenu = <T>(id: string) => {
|
||||
const [getData, setData] = createSignal<T | undefined>(undefined);
|
||||
const [getPos, setPos] = createSignal({ x: 0, y: 0 });
|
||||
|
||||
const getVisible = (): boolean => {
|
||||
return state.app.contextMenuOpenId === id;
|
||||
};
|
||||
|
||||
const open = (e: MouseEvent, data: T) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setData(data as Exclude<T, Function>);
|
||||
setPos({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuOpenId(id);
|
||||
};
|
||||
|
||||
const close = () => resetContextMenuOpenId();
|
||||
|
||||
const handleGlobal = (e: MouseEvent | KeyboardEvent) => {
|
||||
if (e instanceof KeyboardEvent && e.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
if (e instanceof MouseEvent) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("click", handleGlobal);
|
||||
window.addEventListener("keydown", handleGlobal);
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("click", handleGlobal);
|
||||
window.removeEventListener("keydown", handleGlobal);
|
||||
});
|
||||
|
||||
return { getData, getVisible, getPos, open, close };
|
||||
};
|
||||
|
||||
export { useContextMenu };
|
||||
|
|
@ -32,7 +32,7 @@ const FileInput: Component<IFileInputProps> = (props: IFileInputProps) => {
|
|||
class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
>
|
||||
<label
|
||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 focus:border-none outline-none cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 focus:border-none outline-none border-2 border-stone-700 cursor-pointer ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
>
|
||||
{props.picture ? pictureHtml() : iconOnlyHtml()}
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ const FilePreview: Component<IFilePreviewProps> = (
|
|||
) => {
|
||||
return (
|
||||
<div
|
||||
class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
class={`bg-stone-800 p-2 ${props.allowResize ? "min-h-40 min-w-40 max-h-fit max-w-82" : "h-40 w-40"} ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
>
|
||||
<label
|
||||
class={`relative inline-block bg-stone-900 input w-full h-full p-0 focus:border-none outline-none ${props.clickable ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
class={`relative inline-block bg-stone-900 w-full h-full p-0 border-2 border-stone-700 focus:border-none outline-none ${props.clickable ? "cursor-pointer" : "cursor-default"} ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
>
|
||||
{props.picture ? (
|
||||
<div class="avatar w-full h-full">
|
||||
|
|
@ -22,11 +22,13 @@ const FilePreview: Component<IFilePreviewProps> = (
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
||||
<div class="w-full h-full flex flex-col items-center justify-center min-w-0">
|
||||
<div class="w-12">
|
||||
<Dynamic component={props.icon} />
|
||||
</div>
|
||||
<p class="w-full text-center px-2 truncate">
|
||||
{props.filename}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{props.clickable ? (
|
||||
|
|
@ -34,7 +36,7 @@ const FilePreview: Component<IFilePreviewProps> = (
|
|||
class={`absolute inset-0 flex items-center justify-center bg-black/50 text-white transition-opacity opacity-0 hover:opacity-100 ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
onClick={() => props.onClick?.(props.id)}
|
||||
>
|
||||
<div class="w-full p-12">
|
||||
<div class="w-12">
|
||||
<Dynamic component={props.clickIcon} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface IFilePreviewProps {
|
|||
rounded?: boolean;
|
||||
picture?: string;
|
||||
outline?: boolean;
|
||||
allowResize?: boolean;
|
||||
clickable?: boolean;
|
||||
clickIcon?: (props: IconParameters) => JSXElement;
|
||||
onClick?: (id: string | number) => void;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
import { type Component, type JSXElement } from "solid-js";
|
||||
import { IInputProps } from "./types";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
const Input: Component<IInputProps> = (props: IInputProps) => {
|
||||
const handleEnter = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !props.textArea) {
|
||||
props.onSubmit?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9]$/.test(e.key) && e.key !== "Backspace") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -23,7 +33,7 @@ const Input: Component<IInputProps> = (props: IInputProps) => {
|
|||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
onInput={(e) => props.onChange?.(e.currentTarget.value)}
|
||||
onKeyDown={handleEnter}
|
||||
onKeyDown={handleKey}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -33,7 +43,7 @@ const Input: Component<IInputProps> = (props: IInputProps) => {
|
|||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
onInput={(e) => props.onChange?.(e.currentTarget.value)}
|
||||
onKeyDown={handleEnter}
|
||||
onKeyDown={handleKey}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -44,6 +54,14 @@ const Input: Component<IInputProps> = (props: IInputProps) => {
|
|||
<label
|
||||
class={`bg-stone-800 input ${props.textArea ? "px-0" : "px-5"} w-full h-full focus:border-none outline-none ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
>
|
||||
{props.icon ? (
|
||||
<div class="w-5 mr-2">
|
||||
<Dynamic component={props.icon} />
|
||||
</div>
|
||||
) : undefined}
|
||||
{props.label ? (
|
||||
<div class="mr-2">{props.label}</div>
|
||||
) : undefined}
|
||||
{props.textArea ? textAreaHtml() : inputHtml()}
|
||||
</label>
|
||||
{props.submitText ? submitHtml() : undefined}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { JSXElement } from "solid-js";
|
||||
import { IconParameters } from "../../icons";
|
||||
|
||||
interface IInputProps {
|
||||
value: string;
|
||||
type?: "text" | "password" | "email";
|
||||
type?: "text" | "password" | "email" | "number" | "date";
|
||||
outline?: boolean;
|
||||
rounded?: boolean;
|
||||
textArea?: boolean;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
icon?: (props: IconParameters) => JSXElement;
|
||||
submitText?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const Member: Component<IMemberProps> = (props: IMemberProps) => {
|
|||
<li
|
||||
class={`flex flex-row gap-4 items-center p-1 cursor-pointer rounded-lg ${props.active ? "bg-stone-700 hover:bg-stone-700" : "hover:bg-stone-800"}`}
|
||||
onClick={() => props.onMemberClick?.(props.id)}
|
||||
onContextMenu={(e) => props.onMemberRightClick?.(props.id, e)}
|
||||
>
|
||||
<div class="avatar">
|
||||
{props.avatar ? (
|
||||
|
|
@ -19,7 +20,9 @@ const Member: Component<IMemberProps> = (props: IMemberProps) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="font-bold">{props.username}</div>
|
||||
<div class="font-bold" style={{ color: props.color }}>
|
||||
{props.nickname}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
interface IMemberProps {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
active: boolean;
|
||||
color?: string;
|
||||
onMemberClick?: (id: string) => void;
|
||||
onMemberRightClick?: (id: string, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export { type IMemberProps };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Component, JSXElement } from "solid-js";
|
||||
import { createSignal, type Component, type JSXElement } from "solid-js";
|
||||
import { IMessageProps } from "./types";
|
||||
import {
|
||||
DownloadIcon,
|
||||
|
|
@ -12,8 +12,11 @@ import { state } from "../../store/state";
|
|||
import { FilePreview } from "../FilePreview";
|
||||
import { fetchFile } from "../../services/file";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
import { Input } from "../Input";
|
||||
|
||||
const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
||||
const [getEditedText, setEditedText] = createSignal<string>(props.message);
|
||||
|
||||
const avatarHtml = (): JSXElement => (
|
||||
<div
|
||||
class="avatar cursor-pointer"
|
||||
|
|
@ -31,9 +34,9 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const nameHtml = (): JSXElement => (
|
||||
<div class="font-bold">{props.username}</div>
|
||||
);
|
||||
const onUpdateMessage = () => {
|
||||
props.onMessageEdit?.(props.messageId, getEditedText());
|
||||
};
|
||||
|
||||
const onDownloadAttachment = (attachmentId: string | number) => {
|
||||
const attachment = state.file.attachments[attachmentId];
|
||||
|
|
@ -100,6 +103,7 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
<FilePreview
|
||||
id={attachmentId}
|
||||
picture={URL.createObjectURL(attachment.fullFile)}
|
||||
allowResize={props.attachments.length === 1}
|
||||
clickable={true}
|
||||
clickIcon={ZoomIcon}
|
||||
onClick={onOpenAttachment}
|
||||
|
|
@ -113,23 +117,56 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
|
|||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
class={`flex list-row hover:bg-stone-700 after:hidden ${props.plain ? "py-1" : "py-2 mt-3"} ${props.plain ? "" : "before:absolute before:top-0 before:left-2 before:right-2 before:h-px before:bg-stone-700 first:before:hidden"}`}
|
||||
>
|
||||
{props.plain ? undefined : avatarHtml()}
|
||||
<div class={`pr-14 ${props.plain ? "pl-14" : ""}`}>
|
||||
{props.plain ? undefined : nameHtml()}
|
||||
{props.message.length ===
|
||||
0 ? undefined : props.decryptionStatus ? (
|
||||
<p class="list-col-wrap text-xs">{props.message}</p>
|
||||
const nameHtml = (): JSXElement => (
|
||||
<div class="font-bold" style={{ color: props.color }}>
|
||||
{props.username}
|
||||
</div>
|
||||
);
|
||||
|
||||
const timeHtml = (): JSXElement => (
|
||||
<div class="flex-1 text-[0.6rem] opacity-40 text-end text-nowrap">
|
||||
{new Date(props.time).toLocaleTimeString()}
|
||||
</div>
|
||||
);
|
||||
|
||||
const editingHtml = (): JSXElement => (
|
||||
<div class="">
|
||||
<Input
|
||||
value={getEditedText()}
|
||||
onChange={setEditedText}
|
||||
onSubmit={onUpdateMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const messageHtml = (): JSXElement => (
|
||||
<>
|
||||
{props.message.length === 0 ? undefined : props.decryptionStatus ? (
|
||||
<p class="list-col-wrap text-xs">
|
||||
{props.editing ? editingHtml() : props.message}
|
||||
</p>
|
||||
) : (
|
||||
<p class="list-col-wrap text-xs italic opacity-75">
|
||||
Decryption failed
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
class={`flex list-row hover:bg-stone-700 after:hidden ${props.plain ? "py-1" : "py-2 mt-3"} ${props.plain ? "" : "before:absolute before:top-0 before:left-2 before:right-2 before:h-px before:bg-stone-700 first:before:hidden"}`}
|
||||
onContextMenu={(e) =>
|
||||
props.onMessageRightClick?.(props.messageId, e)
|
||||
}
|
||||
>
|
||||
{props.plain ? undefined : avatarHtml()}
|
||||
<div class={`w-full pr-14 ${props.plain ? "pl-14" : ""}`}>
|
||||
{props.plain ? undefined : nameHtml()}
|
||||
{messageHtml()}
|
||||
{props.attachments.length > 0 ? attachmentsHtml() : undefined}
|
||||
</div>
|
||||
{timeHtml()}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@ interface IMessageProps {
|
|||
message: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
color?: string;
|
||||
avatar?: string;
|
||||
attachments: string[];
|
||||
time: number;
|
||||
plain: boolean;
|
||||
decryptionStatus: boolean;
|
||||
editing: boolean;
|
||||
onProfileClick?: (userId: string) => void;
|
||||
onMessageRightClick?: (id: string, event: MouseEvent) => void;
|
||||
onMessageEdit?: (id: string, text: string) => void;
|
||||
}
|
||||
|
||||
export { type IMessageProps };
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const RichSettingsItem: Component<IRichSettingsItemProps> = (
|
|||
|
||||
return (
|
||||
<div
|
||||
class={`collapse collapse-arrow rounded-xl transition-all border-2 border-stone-700 ${props.active ? "bg-stone-700 hover:bg-stone-700" : "bg-stone-900 hover:bg-stone-800"}`}
|
||||
class={`collapse collapse-arrow rounded-xl transition-all border-2 ${props.active ? "bg-stone-700 hover:bg-stone-700 border-stone-700 hover:border-stone-700" : "bg-stone-900 hover:bg-stone-800 border-stone-800 hover:border-stone-700"}`}
|
||||
>
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title font-semibold flex flex-row items-center gap-4 p-1">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { IconParameters } from "../../icons";
|
|||
interface IRichSettingsItemProps {
|
||||
id: string;
|
||||
title: string;
|
||||
text?: string;
|
||||
info?: string;
|
||||
avatar?: string;
|
||||
icon?: (props: IconParameters) => JSXElement;
|
||||
|
|
|
|||
44
src/components/Select/Select.tsx
Normal file
44
src/components/Select/Select.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { type Component, type JSXElement } from "solid-js";
|
||||
import { ISelectOption, ISelectProps } from "./types";
|
||||
|
||||
const Select: Component<ISelectProps> = (props: ISelectProps) => {
|
||||
const submitHtml = (): JSXElement => (
|
||||
<button
|
||||
class={`bg-stone-950 btn btn-neutral h-full ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
onClick={props.onSubmit}
|
||||
>
|
||||
{props.submitText}
|
||||
</button>
|
||||
);
|
||||
|
||||
const mapOption = (option: ISelectOption): JSXElement => (
|
||||
<option
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
selected={option.value === props.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`bg-stone-800 h-16 p-2 flex flex-row gap-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`}
|
||||
>
|
||||
<select
|
||||
class={`select bg-stone-800 px-5 w-full h-full outline-none ${props.rounded ? "rounded-full" : "rounded-xl"}`}
|
||||
onChange={(e) => props.onChange?.(e.currentTarget.value)}
|
||||
>
|
||||
{mapOption({
|
||||
value: "",
|
||||
label: props.placeholder ?? "",
|
||||
disabled: true,
|
||||
})}
|
||||
{props.options.map(mapOption)}
|
||||
</select>
|
||||
{props.submitText ? submitHtml() : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Select };
|
||||
2
src/components/Select/index.ts
Normal file
2
src/components/Select/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./Select";
|
||||
export * from "./types";
|
||||
18
src/components/Select/types.ts
Normal file
18
src/components/Select/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
interface ISelectProps {
|
||||
value: string;
|
||||
options: ISelectOption[];
|
||||
outline?: boolean;
|
||||
rounded?: boolean;
|
||||
placeholder?: string;
|
||||
submitText?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
}
|
||||
|
||||
interface ISelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export { type ISelectProps, type ISelectOption };
|
||||
|
|
@ -6,7 +6,7 @@ const SettingsItem: Component<ISettingsItemProps> = (
|
|||
) => {
|
||||
return (
|
||||
<div
|
||||
class={`w-48 py-2 cursor-pointer rounded-xl text-center text-md font-semibold transition-all border-2 border-stone-700 ${props.active ? "bg-stone-700 hover:bg-stone-700" : "bg-stone-900 hover:bg-stone-800"}`}
|
||||
class={`w-48 py-3 cursor-pointer rounded-xl text-center text-md font-semibold transition-all border-2 ${props.active ? "bg-stone-700 hover:bg-stone-700 border-stone-700 hover:border-stone-700" : "bg-stone-900 hover:bg-stone-800 border-stone-800 hover:border-stone-700"}`}
|
||||
onClick={() => props.onClick?.(props.id)}
|
||||
>
|
||||
{props.text}
|
||||
|
|
|
|||
34
src/icons/CheckIcon.tsx
Normal file
34
src/icons/CheckIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const CheckIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m4.5 12.75 6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckIcon;
|
||||
34
src/icons/DownIcon.tsx
Normal file
34
src/icons/DownIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const DownIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownIcon;
|
||||
27
src/icons/EditIcon.tsx
Normal file
27
src/icons/EditIcon.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const EditIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditIcon;
|
||||
34
src/icons/HashIcon.tsx
Normal file
34
src/icons/HashIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const HashIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default HashIcon;
|
||||
34
src/icons/InviteIcon.tsx
Normal file
34
src/icons/InviteIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const InviteIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteIcon;
|
||||
31
src/icons/KeyIcon.tsx
Normal file
31
src/icons/KeyIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const KeyIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.75 1.5a6.75 6.75 0 0 0-6.651 7.906c.067.39-.032.717-.221.906l-6.5 6.499a3 3 0 0 0-.878 2.121v2.818c0 .414.336.75.75.75H6a.75.75 0 0 0 .75-.75v-1.5h1.5A.75.75 0 0 0 9 19.5V18h1.5a.75.75 0 0 0 .53-.22l2.658-2.658c.19-.189.517-.288.906-.22A6.75 6.75 0 1 0 15.75 1.5Zm0 3a.75.75 0 0 0 0 1.5A2.25 2.25 0 0 1 18 8.25a.75.75 0 0 0 1.5 0 3.75 3.75 0 0 0-3.75-3.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyIcon;
|
||||
34
src/icons/LeaveIcon.tsx
Normal file
34
src/icons/LeaveIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const LeaveIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.5 3.75A1.5 1.5 0 0 0 6 5.25v13.5a1.5 1.5 0 0 0 1.5 1.5h6a1.5 1.5 0 0 0 1.5-1.5V15a.75.75 0 0 1 1.5 0v3.75a3 3 0 0 1-3 3h-6a3 3 0 0 1-3-3V5.25a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3V9A.75.75 0 0 1 15 9V5.25a1.5 1.5 0 0 0-1.5-1.5h-6Zm5.03 4.72a.75.75 0 0 1 0 1.06l-1.72 1.72h10.94a.75.75 0 0 1 0 1.5H10.81l1.72 1.72a.75.75 0 1 1-1.06 1.06l-3-3a.75.75 0 0 1 0-1.06l3-3a.75.75 0 0 1 1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveIcon;
|
||||
34
src/icons/LeftIcon.tsx
Normal file
34
src/icons/LeftIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const LeftIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftIcon;
|
||||
31
src/icons/LocationIcon.tsx
Normal file
31
src/icons/LocationIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const PinIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="m11.54 22.351.07.04.028.016a.76.76 0 0 0 .723 0l.028-.015.071-.041a16.975 16.975 0 0 0 1.144-.742 19.58 19.58 0 0 0 2.683-2.282c1.944-1.99 3.963-4.98 3.963-8.827a8.25 8.25 0 0 0-16.5 0c0 3.846 2.02 6.837 3.963 8.827a19.58 19.58 0 0 0 2.682 2.282 16.975 16.975 0 0 0 1.145.742ZM12 13.5a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinIcon;
|
||||
31
src/icons/PinIcon.tsx
Normal file
31
src/icons/PinIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const PinIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="m11.54 22.351.07.04.028.016a.76.76 0 0 0 .723 0l.028-.015.071-.041a16.975 16.975 0 0 0 1.144-.742 19.58 19.58 0 0 0 2.683-2.282c1.944-1.99 3.963-4.98 3.963-8.827a8.25 8.25 0 0 0-16.5 0c0 3.846 2.02 6.837 3.963 8.827a19.58 19.58 0 0 0 2.682 2.282 16.975 16.975 0 0 0 1.145.742ZM12 13.5a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinIcon;
|
||||
31
src/icons/ReactIcon.tsx
Normal file
31
src/icons/ReactIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const ReactIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-2.625 6c-.54 0-.828.419-.936.634a1.96 1.96 0 0 0-.189.866c0 .298.059.605.189.866.108.215.395.634.936.634.54 0 .828-.419.936-.634.13-.26.189-.568.189-.866 0-.298-.059-.605-.189-.866-.108-.215-.395-.634-.936-.634Zm4.314.634c.108-.215.395-.634.936-.634.54 0 .828.419.936.634.13.26.189.568.189.866 0 .298-.059.605-.189.866-.108.215-.395.634-.936.634-.54 0-.828-.419-.936-.634a1.96 1.96 0 0 1-.189-.866c0-.298.059-.605.189-.866Zm2.023 6.828a.75.75 0 1 0-1.06-1.06 3.75 3.75 0 0 1-5.304 0 .75.75 0 0 0-1.06 1.06 5.25 5.25 0 0 0 7.424 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactIcon;
|
||||
31
src/icons/RemoveIcon.tsx
Normal file
31
src/icons/RemoveIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
|
||||
|
||||
const RemoveIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveIcon;
|
||||
34
src/icons/RemoveStrokeIcon.tsx
Normal file
34
src/icons/RemoveStrokeIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const RemoveStrokeIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveStrokeIcon;
|
||||
34
src/icons/RightIcon.tsx
Normal file
34
src/icons/RightIcon.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Component } from "solid-js";
|
||||
|
||||
import {
|
||||
IconParameters,
|
||||
defaultStrokeIconParameters as defaults,
|
||||
} from "./types";
|
||||
|
||||
const RightIcon: Component<IconParameters> = ({
|
||||
width,
|
||||
height,
|
||||
fill = defaults.fill,
|
||||
stroke = defaults.stroke,
|
||||
strokeWidth = defaults.strokeWidth,
|
||||
}: IconParameters) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke={stroke}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightIcon;
|
||||
|
|
@ -5,7 +5,12 @@ import PlusIcon from "./PlusIcon";
|
|||
import MinusIcon from "./MinusIcon";
|
||||
import DeviceIcon from "./DeviceIcon";
|
||||
import TrashIcon from "./TrashIcon";
|
||||
import RemoveIcon from "./RemoveIcon";
|
||||
import RemoveStrokeIcon from "./RemoveStrokeIcon";
|
||||
import UpIcon from "./UpIcon";
|
||||
import DownIcon from "./DownIcon";
|
||||
import LeftIcon from "./LeftIcon";
|
||||
import RightIcon from "./RightIcon";
|
||||
import UploadIcon from "./UploadIcon";
|
||||
import UploadMultiIcon from "./UploadMultiIcon";
|
||||
import AttachmentIcon from "./AttachmentIcon";
|
||||
|
|
@ -16,6 +21,15 @@ import DownloadIcon from "./DownloadIcon";
|
|||
import ErrorIcon from "./ErrorIcon";
|
||||
import ZoomIcon from "./ZoomIcon";
|
||||
import PictureIcon from "./PictureIcon";
|
||||
import LeaveIcon from "./LeaveIcon";
|
||||
import InviteIcon from "./InviteIcon";
|
||||
import CheckIcon from "./CheckIcon";
|
||||
import KeyIcon from "./KeyIcon";
|
||||
import HashIcon from "./HashIcon";
|
||||
import EditIcon from "./EditIcon";
|
||||
import ReactIcon from "./ReactIcon";
|
||||
import PinIcon from "./PinIcon";
|
||||
import LocationIcon from "./LocationIcon";
|
||||
|
||||
import type { IconParameters } from "./types";
|
||||
|
||||
|
|
@ -28,7 +42,12 @@ export {
|
|||
MinusIcon,
|
||||
DeviceIcon,
|
||||
TrashIcon,
|
||||
RemoveIcon,
|
||||
RemoveStrokeIcon,
|
||||
UpIcon,
|
||||
DownIcon,
|
||||
LeftIcon,
|
||||
RightIcon,
|
||||
UploadIcon,
|
||||
UploadMultiIcon,
|
||||
AttachmentIcon,
|
||||
|
|
@ -39,4 +58,13 @@ export {
|
|||
ErrorIcon,
|
||||
ZoomIcon,
|
||||
PictureIcon,
|
||||
LeaveIcon,
|
||||
InviteIcon,
|
||||
CheckIcon,
|
||||
KeyIcon,
|
||||
HashIcon,
|
||||
EditIcon,
|
||||
ReactIcon,
|
||||
PinIcon,
|
||||
LocationIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
setAuthSessionIV,
|
||||
} from "../../store/auth";
|
||||
import { state } from "../../store/state";
|
||||
import { hexToBytes } from "../crypto";
|
||||
import { base64ToBuffer } from "../crypto";
|
||||
|
||||
const fetchRegister = async (
|
||||
username: string,
|
||||
|
|
@ -61,14 +61,14 @@ const fetchRefresh = async () => {
|
|||
|
||||
if (
|
||||
!state.auth.session?.storageSecret ||
|
||||
state.auth.session.storageSecret.length !== 89
|
||||
state.auth.session.storageSecret.length !== 61
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [keyHex, ivHex] = state.auth.session.storageSecret.split(";");
|
||||
const key = hexToBytes(keyHex);
|
||||
const iv = hexToBytes(ivHex);
|
||||
const [keyBase64, ivBase64] = state.auth.session.storageSecret.split(";");
|
||||
const key = base64ToBuffer(keyBase64);
|
||||
const iv = base64ToBuffer(ivBase64);
|
||||
|
||||
if (key && iv) {
|
||||
setAuthSessionKey(new Uint8Array(key));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
updateChannelApi,
|
||||
removeChannelApi,
|
||||
fetchChannelMessagesApi,
|
||||
IUpdateChannelRequest,
|
||||
} from "../../api/channel";
|
||||
import {
|
||||
deleteChannel,
|
||||
|
|
@ -39,16 +40,8 @@ const createChannel = async (name: string, communityId: string) => {
|
|||
setChannel(data);
|
||||
};
|
||||
|
||||
const updateChannel = async (
|
||||
id: string,
|
||||
name?: string,
|
||||
description?: string,
|
||||
) => {
|
||||
const data = await updateChannelApi({
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
});
|
||||
const updateChannel = async (updateChannelData: IUpdateChannelRequest) => {
|
||||
const data = await updateChannelApi(updateChannelData);
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import {
|
|||
fetchCommunityRolesApi,
|
||||
fetchCommunityMembersApi,
|
||||
fetchCommunityInvitesApi,
|
||||
updateCommunityChannelOrderApi,
|
||||
updateCommunityRoleOrderApi,
|
||||
removeCommunityMemberApi,
|
||||
createCommunityInviteApi,
|
||||
IUpdateCommunityRequest,
|
||||
ICreateCommunityInviteRequest,
|
||||
} from "../../api/community";
|
||||
import { setChannel } from "../../store/channel";
|
||||
import {
|
||||
|
|
@ -24,7 +29,7 @@ import {
|
|||
import { setInvite } from "../../store/invite";
|
||||
import { setRole } from "../../store/role";
|
||||
import { state } from "../../store/state";
|
||||
import { setUser } from "../../store/user";
|
||||
import { getLoggedInUser, setUser } from "../../store/user";
|
||||
import { DB_STORE, dbLoadEncrypted } from "../database";
|
||||
import { fetchUserCommunities } from "../user";
|
||||
|
||||
|
|
@ -152,6 +157,69 @@ const fetchCommunityInvites = async (id: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
const updateCommunityChannelOrder = async (id: string, order: string[]) => {
|
||||
const data = await updateCommunityChannelOrderApi({
|
||||
id: id,
|
||||
order: order,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchCommunityChannels(id);
|
||||
};
|
||||
|
||||
const updateCommunityRoleOrder = async (id: string, order: string[]) => {
|
||||
const data = await updateCommunityRoleOrderApi({
|
||||
id: id,
|
||||
order: order,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchCommunityRoles(id);
|
||||
fetchCommunityMembers(id);
|
||||
};
|
||||
|
||||
const createCommunityInvite = async (
|
||||
createCommunityInviteData: ICreateCommunityInviteRequest,
|
||||
) => {
|
||||
const data = await createCommunityInviteApi(createCommunityInviteData);
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
setInvite(data);
|
||||
|
||||
fetchCommunityInvites(createCommunityInviteData.communityId);
|
||||
};
|
||||
|
||||
const removeCommunityMember = async (id: string, memberId: string) => {
|
||||
const data = await removeCommunityMemberApi({
|
||||
id: id,
|
||||
memberId: memberId,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (memberId !== getLoggedInUser()?.id) {
|
||||
fetchCommunityMembers(data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
deleteCommunity(data.id);
|
||||
|
||||
if (state.user.loggedUserId) {
|
||||
fetchUserCommunities(state.user.loggedUserId);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCommunityCryptoStates = async () => {
|
||||
if (!state.user.loggedUserId) {
|
||||
return;
|
||||
|
|
@ -189,6 +257,10 @@ export {
|
|||
fetchCommunityRoles,
|
||||
fetchCommunityMembers,
|
||||
fetchCommunityInvites,
|
||||
updateCommunityChannelOrder,
|
||||
updateCommunityRoleOrder,
|
||||
createCommunityInvite,
|
||||
removeCommunityMember,
|
||||
loadCommunityCryptoStates,
|
||||
getCommunityAvatarUrl,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -86,26 +86,6 @@ const generateIv = (): Uint8Array<ArrayBuffer> => {
|
|||
return crypto.getRandomValues(new Uint8Array(12));
|
||||
};
|
||||
|
||||
const hexToBytes = (hex: string): ArrayBuffer | undefined => {
|
||||
if (hex.length % 2 !== 0) {
|
||||
return;
|
||||
}
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i++) {
|
||||
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
const bytesToHex = (bytes: ArrayBuffer): string => {
|
||||
const bytesUint8 = new Uint8Array(bytes);
|
||||
let hex = "";
|
||||
for (const byte of bytesUint8) {
|
||||
hex += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
return hex;
|
||||
};
|
||||
|
||||
const bufferToBase64 = (buffer: ArrayBuffer): string => {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
|
|
@ -137,8 +117,6 @@ export {
|
|||
encryptBytes,
|
||||
decryptBytes,
|
||||
generateIv,
|
||||
hexToBytes,
|
||||
bytesToHex,
|
||||
bufferToBase64,
|
||||
base64ToBuffer,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ import {
|
|||
} from "../../store/file";
|
||||
import { state } from "../../store/state";
|
||||
import {
|
||||
base64ToBuffer,
|
||||
bufferToBase64,
|
||||
decryptBytes,
|
||||
decryptData,
|
||||
encryptBytes,
|
||||
encryptData,
|
||||
generateIv,
|
||||
} from "../crypto";
|
||||
|
||||
|
|
@ -52,6 +55,7 @@ const uploadCommunityAvatar = async (
|
|||
|
||||
const fetchAttachment = async (
|
||||
id: string,
|
||||
communityId: string,
|
||||
): Promise<IAttachment | undefined> => {
|
||||
const data = await fetchAttachmentApi({
|
||||
id: id,
|
||||
|
|
@ -61,9 +65,52 @@ const fetchAttachment = async (
|
|||
return;
|
||||
}
|
||||
|
||||
setAttachment(data);
|
||||
const key = state.community.communities[communityId]?.derivedKey;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
return data;
|
||||
try {
|
||||
const ivBytes = base64ToBuffer(data.iv);
|
||||
const filenameBytes = base64ToBuffer(data.filename);
|
||||
const mimetypeBytes = base64ToBuffer(data.mimetype);
|
||||
if (!ivBytes || !filenameBytes || !mimetypeBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = await decryptData<string>({
|
||||
key: key,
|
||||
iv: new Uint8Array(ivBytes),
|
||||
encryptedData: filenameBytes,
|
||||
});
|
||||
|
||||
const mimetype = await decryptData<string>({
|
||||
key: key,
|
||||
iv: new Uint8Array(ivBytes),
|
||||
encryptedData: mimetypeBytes,
|
||||
});
|
||||
|
||||
const decryptedData: IAttachment = {
|
||||
...data,
|
||||
filename: filename,
|
||||
mimetype: mimetype,
|
||||
decryptionStatus: true,
|
||||
};
|
||||
|
||||
setAttachment(decryptedData);
|
||||
|
||||
return decryptedData;
|
||||
} catch {
|
||||
setAttachment({
|
||||
...data,
|
||||
decryptionStatus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
decryptionStatus: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const createAttachment = async (
|
||||
|
|
@ -72,10 +119,33 @@ const createAttachment = async (
|
|||
mimetype: string,
|
||||
size: number,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const key = state.community.communities[communityId]?.derivedKey;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iv = generateIv();
|
||||
|
||||
const encryptedFilename = await encryptData<string>({
|
||||
key: key,
|
||||
iv: iv,
|
||||
data: filename,
|
||||
});
|
||||
const encryptedMimetype = await encryptData<string>({
|
||||
key: key,
|
||||
iv: iv,
|
||||
data: mimetype,
|
||||
});
|
||||
if (!encryptedFilename || !encryptedMimetype) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await createAttachmentApi({
|
||||
iv: bufferToBase64(iv.buffer),
|
||||
communityId: communityId,
|
||||
filename: filename,
|
||||
mimetype: mimetype,
|
||||
filename: bufferToBase64(encryptedFilename),
|
||||
mimetype: bufferToBase64(encryptedMimetype),
|
||||
size: size,
|
||||
});
|
||||
|
||||
|
|
@ -86,6 +156,9 @@ const createAttachment = async (
|
|||
setAttachment({ ...data, fullFile: undefined });
|
||||
|
||||
return data.id;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const finishAttachment = async (id: string) => {
|
||||
|
|
@ -170,7 +243,7 @@ const fetchChunks = async (
|
|||
};
|
||||
|
||||
const fetchFile = async (communityId: string, attachmentId: string) => {
|
||||
const attachment = await fetchAttachment(attachmentId);
|
||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -192,12 +265,15 @@ const fetchFile = async (communityId: string, attachmentId: string) => {
|
|||
};
|
||||
|
||||
const fetchMedia = async (communityId: string, attachmentId: string) => {
|
||||
const attachment = await fetchAttachment(attachmentId);
|
||||
const attachment = await fetchAttachment(attachmentId, communityId);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment.mimetype.startsWith("image/")) {
|
||||
if (
|
||||
attachment.decryptionStatus &&
|
||||
attachment.mimetype.startsWith("image/")
|
||||
) {
|
||||
await fetchChunks(
|
||||
communityId,
|
||||
attachmentId,
|
||||
|
|
@ -260,7 +336,12 @@ const uploadChunks = async (
|
|||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const blob = file.slice(start, end);
|
||||
|
||||
const uploaded = uploadChunk(communityId, attachmentId, index, blob);
|
||||
const uploaded = await uploadChunk(
|
||||
communityId,
|
||||
attachmentId,
|
||||
index,
|
||||
blob,
|
||||
);
|
||||
if (!uploaded) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
removeInviteApi,
|
||||
acceptInviteApi,
|
||||
} from "../../api/invite";
|
||||
import { deleteInvite, setInvite } from "../../store/invite";
|
||||
import { deleteInvite, setInvite, setInviteView } from "../../store/invite";
|
||||
import { state } from "../../store/state";
|
||||
import { fetchUserCommunities } from "../user";
|
||||
|
||||
|
|
@ -19,6 +19,19 @@ const fetchInvite = async (id: string) => {
|
|||
setInvite(data);
|
||||
};
|
||||
|
||||
const fetchInviteView = async (id: string) => {
|
||||
const data = await fetchInviteApi({
|
||||
id: id,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
setInviteView(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setInviteView(data);
|
||||
};
|
||||
|
||||
const removeInvite = async (id: string) => {
|
||||
const data = await removeInviteApi({
|
||||
id: id,
|
||||
|
|
@ -45,4 +58,4 @@ const acceptInvite = async (id: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export { fetchInvite, removeInvite, acceptInvite };
|
||||
export { fetchInvite, fetchInviteView, removeInvite, acceptInvite };
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import {
|
|||
updateMessageApi,
|
||||
removeMessageApi,
|
||||
} from "../../api/message";
|
||||
import { deleteChannelMessage, setChannelMessage } from "../../store/channel";
|
||||
import { deleteAttachment } from "../../store/file";
|
||||
import { deleteMessage, setMessage } from "../../store/message";
|
||||
import { state } from "../../store/state";
|
||||
import {
|
||||
bytesToHex,
|
||||
base64ToBuffer,
|
||||
bufferToBase64,
|
||||
decryptData,
|
||||
encryptData,
|
||||
generateIv,
|
||||
hexToBytes,
|
||||
} from "../crypto";
|
||||
import { fetchMedia } from "../file";
|
||||
|
||||
|
|
@ -66,10 +67,10 @@ const createMessage = async (
|
|||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage({
|
||||
...data,
|
||||
text: text,
|
||||
decryptionStatus: true,
|
||||
});
|
||||
|
||||
attachments.forEach((attachmentId) => {
|
||||
|
|
@ -77,20 +78,31 @@ const createMessage = async (
|
|||
});
|
||||
};
|
||||
|
||||
const updateMessage = async (id: string, text: string) => {
|
||||
const updateMessage = async (id: string, text: string, communityId: string) => {
|
||||
const encrypted = await encryptMessage(communityId, text);
|
||||
if (!encrypted) {
|
||||
return;
|
||||
}
|
||||
const [encryptedMessage, iv] = encrypted;
|
||||
|
||||
const data = await updateMessageApi({
|
||||
id: id,
|
||||
text: text,
|
||||
iv: iv,
|
||||
text: encryptedMessage,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage(data);
|
||||
setMessage({
|
||||
...data,
|
||||
text: text,
|
||||
decryptionStatus: true,
|
||||
});
|
||||
};
|
||||
|
||||
const removeMessage = async (id: string) => {
|
||||
const removeMessage = async (id: string, channelId?: string) => {
|
||||
const data = await removeMessageApi({
|
||||
id: id,
|
||||
});
|
||||
|
|
@ -100,6 +112,10 @@ const removeMessage = async (id: string) => {
|
|||
}
|
||||
|
||||
deleteMessage();
|
||||
|
||||
if (channelId) {
|
||||
deleteChannelMessage(channelId, id);
|
||||
}
|
||||
};
|
||||
|
||||
const encryptMessage = async (
|
||||
|
|
@ -114,7 +130,7 @@ const encryptMessage = async (
|
|||
const iv = generateIv();
|
||||
|
||||
if (text.length === 0) {
|
||||
return ["", bytesToHex(iv.buffer)];
|
||||
return ["", bufferToBase64(iv.buffer)];
|
||||
}
|
||||
|
||||
const encrypted = await encryptData<string>({
|
||||
|
|
@ -123,7 +139,7 @@ const encryptMessage = async (
|
|||
data: text,
|
||||
});
|
||||
|
||||
return [bytesToHex(encrypted), bytesToHex(iv.buffer)];
|
||||
return [bufferToBase64(encrypted), bufferToBase64(iv.buffer)];
|
||||
};
|
||||
|
||||
const decryptMessage = async (
|
||||
|
|
@ -136,13 +152,13 @@ const decryptMessage = async (
|
|||
return;
|
||||
}
|
||||
|
||||
const ivBytes = hexToBytes(iv);
|
||||
const textBytes = hexToBytes(text);
|
||||
const ivBytes = base64ToBuffer(iv);
|
||||
const textBytes = base64ToBuffer(text);
|
||||
if (!ivBytes || !textBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await decryptData({
|
||||
return await decryptData<string>({
|
||||
key: key,
|
||||
iv: new Uint8Array(ivBytes),
|
||||
encryptedData: textBytes,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ import {
|
|||
createRoleApi,
|
||||
updateRoleApi,
|
||||
removeRoleApi,
|
||||
assignRoleApi,
|
||||
unassignRoleApi,
|
||||
fetchPermissionsApi,
|
||||
IUpdateRoleRequest,
|
||||
} from "../../api/role";
|
||||
import { deleteRole, setRole } from "../../store/role";
|
||||
import { deleteRole, setPermissions, setRole } from "../../store/role";
|
||||
import { state } from "../../store/state";
|
||||
import { fetchUserCommunityRoles } from "../user";
|
||||
|
||||
const fetchRole = async (id: string) => {
|
||||
const data = await fetchRoleApi({
|
||||
|
|
@ -22,6 +28,7 @@ const createRole = async (name: string, communityId: string) => {
|
|||
const data = await createRoleApi({
|
||||
name: name,
|
||||
communityId: communityId,
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
|
|
@ -31,11 +38,8 @@ const createRole = async (name: string, communityId: string) => {
|
|||
setRole(data);
|
||||
};
|
||||
|
||||
const updateRole = async (id: string, name?: string) => {
|
||||
const data = await updateRoleApi({
|
||||
id: id,
|
||||
name: name,
|
||||
});
|
||||
const updateRole = async (updateRoleData: IUpdateRoleRequest) => {
|
||||
const data = await updateRoleApi(updateRoleData);
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
|
|
@ -56,4 +60,91 @@ const removeRole = async (id: string) => {
|
|||
deleteRole(data.id);
|
||||
};
|
||||
|
||||
export { fetchRole, createRole, updateRole, removeRole };
|
||||
const assignRole = async (id: string, userId: string) => {
|
||||
const data = await assignRoleApi({
|
||||
id: id,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchUserCommunityRoles(data.userId, data.communityId);
|
||||
};
|
||||
|
||||
const unassignRole = async (id: string, userId: string) => {
|
||||
const data = await unassignRoleApi({
|
||||
id: id,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchUserCommunityRoles(data.userId, data.communityId);
|
||||
};
|
||||
|
||||
const fetchPermissions = async () => {
|
||||
const data = await fetchPermissionsApi();
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
setPermissions(data.permissions);
|
||||
};
|
||||
|
||||
const getUserBestRoleId = (
|
||||
userId: string,
|
||||
communityId: string,
|
||||
): string | undefined => {
|
||||
const user = state.user.users[userId];
|
||||
const community = state.community.communities[communityId];
|
||||
if (!user || !community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const communityRoleIds = community.roles;
|
||||
if (!communityRoleIds) {
|
||||
return;
|
||||
}
|
||||
const communityRoles = communityRoleIds
|
||||
.map((roleId) => {
|
||||
const role = state.role.roles[roleId];
|
||||
if (!role) {
|
||||
return {
|
||||
id: roleId,
|
||||
};
|
||||
}
|
||||
|
||||
return role;
|
||||
})
|
||||
.filter((role) => role.id !== "" && role.showInMembers);
|
||||
|
||||
let bestRoleOrder = 999999;
|
||||
let bestRoleId: string | undefined = undefined;
|
||||
communityRoles.forEach((role) => {
|
||||
if (
|
||||
(role.order ?? 999999) < bestRoleOrder &&
|
||||
(user.roles ?? {})[communityId].includes(role.id)
|
||||
) {
|
||||
bestRoleOrder = role.order ?? 999999;
|
||||
bestRoleId = role.id;
|
||||
}
|
||||
});
|
||||
|
||||
return bestRoleId;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchRole,
|
||||
createRole,
|
||||
updateRole,
|
||||
removeRole,
|
||||
assignRole,
|
||||
unassignRole,
|
||||
fetchPermissions,
|
||||
getUserBestRoleId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,15 +5,18 @@ import {
|
|||
fetchUserApi,
|
||||
fetchUserSessionsApi,
|
||||
fetchUserCommunitiesApi,
|
||||
fetchUserCommunityRolesApi,
|
||||
updateUserApi,
|
||||
IUpdateUserRequest,
|
||||
} from "../../api/user";
|
||||
import { setCommunity } from "../../store/community";
|
||||
import { setRole } from "../../store/role";
|
||||
import { setSession } from "../../store/session";
|
||||
import {
|
||||
setLoggedUserId,
|
||||
setUser,
|
||||
setUserCommunities,
|
||||
setUserCommunityRoles,
|
||||
setUserSessions,
|
||||
} from "../../store/user";
|
||||
|
||||
|
|
@ -71,6 +74,23 @@ const fetchUserCommunities = async (id: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
const fetchUserCommunityRoles = async (id: string, communityId: string) => {
|
||||
const data = await fetchUserCommunityRolesApi({
|
||||
id: id,
|
||||
communityId: communityId,
|
||||
});
|
||||
|
||||
if (typeof data.error === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
setUserCommunityRoles(data);
|
||||
|
||||
data.roles.forEach((role) => {
|
||||
setRole(role);
|
||||
});
|
||||
};
|
||||
|
||||
const updateUser = async (updateUserData: IUpdateUserRequest) => {
|
||||
const data = await updateUserApi(updateUserData);
|
||||
|
||||
|
|
@ -93,6 +113,7 @@ export {
|
|||
fetchUser,
|
||||
fetchUserSessions,
|
||||
fetchUserCommunities,
|
||||
fetchUserCommunityRoles,
|
||||
updateUser,
|
||||
getUserAvatarUrl,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ enum SocketMessageTypes {
|
|||
ANNOUNCEMENT = "ANNOUNCEMENT",
|
||||
SET_MESSAGE = "SET_MESSAGE",
|
||||
DELETE_MESSAGE = "DELETE_MESSAGE",
|
||||
UPDATE_COMMUNITY = "UPDATE_COMMUNITY",
|
||||
UPDATE_CHANNELS = "UPDATE_CHANNELS",
|
||||
UPDATE_ROLES = "UPDATE_ROLES",
|
||||
UPDATE_MEMBERS = "UPDATE_MEMBERS",
|
||||
|
|
@ -22,7 +23,7 @@ type SocketMessage =
|
|||
type: SocketMessageTypes.ANNOUNCEMENT;
|
||||
payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
|
@ -45,6 +46,12 @@ type SocketMessage =
|
|||
communityId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.UPDATE_COMMUNITY;
|
||||
payload: {
|
||||
communityId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.UPDATE_ROLES;
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { setAnnouncement, setAnnouncementOpen } from "../../store/app";
|
||||
import { deleteChannelMessage, setChannelMessage } from "../../store/channel";
|
||||
import { state } from "../../store/state";
|
||||
import {
|
||||
fetchCommunity,
|
||||
fetchCommunityChannels,
|
||||
fetchCommunityMembers,
|
||||
fetchCommunityRoles,
|
||||
|
|
@ -10,9 +12,13 @@ import { decryptMessage } from "../message";
|
|||
import config from "./config.json";
|
||||
import { SocketMessage, SocketMessageTypes } from "./types";
|
||||
|
||||
let connection: WebSocket;
|
||||
let connection: WebSocket | undefined = undefined;
|
||||
|
||||
const connectWs = () => {
|
||||
if (connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
connection = new WebSocket(
|
||||
`${config.schema}://${config.url}:${config.port}/${config.path}`,
|
||||
);
|
||||
|
|
@ -90,6 +96,10 @@ const handleMessage = (message: SocketMessage) => {
|
|||
);
|
||||
break;
|
||||
}
|
||||
case SocketMessageTypes.UPDATE_COMMUNITY: {
|
||||
fetchCommunity(message.payload.communityId);
|
||||
break;
|
||||
}
|
||||
case SocketMessageTypes.UPDATE_CHANNELS: {
|
||||
fetchCommunityChannels(message.payload.communityId);
|
||||
break;
|
||||
|
|
@ -103,6 +113,11 @@ const handleMessage = (message: SocketMessage) => {
|
|||
break;
|
||||
}
|
||||
case SocketMessageTypes.ANNOUNCEMENT: {
|
||||
setAnnouncement({
|
||||
title: message.payload.title,
|
||||
text: message.payload.text,
|
||||
});
|
||||
setAnnouncementOpen(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,69 @@
|
|||
import { setState } from "../state";
|
||||
import { IAnnouncement } from "./types";
|
||||
|
||||
const setHomeOpen = (value: boolean) => {
|
||||
setState("app", "homeOpen", value);
|
||||
const setAnnouncement = (announcement: IAnnouncement) => {
|
||||
setState("app", "announcement", announcement);
|
||||
};
|
||||
|
||||
const setSettingsOpen = (value: boolean) => {
|
||||
const setHomeOpen = (open: boolean) => {
|
||||
setState("app", "homeOpen", open);
|
||||
};
|
||||
|
||||
const setAnnouncementOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "announcementOpen", false);
|
||||
setState("app", "dialogsOpen", "announcementOpen", open);
|
||||
};
|
||||
|
||||
const setSettingsOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "settingsOpen", false);
|
||||
setState("app", "dialogsOpen", "settingsOpen", value);
|
||||
setState("app", "dialogsOpen", "settingsOpen", open);
|
||||
};
|
||||
|
||||
const setAddCommunityOpen = (value: boolean) => {
|
||||
const setAddCommunityOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "addCommunityOpen", false);
|
||||
setState("app", "dialogsOpen", "addCommunityOpen", value);
|
||||
setState("app", "dialogsOpen", "addCommunityOpen", open);
|
||||
};
|
||||
|
||||
const setCommunitySettingsOpen = (value: boolean) => {
|
||||
const setCommunitySettingsOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "communitySettingsOpen", false);
|
||||
setState("app", "dialogsOpen", "communitySettingsOpen", value);
|
||||
setState("app", "dialogsOpen", "communitySettingsOpen", open);
|
||||
};
|
||||
|
||||
const setCreateChannelOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "createChannelOpen", false);
|
||||
setState("app", "dialogsOpen", "createChannelOpen", open);
|
||||
};
|
||||
|
||||
const setCreateRoleOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "createRoleOpen", false);
|
||||
setState("app", "dialogsOpen", "createRoleOpen", open);
|
||||
};
|
||||
|
||||
const setCreateInviteOpen = (open: boolean) => {
|
||||
setState("app", "dialogsOpen", "createInviteOpen", false);
|
||||
setState("app", "dialogsOpen", "createInviteOpen", open);
|
||||
};
|
||||
|
||||
const setContextMenuOpenId = (contextMenuOpenId: string) => {
|
||||
setState("app", "contextMenuOpenId", contextMenuOpenId);
|
||||
setState("app", "contextMenuOpenId", contextMenuOpenId);
|
||||
};
|
||||
|
||||
const resetContextMenuOpenId = () => {
|
||||
setState("app", "contextMenuOpenId", null);
|
||||
setState("app", "contextMenuOpenId", null);
|
||||
};
|
||||
|
||||
export {
|
||||
setAnnouncement,
|
||||
setHomeOpen,
|
||||
setAnnouncementOpen,
|
||||
setSettingsOpen,
|
||||
setAddCommunityOpen,
|
||||
setCommunitySettingsOpen,
|
||||
setCreateChannelOpen,
|
||||
setCreateRoleOpen,
|
||||
setCreateInviteOpen,
|
||||
setContextMenuOpenId,
|
||||
resetContextMenuOpenId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
interface IAppState {
|
||||
homeOpen: boolean;
|
||||
announcement: IAnnouncement;
|
||||
dialogsOpen: IDialogsOpen;
|
||||
contextMenuOpenId: string | null;
|
||||
}
|
||||
|
||||
interface IAnnouncement {
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IDialogsOpen {
|
||||
announcementOpen: boolean;
|
||||
settingsOpen: boolean;
|
||||
addCommunityOpen: boolean;
|
||||
communitySettingsOpen: boolean;
|
||||
createChannelOpen: boolean;
|
||||
createRoleOpen: boolean;
|
||||
createInviteOpen: boolean;
|
||||
}
|
||||
|
||||
export { type IAppState, type IDialogsOpen };
|
||||
export { type IAppState, type IAnnouncement, type IDialogsOpen };
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ interface IChannel {
|
|||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
order?: number;
|
||||
communityId?: string;
|
||||
creationDate?: number;
|
||||
text?: string;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ interface IFileState {
|
|||
|
||||
interface IAttachment {
|
||||
id: string;
|
||||
iv: string;
|
||||
filename: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ const setInvite = (invite: IInvite) => {
|
|||
setState("invite", "invites", invite.id, invite);
|
||||
};
|
||||
|
||||
const setInviteView = (invite: IInvite | false) => {
|
||||
setState("invite", "inviteView", invite);
|
||||
};
|
||||
|
||||
const deleteInvite = (inviteId: string) => {
|
||||
setState("invite", "invites", inviteId, undefined);
|
||||
};
|
||||
|
||||
export { setInvite, deleteInvite };
|
||||
export { setInvite, setInviteView, deleteInvite };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
interface IInviteState {
|
||||
inviteView?: IInvite | false;
|
||||
invites: Record<string, IInvite | undefined>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,20 @@ const setRole = (role: IRole) => {
|
|||
setState("role", "roles", role.id, role);
|
||||
};
|
||||
|
||||
const setRoleName = (roleId: string, name: string) => {
|
||||
setState("role", "roles", roleId, "name", name);
|
||||
};
|
||||
|
||||
const setRolePermissions = (roleId: string, permissions: string[]) => {
|
||||
setState("role", "roles", roleId, "permissions", permissions);
|
||||
};
|
||||
|
||||
const deleteRole = (roleId: string) => {
|
||||
setState("role", "roles", roleId, undefined);
|
||||
};
|
||||
|
||||
export { setRole, deleteRole };
|
||||
const setPermissions = (permissions: string[]) => {
|
||||
setState("role", "permissions", permissions);
|
||||
};
|
||||
|
||||
export { setRole, setRoleName, setRolePermissions, deleteRole, setPermissions };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
interface IRoleState {
|
||||
roles: Record<string, IRole | undefined>;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface IRole {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
order?: number;
|
||||
showInMembers?: boolean;
|
||||
communityId?: string;
|
||||
permissions?: string[];
|
||||
creationDate?: number;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,20 @@ import { IState } from "./types";
|
|||
const [state, setState] = createStore<IState>({
|
||||
app: {
|
||||
homeOpen: true,
|
||||
announcement: {
|
||||
title: "",
|
||||
text: "",
|
||||
},
|
||||
dialogsOpen: {
|
||||
announcementOpen: false,
|
||||
settingsOpen: false,
|
||||
addCommunityOpen: false,
|
||||
communitySettingsOpen: false,
|
||||
createChannelOpen: false,
|
||||
createRoleOpen: false,
|
||||
createInviteOpen: false,
|
||||
},
|
||||
contextMenuOpenId: null,
|
||||
},
|
||||
auth: {
|
||||
registerSuccess: undefined,
|
||||
|
|
@ -27,11 +36,13 @@ const [state, setState] = createStore<IState>({
|
|||
},
|
||||
role: {
|
||||
roles: {},
|
||||
permissions: [],
|
||||
},
|
||||
session: {
|
||||
sessions: {},
|
||||
},
|
||||
invite: {
|
||||
inviteView: undefined,
|
||||
invites: {},
|
||||
},
|
||||
message: {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface IUser {
|
|||
lastLogin?: number;
|
||||
sessions?: string[];
|
||||
communities?: string[];
|
||||
roles?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export { type IUserState, type IUser };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
IFetchUserCommunitiesResponse,
|
||||
IFetchUserCommunityRolesResponse,
|
||||
IFetchUserSessionsResponse,
|
||||
} from "../../api/user";
|
||||
import { loadCommunityCryptoStates } from "../../services/community";
|
||||
|
|
@ -80,6 +81,21 @@ const setUserCommunities = (communities: IFetchUserCommunitiesResponse) => {
|
|||
loadCommunityCryptoStates();
|
||||
};
|
||||
|
||||
const setUserCommunityRoles = (roles: IFetchUserCommunityRolesResponse) => {
|
||||
if (!state.user.users[roles.id].roles) {
|
||||
setState("user", "users", roles.id, "roles", {});
|
||||
}
|
||||
|
||||
setState(
|
||||
"user",
|
||||
"users",
|
||||
roles.id,
|
||||
"roles",
|
||||
roles.communityId,
|
||||
roles.roles.map((role) => role.id),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
getLoggedInUser,
|
||||
setUser,
|
||||
|
|
@ -94,4 +110,5 @@ export {
|
|||
setLoggedUserId,
|
||||
setUserSessions,
|
||||
setUserCommunities,
|
||||
setUserCommunityRoles,
|
||||
};
|
||||
|
|
|
|||
31
src/views/AnnouncementModalView/AnnouncementModalView.tsx
Normal file
31
src/views/AnnouncementModalView/AnnouncementModalView.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { type Component } from "solid-js";
|
||||
import { IAnnouncementModalViewProps } from "./types";
|
||||
import { state } from "../../store/state";
|
||||
|
||||
const AnnouncementModalView: Component<IAnnouncementModalViewProps> = (
|
||||
props: IAnnouncementModalViewProps,
|
||||
) => {
|
||||
return (
|
||||
<div>
|
||||
<dialog
|
||||
ref={props.dialogRef}
|
||||
class="modal outline-none bg-[#00000050]"
|
||||
>
|
||||
<div class="modal-box bg-stone-950 rounded-3xl">
|
||||
<h3 class="text-lg font-bold text-center">
|
||||
{state.app.announcement.title}
|
||||
</h3>
|
||||
<div class="divider"></div>
|
||||
<p>{state.app.announcement.text}</p>
|
||||
</div>
|
||||
<form
|
||||
onClick={props.onClose}
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
></form>
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementModalView;
|
||||
2
src/views/AnnouncementModalView/index.ts
Normal file
2
src/views/AnnouncementModalView/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./AnnouncementModalView";
|
||||
export * from "./types";
|
||||
6
src/views/AnnouncementModalView/types.ts
Normal file
6
src/views/AnnouncementModalView/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface IAnnouncementModalViewProps {
|
||||
dialogRef?: (element: HTMLDialogElement) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export { type IAnnouncementModalViewProps };
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
fetchUser,
|
||||
fetchUserCommunities,
|
||||
} from "../../services/user";
|
||||
import { fetchPermissions } from "../../services/role";
|
||||
|
||||
const AppView: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -32,13 +33,15 @@ const AppView: Component = () => {
|
|||
if (state.user.loggedUserId) {
|
||||
fetchUser(state.user.loggedUserId);
|
||||
fetchUserCommunities(state.user.loggedUserId);
|
||||
fetchPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(state.auth.loggedIn);
|
||||
if (state.auth.loggedIn === false) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
} else if (state.auth.loggedIn === true) {
|
||||
connectWs();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,126 @@
|
|||
import { createEffect, createMemo, type Component } from "solid-js";
|
||||
import { createEffect, createMemo, JSXElement, type Component } from "solid-js";
|
||||
import { state } from "../../store/state";
|
||||
import { ICommunity } from "../../store/community";
|
||||
import {
|
||||
getActiveCommunity,
|
||||
ICommunity,
|
||||
resetActiveCommunity,
|
||||
} from "../../store/community";
|
||||
import { Channel } from "../../components/Channel";
|
||||
import { CommunityBar } from "../../components/CommunityBar";
|
||||
import { setCommunitySettingsOpen } from "../../store/app";
|
||||
import { fetchCommunityChannels } from "../../services/community";
|
||||
import { fetchChannel } from "../../services/channel";
|
||||
import { setActiveChannel } from "../../store/channel";
|
||||
import {
|
||||
setCommunitySettingsOpen,
|
||||
setCreateChannelOpen,
|
||||
} from "../../store/app";
|
||||
import {
|
||||
fetchCommunityChannels,
|
||||
getCommunityAvatarUrl,
|
||||
updateCommunityChannelOrder,
|
||||
} from "../../services/community";
|
||||
import { fetchChannel, removeChannel } from "../../services/channel";
|
||||
import {
|
||||
getActiveChannel,
|
||||
IChannel,
|
||||
resetActiveChannel,
|
||||
setActiveChannel,
|
||||
} from "../../store/channel";
|
||||
import {
|
||||
ContextMenu,
|
||||
IContextMenuItem,
|
||||
useContextMenu,
|
||||
} from "../../components/ContextMenu";
|
||||
import { DownIcon, PlusIcon, TrashIcon, UpIcon } from "../../icons";
|
||||
|
||||
const ChannelView: Component = () => {
|
||||
const channelIds = createMemo(() => {
|
||||
const activeCommunityId = state.community.active;
|
||||
if (!activeCommunityId) {
|
||||
const listMenu = useContextMenu("channel-list-menu");
|
||||
const categoryMenu = useContextMenu<string | null>("channel-category-menu");
|
||||
const channelMenu = useContextMenu<string>("channel-channel-menu");
|
||||
|
||||
const listMenuItems: IContextMenuItem[] = [
|
||||
{
|
||||
label: "Add new channel",
|
||||
icon: PlusIcon,
|
||||
color: "var(--color-stone-200)",
|
||||
onClick: () => onCreateChannel(),
|
||||
},
|
||||
];
|
||||
|
||||
const categoryMenuItems = createMemo<IContextMenuItem[]>(() => {
|
||||
if (!categoryMenu.getData()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const community = state.community.communities[activeCommunityId];
|
||||
return [
|
||||
{
|
||||
label: "Move category up",
|
||||
icon: UpIcon,
|
||||
onClick: () => onSwapCategory("prev", categoryMenu.getData()),
|
||||
},
|
||||
{
|
||||
label: "Move category down",
|
||||
icon: DownIcon,
|
||||
onClick: () => onSwapCategory("next", categoryMenu.getData()),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const channelMenuItems: IContextMenuItem[] = [
|
||||
{
|
||||
label: "Move up",
|
||||
icon: UpIcon,
|
||||
onClick: () => onSwapChannel("prev", channelMenu.getData()),
|
||||
},
|
||||
{
|
||||
label: "Move down",
|
||||
icon: DownIcon,
|
||||
onClick: () => onSwapChannel("next", channelMenu.getData()),
|
||||
},
|
||||
{
|
||||
label: "Remove channel",
|
||||
icon: TrashIcon,
|
||||
color: "var(--color-error)",
|
||||
onClick: () => onRemoveChannel(channelMenu.getData()),
|
||||
},
|
||||
];
|
||||
|
||||
const channelIds = createMemo(() => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
resetActiveChannel();
|
||||
resetActiveCommunity();
|
||||
return [];
|
||||
}
|
||||
|
||||
const channel = getActiveChannel();
|
||||
if (!channel) {
|
||||
resetActiveChannel();
|
||||
}
|
||||
|
||||
return community.channels ?? [];
|
||||
});
|
||||
|
||||
const channels = createMemo(() => {
|
||||
return channelIds()
|
||||
.map((channelId) => {
|
||||
const channel = state.channel.channels[channelId];
|
||||
if (!channel) {
|
||||
return {
|
||||
id: channelId,
|
||||
};
|
||||
}
|
||||
|
||||
return channel;
|
||||
})
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
});
|
||||
|
||||
const categories = createMemo<string[]>(() => {
|
||||
const categoriesDuplicate = channels()
|
||||
.map((channel) => channel.category ?? "")
|
||||
.filter((category) => category !== "");
|
||||
|
||||
return [...new Set(categoriesDuplicate)];
|
||||
});
|
||||
|
||||
const communityInfo = createMemo<ICommunity | undefined>(() => {
|
||||
const activeCommunityId = state.community.active;
|
||||
return state.community.communities[activeCommunityId ?? 0];
|
||||
|
|
@ -43,35 +141,229 @@ const ChannelView: Component = () => {
|
|||
setActiveChannel(id);
|
||||
};
|
||||
|
||||
const mapChannel = (channelId: string) => {
|
||||
const channel = state.channel.channels[channelId];
|
||||
if (!channel) {
|
||||
return undefined;
|
||||
const onCategoryRightClick = (category: string | null, e: MouseEvent) => {
|
||||
categoryMenu.open(e, category);
|
||||
};
|
||||
|
||||
const onChannelRightClick = (id: string, e: MouseEvent) => {
|
||||
channelMenu.open(e, id);
|
||||
};
|
||||
|
||||
const onRightClick = (e: MouseEvent) => {
|
||||
listMenu.open(e, undefined);
|
||||
};
|
||||
|
||||
const onCreateChannel = () => {
|
||||
setCreateChannelOpen(true);
|
||||
};
|
||||
|
||||
const onRemoveChannel = (channelId: string | undefined) => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeChannel(channelId);
|
||||
};
|
||||
|
||||
const onSwapCategory = (
|
||||
direction: "prev" | "next",
|
||||
category: string | null | undefined,
|
||||
) => {
|
||||
const chs = channels();
|
||||
|
||||
const community = getActiveCommunity();
|
||||
if (!community || !category) {
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryChannelIndex = findCategoryChannelIndex(category);
|
||||
const categoryChannel = chs[categoryChannelIndex];
|
||||
|
||||
const swapCategoryChannelIndex = findNeighbouringCategoryChannelIndex(
|
||||
categoryChannel,
|
||||
direction,
|
||||
);
|
||||
|
||||
const channelMod = chs[categoryChannelIndex];
|
||||
const channelSwap = chs[swapCategoryChannelIndex];
|
||||
|
||||
let newChannels: IChannel[] = [...chs];
|
||||
newChannels[categoryChannelIndex] = channelSwap;
|
||||
newChannels[swapCategoryChannelIndex] = channelMod;
|
||||
|
||||
finalizeChannelOrder(community.id, newChannels);
|
||||
};
|
||||
|
||||
const onSwapChannel = (
|
||||
direction: "prev" | "next",
|
||||
channelId: string | undefined,
|
||||
) => {
|
||||
const chs = channels();
|
||||
|
||||
const community = getActiveCommunity();
|
||||
if (!channelId || !community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modIndex = chs.findIndex((channel) => channel.id === channelId);
|
||||
if (modIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelMod = chs[modIndex];
|
||||
const swapIndex = findNeighbouringChannelIndexInCategory(
|
||||
channelMod,
|
||||
direction,
|
||||
);
|
||||
const channelSwap = chs[swapIndex];
|
||||
|
||||
let newChannels: IChannel[] = [...chs];
|
||||
newChannels[modIndex] = channelSwap;
|
||||
newChannels[swapIndex] = channelMod;
|
||||
|
||||
finalizeChannelOrder(community.id, newChannels);
|
||||
};
|
||||
|
||||
const findNeighbouringCategoryChannelIndex = (
|
||||
channel: IChannel,
|
||||
direction: "prev" | "next",
|
||||
): number => {
|
||||
const cats = categories();
|
||||
const categoryChannelIndex = findCategoryChannelIndex(
|
||||
channel.category ?? "",
|
||||
);
|
||||
const categoryIndex = cats.findIndex((cat) => cat === channel.category);
|
||||
|
||||
if (direction === "prev" && categoryIndex === 0) {
|
||||
return categoryChannelIndex;
|
||||
}
|
||||
if (direction === "next" && categoryIndex === cats.length - 1) {
|
||||
return categoryChannelIndex;
|
||||
}
|
||||
|
||||
const neighbouringCategory =
|
||||
cats[direction === "prev" ? categoryIndex - 1 : categoryIndex + 1];
|
||||
|
||||
return findCategoryChannelIndex(neighbouringCategory);
|
||||
};
|
||||
|
||||
const findCategoryChannelIndex = (category: string): number => {
|
||||
const channelsInCategory = channels().filter(
|
||||
(ch) => ch.category === category,
|
||||
);
|
||||
|
||||
return findChannelIndexById(channelsInCategory[0].id);
|
||||
};
|
||||
|
||||
const findNeighbouringChannelIndexInCategory = (
|
||||
channel: IChannel,
|
||||
direction: "prev" | "next",
|
||||
): number => {
|
||||
const channelsInCategory = channels().filter(
|
||||
(ch) => ch.category === channel.category,
|
||||
);
|
||||
|
||||
const modIndex = channelsInCategory.findIndex(
|
||||
(ch) => ch.id === channel.id,
|
||||
);
|
||||
if (direction === "prev" && modIndex === 0) {
|
||||
return findChannelIndexById(channel.id);
|
||||
}
|
||||
if (
|
||||
direction === "next" &&
|
||||
modIndex === channelsInCategory.length - 1
|
||||
) {
|
||||
return findChannelIndexById(channel.id);
|
||||
}
|
||||
|
||||
const neighbouringChannel =
|
||||
channelsInCategory[
|
||||
direction === "prev" ? modIndex - 1 : modIndex + 1
|
||||
];
|
||||
|
||||
return findChannelIndexById(neighbouringChannel.id);
|
||||
};
|
||||
|
||||
const findChannelIndexById = (id: string): number => {
|
||||
return channels().findIndex((channel) => channel.id === id);
|
||||
};
|
||||
|
||||
const finalizeChannelOrder = (communityId: string, chs: IChannel[]) => {
|
||||
for (let i = 0; i < chs.length; i++) {
|
||||
chs[i] = { ...chs[i], order: i };
|
||||
}
|
||||
|
||||
updateCommunityChannelOrder(
|
||||
communityId,
|
||||
chs.map((channel) => channel.id),
|
||||
);
|
||||
};
|
||||
|
||||
const mapChannel = (channel: IChannel): JSXElement => {
|
||||
return (
|
||||
<Channel
|
||||
id={channel.id}
|
||||
name={channel.name ?? ""}
|
||||
active={channel.id === state.channel.active}
|
||||
onChannelClick={onChannelClick}
|
||||
onChannelRightClick={onChannelRightClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapCategory = (category: string | null): JSXElement => {
|
||||
const mappedChannels = channels()
|
||||
.filter((channel) => channel.category === category)
|
||||
.map(mapChannel);
|
||||
|
||||
if (mappedChannels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="collapse collapse-arrow bg-stone-900 rounded-xl"
|
||||
onContextMenu={(e) => onCategoryRightClick(category, e)}
|
||||
>
|
||||
<input type="checkbox" checked={true} />
|
||||
<div class="collapse-title font-semibold opacity-50">
|
||||
{category || "Uncategorized"}
|
||||
</div>
|
||||
<div class="collapse-content">{mappedChannels}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="bg-stone-900 w-80 shadow-panel z-20 h-full relative">
|
||||
<ContextMenu
|
||||
visible={listMenu.getVisible()}
|
||||
position={listMenu.getPos()}
|
||||
items={listMenuItems}
|
||||
/>
|
||||
<ContextMenu
|
||||
visible={categoryMenu.getVisible()}
|
||||
position={categoryMenu.getPos()}
|
||||
items={categoryMenuItems()}
|
||||
/>
|
||||
<ContextMenu
|
||||
visible={channelMenu.getVisible()}
|
||||
position={channelMenu.getPos()}
|
||||
items={channelMenuItems}
|
||||
/>
|
||||
<CommunityBar
|
||||
id={communityInfo()?.id}
|
||||
name={communityInfo()?.name}
|
||||
description={communityInfo()?.description}
|
||||
avatar={
|
||||
"https://img.daisyui.com/images/profile/demo/yellingcat@192.webp"
|
||||
}
|
||||
avatar={getCommunityAvatarUrl(communityInfo()?.avatar)}
|
||||
onSettingsClick={onCommunitySettingsClick}
|
||||
/>
|
||||
<ul class="h-full list flex flex-col p-2 gap-1 pt-18 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
|
||||
{channelIds().map(mapChannel)}
|
||||
<ul
|
||||
class="h-full list flex flex-col p-2 gap-1 pt-18 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800"
|
||||
onContextMenu={onRightClick}
|
||||
>
|
||||
{categories().map(mapCategory)}
|
||||
{mapCategory(null)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import { createMemo, onMount, type Component } from "solid-js";
|
||||
import {
|
||||
createMemo,
|
||||
createSignal,
|
||||
JSXElement,
|
||||
onMount,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { ChannelBar } from "../../components/ChannelBar";
|
||||
import { MessageBar } from "../../components/MessageBar";
|
||||
import { Message } from "../../components/Message";
|
||||
import { state } from "../../store/state";
|
||||
import { IChannel, setText } from "../../store/channel";
|
||||
import { getActiveChannel, IChannel, setText } from "../../store/channel";
|
||||
import { fetchChannelMessages } from "../../services/channel";
|
||||
import { fetchUser, getUserAvatarUrl } from "../../services/user";
|
||||
import { createMessage } from "../../services/message";
|
||||
import {
|
||||
createMessage,
|
||||
removeMessage,
|
||||
updateMessage,
|
||||
} from "../../services/message";
|
||||
import { IMessage } from "../../store/message";
|
||||
import {
|
||||
createAttachment,
|
||||
|
|
@ -14,8 +24,68 @@ import {
|
|||
uploadChunks,
|
||||
} from "../../services/file";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
import {
|
||||
ContextMenu,
|
||||
IContextMenuItem,
|
||||
useContextMenu,
|
||||
} from "../../components/ContextMenu";
|
||||
import {
|
||||
EditIcon,
|
||||
PinIcon,
|
||||
ReactIcon,
|
||||
RemoveIcon,
|
||||
TrashIcon,
|
||||
} from "../../icons";
|
||||
import { getUserBestRoleId } from "../../services/role";
|
||||
import { IRole } from "../../store/role";
|
||||
|
||||
const ChatView: Component = () => {
|
||||
const menu = useContextMenu<string>("chat-message-menu");
|
||||
|
||||
const [getEditingMessages, setEditingMessages] = createSignal<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const editingMenuItems = createMemo<IContextMenuItem[]>(() => {
|
||||
if (getEditingMessages().has(menu.getData() ?? "")) {
|
||||
return [
|
||||
{
|
||||
label: "Cancel editing",
|
||||
icon: RemoveIcon,
|
||||
onClick: () => onEditMessageChange(menu.getData()),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: "Edit message",
|
||||
icon: EditIcon,
|
||||
onClick: () => onEditMessageChange(menu.getData()),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const menuItems = createMemo<IContextMenuItem[]>(() => [
|
||||
...editingMenuItems(),
|
||||
{
|
||||
label: "React",
|
||||
icon: ReactIcon,
|
||||
onClick: () => onReactMessage(menu.getData()),
|
||||
},
|
||||
{
|
||||
label: "Pin message",
|
||||
icon: PinIcon,
|
||||
onClick: () => onPinMessage(menu.getData()),
|
||||
},
|
||||
{
|
||||
label: "Remove message",
|
||||
icon: TrashIcon,
|
||||
color: "var(--color-error)",
|
||||
onClick: () => onRemoveMessage(menu.getData()),
|
||||
},
|
||||
]);
|
||||
|
||||
let scrollRef: HTMLUListElement | undefined;
|
||||
let lastScrollTop = 0;
|
||||
let autoScroll = true;
|
||||
|
|
@ -105,7 +175,7 @@ const ChatView: Component = () => {
|
|||
setText(channel.id, text);
|
||||
};
|
||||
|
||||
const onMessageSend = async (files: File[]) => {
|
||||
const onSendMessage = async (files: File[]) => {
|
||||
autoScroll = true;
|
||||
|
||||
const channel = channelInfo();
|
||||
|
|
@ -153,6 +223,65 @@ const ChatView: Component = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const onRemoveMessage = (messageId: string | undefined) => {
|
||||
const channel = getActiveChannel();
|
||||
if (!channel || !messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeMessage(messageId, channel.id);
|
||||
};
|
||||
|
||||
const onEditMessageChange = (messageId: string | undefined) => {
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editingMessages = getEditingMessages();
|
||||
if (editingMessages.has(messageId)) {
|
||||
editingMessages.delete(messageId);
|
||||
} else {
|
||||
editingMessages.add(messageId);
|
||||
}
|
||||
|
||||
setEditingMessages(new Set([...editingMessages]));
|
||||
};
|
||||
|
||||
const onEditMessageFinish = (messageId: string, text: string) => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community || !messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedText = text.trim();
|
||||
|
||||
if (updatedText.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMessage(messageId, updatedText, community.id);
|
||||
|
||||
onEditMessageChange(messageId);
|
||||
};
|
||||
|
||||
const onReactMessage = (messageId: string | undefined) => {
|
||||
const channel = getActiveChannel();
|
||||
if (!channel || !messageId) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onPinMessage = (messageId: string | undefined) => {
|
||||
const channel = getActiveChannel();
|
||||
if (!channel || !messageId) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onMessageRightClick = (id: string, e: MouseEvent) => {
|
||||
menu.open(e, id);
|
||||
};
|
||||
|
||||
const isPlainMessage = (
|
||||
message: IMessage,
|
||||
previousMessage: IMessage | undefined,
|
||||
|
|
@ -168,8 +297,50 @@ const ChatView: Component = () => {
|
|||
return message.creationDate < previousMessage.creationDate + 600000;
|
||||
};
|
||||
|
||||
const mapMessage = (
|
||||
message: IMessage,
|
||||
index: number,
|
||||
allMessages: IMessage[],
|
||||
): JSXElement => {
|
||||
const user = state.user.users[message.ownerId];
|
||||
const community = getActiveCommunity();
|
||||
if (!user || !community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bestRoleId = getUserBestRoleId(user.id, community.id);
|
||||
let bestRole: IRole | undefined;
|
||||
if (bestRoleId) {
|
||||
bestRole = state.role.roles[bestRoleId];
|
||||
}
|
||||
|
||||
return (
|
||||
<Message
|
||||
messageId={message.id ?? ""}
|
||||
message={message.text}
|
||||
userId={message.ownerId}
|
||||
username={user.nickname ?? user.username ?? ""}
|
||||
color={bestRole?.color}
|
||||
avatar={getUserAvatarUrl(user.avatar)}
|
||||
attachments={message.attachments}
|
||||
time={message.creationDate}
|
||||
plain={isPlainMessage(message, allMessages[index - 1])}
|
||||
decryptionStatus={message.decryptionStatus ?? false}
|
||||
editing={getEditingMessages().has(message.id)}
|
||||
onProfileClick={onProfileClick}
|
||||
onMessageRightClick={onMessageRightClick}
|
||||
onMessageEdit={onEditMessageFinish}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="bg-stone-800 flex-1 z-0 relative">
|
||||
<ContextMenu
|
||||
visible={menu.getVisible()}
|
||||
position={menu.getPos()}
|
||||
items={menuItems()}
|
||||
/>
|
||||
<div class="h-full">
|
||||
<ChannelBar
|
||||
id={channelInfo()?.id}
|
||||
|
|
@ -181,32 +352,13 @@ const ChatView: Component = () => {
|
|||
onScrollEnd={handleScroll}
|
||||
class="h-full list flex flex-col p-2 pt-18 pb-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800"
|
||||
>
|
||||
{messages().map((message, messageIndex, allMessages) => (
|
||||
<Message
|
||||
messageId={message.id ?? ""}
|
||||
message={message.text}
|
||||
userId={message.ownerId}
|
||||
username={
|
||||
state.user.users[message.ownerId].username ?? ""
|
||||
}
|
||||
avatar={getUserAvatarUrl(
|
||||
state.user.users[message.ownerId].avatar,
|
||||
)}
|
||||
attachments={message.attachments}
|
||||
plain={isPlainMessage(
|
||||
message,
|
||||
allMessages[messageIndex - 1],
|
||||
)}
|
||||
decryptionStatus={message.decryptionStatus ?? false}
|
||||
onProfileClick={onProfileClick}
|
||||
/>
|
||||
))}
|
||||
{messages().map(mapMessage)}
|
||||
</ul>
|
||||
{channelInfo() ? (
|
||||
<MessageBar
|
||||
text={channelInfo()?.text ?? ""}
|
||||
onChangeText={onChangeMessageText}
|
||||
onSend={onMessageSend}
|
||||
onSend={onSendMessage}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import { createSignal, type Component, type JSXElement } from "solid-js";
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
type Component,
|
||||
type JSXElement,
|
||||
} from "solid-js";
|
||||
import { ICommunitySettingsModalViewProps } from "./types";
|
||||
import CommunitySettingsProfilePage from "./pages/SettingsProfilePage/CommunitySettingsProfilePage";
|
||||
import CommunitySettingsProfilePage from "./pages/CommunitySettingsProfilePage/CommunitySettingsProfilePage";
|
||||
import CommunitySettingsChannelsPage from "./pages/CommunitySettingsChannelsPage/CommunitySettingsChannelsPage";
|
||||
import CommunitySettingsRolesPage from "./pages/CommunitySettingsRolesPage/CommunitySettingsRolesPage";
|
||||
import CommunitySettingsMembersPage from "./pages/CommunitySettingsMembersPage/CommunitySettingsMembersPage";
|
||||
import CommunitySettingsInvitesPage from "./pages/CommunitySettingsInvitesPage/CommunitySettingsInvitesPage";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { SettingsItem } from "../../components/SettingsItem";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
|
|
@ -8,12 +17,27 @@ import { getActiveCommunity } from "../../store/community";
|
|||
const CommunitySettingsModalView: Component<
|
||||
ICommunitySettingsModalViewProps
|
||||
> = (props: ICommunitySettingsModalViewProps) => {
|
||||
const [getSelectedCommunityId, setSelectedCommunityId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [getSelectedPage, setSelectedPage] = createSignal<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
const comunity = getActiveCommunity();
|
||||
if (getSelectedCommunityId() !== comunity?.id) {
|
||||
setSelectedCommunityId(comunity?.id);
|
||||
setSelectedPage();
|
||||
}
|
||||
});
|
||||
|
||||
const pages = new Map<string, Component>([
|
||||
["Community Profile", CommunitySettingsProfilePage],
|
||||
["Channels", CommunitySettingsChannelsPage],
|
||||
["Roles", CommunitySettingsRolesPage],
|
||||
["Members", CommunitySettingsMembersPage],
|
||||
["Invites", CommunitySettingsInvitesPage],
|
||||
]);
|
||||
|
||||
const getCurrentPage = (): JSXElement => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createSignal,
|
||||
JSXElement,
|
||||
onMount,
|
||||
} from "solid-js";
|
||||
import { Input } from "../../../../components/Input";
|
||||
import { state } from "../../../../store/state";
|
||||
import { IChannel } from "../../../../store/channel";
|
||||
import { SettingsItem } from "../../../../components/SettingsItem";
|
||||
import { getActiveCommunity } from "../../../../store/community";
|
||||
import { fetchCommunityChannels } from "../../../../services/community";
|
||||
import { removeChannel, updateChannel } from "../../../../services/channel";
|
||||
import { CheckIcon, HashIcon, PlusIcon, TrashIcon } from "../../../../icons";
|
||||
import { setCreateChannelOpen } from "../../../../store/app";
|
||||
import { ISelectOption, Select } from "../../../../components/Select";
|
||||
|
||||
const UNCATEGORIZED_VALUE = "Uncategorized";
|
||||
|
||||
const CommunitySettingsChannelsPage: Component = () => {
|
||||
const [getSelectedChannelId, setSelectedChannelId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [getManualCategory, setManualCategory] = createSignal<boolean>(false);
|
||||
const [getSettingsChannelName, setSettingsChannelName] =
|
||||
createSignal<string>("");
|
||||
const [getSettingsChannelDescription, setSettingsChannelDescription] =
|
||||
createSignal<string>("");
|
||||
const [getSettingsChannelCategory, setSettingsChannelCategory] =
|
||||
createSignal<string>("");
|
||||
|
||||
onMount(() => {
|
||||
if (state.community.active) {
|
||||
fetchCommunityChannels(state.community.active);
|
||||
}
|
||||
});
|
||||
|
||||
const getSelectedChannel = (): IChannel | undefined => {
|
||||
const selectedChannelId = getSelectedChannelId();
|
||||
if (!selectedChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.channel.channels[selectedChannelId];
|
||||
};
|
||||
|
||||
const onSelectChannel = (id: string) => {
|
||||
const channel = state.channel.channels[id];
|
||||
if (!channel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setSelectedChannelId(id);
|
||||
|
||||
setManualCategory(false);
|
||||
|
||||
setSettingsChannelName(channel.name ?? "");
|
||||
setSettingsChannelDescription(channel.description ?? "");
|
||||
setSettingsChannelCategory(channel.category ?? "");
|
||||
};
|
||||
|
||||
const channelIds = createMemo(() => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return community.channels ?? [];
|
||||
});
|
||||
|
||||
const channels = createMemo(() => {
|
||||
return channelIds()
|
||||
.map((channelId) => {
|
||||
const channel = state.channel.channels[channelId];
|
||||
if (!channel) {
|
||||
return {
|
||||
id: channelId,
|
||||
};
|
||||
}
|
||||
|
||||
return channel;
|
||||
})
|
||||
.filter((channel) => channel.id !== "")
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
});
|
||||
|
||||
const categoryOptions = createMemo<ISelectOption[]>(() => {
|
||||
const options = channelIds()
|
||||
.map((channelId) => {
|
||||
const category =
|
||||
state.channel.channels[channelId]?.category ?? "";
|
||||
return { value: category, label: category };
|
||||
})
|
||||
.filter((option) => option.value !== "");
|
||||
|
||||
const uncategorized = {
|
||||
value: UNCATEGORIZED_VALUE,
|
||||
label: UNCATEGORIZED_VALUE,
|
||||
};
|
||||
|
||||
const finalOptions = [uncategorized, ...options];
|
||||
|
||||
return [
|
||||
...new Map(
|
||||
finalOptions.map((option) => [option.value, option]),
|
||||
).values(),
|
||||
];
|
||||
});
|
||||
|
||||
const onAddChannel = () => {
|
||||
setCreateChannelOpen(true);
|
||||
};
|
||||
|
||||
const onSaveChannel = () => {
|
||||
const channel = getSelectedChannel();
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = getSettingsChannelName().trim();
|
||||
const description = getSettingsChannelDescription().trim();
|
||||
const category = getSettingsChannelCategory().trim();
|
||||
|
||||
updateChannel({
|
||||
id: channel.id,
|
||||
name: name.length > 0 ? name : channel.name,
|
||||
description: description,
|
||||
category: category === UNCATEGORIZED_VALUE ? null : category,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteChannel = () => {
|
||||
const channel = getSelectedChannel();
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeChannel(channel.id);
|
||||
|
||||
setSelectedChannelId();
|
||||
};
|
||||
|
||||
const mapChannel = (channel: IChannel): JSXElement => {
|
||||
return (
|
||||
<SettingsItem
|
||||
id={channel.id}
|
||||
text={`# ${channel.name ?? ""}`}
|
||||
active={channel.id === getSelectedChannelId()}
|
||||
onClick={onSelectChannel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const categoryHtml = (): JSXElement => {
|
||||
if (getManualCategory()) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter new channel category"
|
||||
value={getSettingsChannelCategory()}
|
||||
submitText="Pick existing"
|
||||
onChange={setSettingsChannelCategory}
|
||||
onSubmit={() => setManualCategory(false)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
value={getSettingsChannelCategory()}
|
||||
placeholder="Pick a category"
|
||||
options={categoryOptions()}
|
||||
submitText="Create new"
|
||||
onChange={setSettingsChannelCategory}
|
||||
onSubmit={() => setManualCategory(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const optionsHtml = (): JSXElement => {
|
||||
const channel = getSelectedChannel();
|
||||
if (!channel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-col gap-0 w-full">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
# {channel.name} Settings
|
||||
</h3>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Channel name
|
||||
</h4>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter channel name"
|
||||
icon={HashIcon}
|
||||
outline={(channel.name ?? "") !== getSettingsChannelName()}
|
||||
value={getSettingsChannelName()}
|
||||
onChange={setSettingsChannelName}
|
||||
/>
|
||||
<div class="py-3"></div>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Channel description
|
||||
</h4>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter channel description"
|
||||
outline={
|
||||
(channel.description ?? "") !==
|
||||
getSettingsChannelDescription()
|
||||
}
|
||||
value={getSettingsChannelDescription()}
|
||||
onChange={setSettingsChannelDescription}
|
||||
/>
|
||||
<div class="py-3"></div>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Channel Category
|
||||
</h4>
|
||||
{categoryHtml()}
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-14"
|
||||
onClick={onSaveChannel}
|
||||
>
|
||||
<div class="w-5">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
Save Channel
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="bg-stone-950 text-error btn btn-error rounded-xl h-14"
|
||||
onClick={onDeleteChannel}
|
||||
>
|
||||
<div class="w-5">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
Delete Channel
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Channels</h3>
|
||||
{channels().map(mapChannel)}
|
||||
{channels().length > 0 ? (
|
||||
<div class="divider"></div>
|
||||
) : undefined}
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-13"
|
||||
onClick={onAddChannel}
|
||||
>
|
||||
<div class="w-6">
|
||||
<PlusIcon />
|
||||
</div>
|
||||
Add new Channel
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunitySettingsChannelsPage;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./CommunitySettingsChannelsPage";
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import {
|
||||
createMemo,
|
||||
createSignal,
|
||||
JSXElement,
|
||||
onMount,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { state } from "../../../../store/state";
|
||||
import { RichSettingsItem } from "../../../../components/RichSettingsItem";
|
||||
import { InviteIcon, PlusIcon, TrashIcon } from "../../../../icons";
|
||||
import { fetchCommunityInvites } from "../../../../services/community";
|
||||
import { getActiveCommunity } from "../../../../store/community";
|
||||
import { removeInvite } from "../../../../services/invite";
|
||||
import { setCreateInviteOpen } from "../../../../store/app";
|
||||
import { IInvite } from "../../../../store/invite";
|
||||
|
||||
const CommunitySettingsInvitesPage: Component = () => {
|
||||
const [getSelectedInviteId, setSelectedInviteId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
if (state.community.active) {
|
||||
fetchCommunityInvites(state.community.active);
|
||||
}
|
||||
});
|
||||
|
||||
const inviteIds = createMemo(() => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return community.invites ?? [];
|
||||
});
|
||||
|
||||
const invites = createMemo(() => {
|
||||
return inviteIds()
|
||||
.map((inviteId) => {
|
||||
const invite = state.invite.invites[inviteId];
|
||||
if (!invite) {
|
||||
return {
|
||||
id: "",
|
||||
};
|
||||
}
|
||||
|
||||
return invite;
|
||||
})
|
||||
.filter((invite) => invite.id !== "");
|
||||
});
|
||||
|
||||
const onRevokeInvite = (inviteId: string) => {
|
||||
removeInvite(inviteId);
|
||||
|
||||
if (state.community.active) {
|
||||
fetchCommunityInvites(state.community.active);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateInvite = () => {
|
||||
setCreateInviteOpen(true);
|
||||
};
|
||||
|
||||
const mapInvite = (invite: IInvite): JSXElement => {
|
||||
return (
|
||||
<RichSettingsItem
|
||||
id={invite.id}
|
||||
title={invite.id}
|
||||
info={invite.valid ? undefined : "Invalid"}
|
||||
icon={InviteIcon}
|
||||
iconPadding={2}
|
||||
active={invite.id === getSelectedInviteId()}
|
||||
onClick={setSelectedInviteId}
|
||||
>
|
||||
<div class="flex flex-col gap-2 font-bold">
|
||||
<p>
|
||||
Invite ID: <span class="font-normal">{invite.id}</span>
|
||||
</p>
|
||||
{invite.hasExpiration ? (
|
||||
<p>
|
||||
Expires:{" "}
|
||||
<span class="font-normal">
|
||||
{invite.expirationDate
|
||||
? new Date(
|
||||
invite.expirationDate,
|
||||
).toLocaleString()
|
||||
: "unknown"}
|
||||
</span>
|
||||
</p>
|
||||
) : undefined}
|
||||
<p>
|
||||
Remaining Invites:{" "}
|
||||
<span class="font-normal">
|
||||
{invite.unlimitedInvites ? (
|
||||
"unlimited"
|
||||
) : (
|
||||
<>
|
||||
{invite.remainingInvites}/
|
||||
{invite.totalInvites}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Created:{" "}
|
||||
<span class="font-normal">
|
||||
{invite.creationDate
|
||||
? new Date(invite.creationDate).toLocaleString()
|
||||
: "unknown"}
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-error rounded-lg self-end mt-1 pr-2"
|
||||
onClick={() => onRevokeInvite(invite.id)}
|
||||
>
|
||||
Revoke Invite
|
||||
<div class="w-6">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</RichSettingsItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-col gap-2 w-full">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Invites</h3>
|
||||
{invites().map(mapInvite)}
|
||||
{invites().length > 0 ? <div class="divider"></div> : undefined}
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-14"
|
||||
onClick={onCreateInvite}
|
||||
>
|
||||
<div class="w-5">
|
||||
<PlusIcon />
|
||||
</div>
|
||||
Create Invite
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunitySettingsInvitesPage;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./CommunitySettingsInvitesPage";
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createSignal,
|
||||
JSXElement,
|
||||
onMount,
|
||||
} from "solid-js";
|
||||
import { state } from "../../../../store/state";
|
||||
import { SettingsItem } from "../../../../components/SettingsItem";
|
||||
import { IUser } from "../../../../store/user";
|
||||
import { getActiveCommunity } from "../../../../store/community";
|
||||
import {
|
||||
fetchCommunityMembers,
|
||||
fetchCommunityRoles,
|
||||
removeCommunityMember,
|
||||
} from "../../../../services/community";
|
||||
import { TrashIcon } from "../../../../icons";
|
||||
import { fetchUserCommunityRoles } from "../../../../services/user";
|
||||
import { ISelectOption, Select } from "../../../../components/Select";
|
||||
import { IRole } from "../../../../store/role";
|
||||
import { assignRole, unassignRole } from "../../../../services/role";
|
||||
import { BadgeItem } from "../../../../components/BadgeItem";
|
||||
|
||||
const CommunitySettingsMembersPage: Component = () => {
|
||||
const [getSelectedMemberId, setSelectedMemberId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [getSelectedRoleId, setSelectedRoleId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
onMount(() => {
|
||||
if (state.community.active) {
|
||||
fetchCommunityMembers(state.community.active);
|
||||
fetchCommunityRoles(state.community.active);
|
||||
}
|
||||
});
|
||||
|
||||
const getSelectedMember = (): IUser | undefined => {
|
||||
const selectedMemberId = getSelectedMemberId();
|
||||
if (!selectedMemberId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.user.users[selectedMemberId];
|
||||
};
|
||||
|
||||
const getSelectedRole = (): IRole | undefined => {
|
||||
const selectedRoleId = getSelectedRoleId();
|
||||
if (!selectedRoleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.role.roles[selectedRoleId];
|
||||
};
|
||||
|
||||
const onSelectMember = (id: string) => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = state.user.users[id];
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchUserCommunityRoles(member.id, community.id);
|
||||
|
||||
setSelectedMemberId(id);
|
||||
};
|
||||
|
||||
const memberIds = createMemo(() => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return community.members ?? [];
|
||||
});
|
||||
|
||||
const members = createMemo(() => {
|
||||
return memberIds()
|
||||
.map((memberId) => {
|
||||
const member = state.user.users[memberId];
|
||||
if (!member) {
|
||||
return {
|
||||
id: memberId,
|
||||
};
|
||||
}
|
||||
|
||||
return member;
|
||||
})
|
||||
.filter((member) => member.id !== "");
|
||||
});
|
||||
|
||||
const onRemoveMember = () => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = getSelectedMember();
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeCommunityMember(community.id, member.id);
|
||||
|
||||
setSelectedMemberId();
|
||||
};
|
||||
|
||||
const onUnassignRole = (id: string) => {
|
||||
const role = state.role.roles[id];
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const member = getSelectedMember();
|
||||
if (!member) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
unassignRole(role.id, member.id);
|
||||
};
|
||||
|
||||
const onAssignRole = () => {
|
||||
const role = getSelectedRole();
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const member = getSelectedMember();
|
||||
if (!member) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
assignRole(role.id, member.id);
|
||||
};
|
||||
|
||||
const mapMember = (member: IUser): JSXElement => {
|
||||
return (
|
||||
<SettingsItem
|
||||
id={member.id}
|
||||
text={member.nickname ?? ""}
|
||||
active={member.id === getSelectedMemberId()}
|
||||
onClick={onSelectMember}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapRole = (roleId: string): JSXElement => {
|
||||
const role = state.role.roles[roleId];
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = getSelectedMember();
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<BadgeItem
|
||||
id={role.id}
|
||||
text={role.name ?? ""}
|
||||
removable={true}
|
||||
onRemove={onUnassignRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const optionsHtml = (): JSXElement => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = getSelectedMember();
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleIds = (member.roles ?? {})[community.id] ?? [];
|
||||
|
||||
const roleOptions: ISelectOption[] =
|
||||
community.roles?.map((role) => ({
|
||||
value: role,
|
||||
label: state.role.roles[role]?.name ?? role,
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-col gap-0 w-full">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
{member.nickname ?? member.username} Settings
|
||||
</h3>
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{roleIds.map(mapRole)}
|
||||
</div>
|
||||
<div class="my-1"></div>
|
||||
<Select
|
||||
value={getSelectedRoleId() ?? ""}
|
||||
placeholder="Pick a role to assign"
|
||||
options={roleOptions}
|
||||
submitText="Assign"
|
||||
onChange={setSelectedRoleId}
|
||||
onSubmit={onAssignRole}
|
||||
/>
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="bg-stone-950 text-error btn btn-error rounded-xl h-14"
|
||||
onClick={onRemoveMember}
|
||||
>
|
||||
<div class="w-5">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
Kick Member
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Members</h3>
|
||||
{members().map(mapMember)}
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunitySettingsMembersPage;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./CommunitySettingsMembersPage";
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
updateCommunity,
|
||||
} from "../../../../services/community";
|
||||
import { setCommunitySettingsOpen } from "../../../../store/app";
|
||||
import { CheckIcon, TrashIcon } from "../../../../icons";
|
||||
|
||||
const CommunitySettingsProfilePage: Component = () => {
|
||||
const [getSettingsName, setSettingsName] = createSignal<string>("");
|
||||
|
|
@ -95,7 +96,7 @@ const CommunitySettingsProfilePage: Component = () => {
|
|||
|
||||
return (
|
||||
<div class="flex-1 flex flex-col gap-0 w-full">
|
||||
<h3 class="text-lg font-bold text-center mb-5">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
{getActiveCommunity()?.name} Profile
|
||||
</h3>
|
||||
<h4 class="text-sm font-semibold mb-2 text-center">
|
||||
|
|
@ -121,7 +122,7 @@ const CommunitySettingsProfilePage: Component = () => {
|
|||
value={getSettingsName()}
|
||||
onChange={setSettingsName}
|
||||
/>
|
||||
<div class="py-3"></div>
|
||||
<div class="divider py-3"></div>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Description
|
||||
</h4>
|
||||
|
|
@ -141,6 +142,9 @@ const CommunitySettingsProfilePage: Component = () => {
|
|||
class="bg-stone-950 btn rounded-xl h-14"
|
||||
onClick={onSaveProfile}
|
||||
>
|
||||
<div class="w-5">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
Save Profile
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
|
|
@ -148,6 +152,9 @@ const CommunitySettingsProfilePage: Component = () => {
|
|||
class="bg-stone-950 text-error btn btn-error rounded-xl h-14"
|
||||
onClick={onDeleteCommunity}
|
||||
>
|
||||
<div class="w-5">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
Delete Community
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
JSXElement,
|
||||
onMount,
|
||||
} from "solid-js";
|
||||
import { state } from "../../../../store/state";
|
||||
import { SettingsItem } from "../../../../components/SettingsItem";
|
||||
import { IRole } from "../../../../store/role";
|
||||
import { getActiveCommunity } from "../../../../store/community";
|
||||
import { fetchCommunityRoles } from "../../../../services/community";
|
||||
import { CheckIcon, PlusIcon, TrashIcon } from "../../../../icons";
|
||||
import { setCreateRoleOpen } from "../../../../store/app";
|
||||
import {
|
||||
fetchPermissions,
|
||||
fetchRole,
|
||||
removeRole,
|
||||
updateRole,
|
||||
} from "../../../../services/role";
|
||||
import { IPermission } from "./types";
|
||||
import { Input } from "../../../../components/Input";
|
||||
|
||||
const CommunitySettingsRolesPage: Component = () => {
|
||||
const [getSelectedRoleId, setSelectedRoleId] = createSignal<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [getSettingsRoleName, setSettingsRoleName] = createSignal<
|
||||
string | undefined
|
||||
>();
|
||||
const [getSettingsRoleDescription, setSettingsRoleDescription] =
|
||||
createSignal<string | undefined>();
|
||||
const [getSettingsRoleColor, setSettingsRoleColor] = createSignal<
|
||||
string | undefined
|
||||
>();
|
||||
const [getSettingsRoleShowInMembers, setSettingsRoleShowInMembers] =
|
||||
createSignal<boolean | undefined>();
|
||||
const [getSettingsPermissions, setSettingsPermissions] = createSignal<
|
||||
IPermission[]
|
||||
>([]);
|
||||
|
||||
onMount(() => {
|
||||
fetchPermissions();
|
||||
|
||||
if (state.community.active) {
|
||||
fetchCommunityRoles(state.community.active);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const role = getSelectedRole();
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsRoleName(role.name);
|
||||
setSettingsRoleDescription(role.description);
|
||||
setSettingsRoleColor(role.color);
|
||||
setSettingsRoleShowInMembers(role.showInMembers);
|
||||
|
||||
setSettingsPermissions(
|
||||
state.role.permissions.map((permission) => ({
|
||||
name: permission,
|
||||
value: role.permissions?.includes(permission) ?? false,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
const getSelectedRole = (): IRole | undefined => {
|
||||
const selectedRoleId = getSelectedRoleId();
|
||||
if (!selectedRoleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.role.roles[selectedRoleId];
|
||||
};
|
||||
|
||||
const onSelectRole = (id: string) => {
|
||||
const role = state.role.roles[id];
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setSelectedRoleId(id);
|
||||
|
||||
fetchRole(role.id);
|
||||
};
|
||||
|
||||
const roleIds = createMemo(() => {
|
||||
const community = getActiveCommunity();
|
||||
if (!community) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return community.roles ?? [];
|
||||
});
|
||||
|
||||
const roles = createMemo(() => {
|
||||
return roleIds()
|
||||
.map((roleId) => {
|
||||
const role = state.role.roles[roleId];
|
||||
if (!role) {
|
||||
return {
|
||||
id: "",
|
||||
};
|
||||
}
|
||||
|
||||
return role;
|
||||
})
|
||||
.filter((role) => role.id !== "")
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
});
|
||||
|
||||
const onAddRole = () => {
|
||||
setCreateRoleOpen(true);
|
||||
};
|
||||
|
||||
const onSaveRole = () => {
|
||||
const role = getSelectedRole();
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateRole({
|
||||
id: role.id,
|
||||
name: getSettingsRoleName(),
|
||||
description: getSettingsRoleDescription(),
|
||||
color: getSettingsRoleColor(),
|
||||
showInMembers: getSettingsRoleShowInMembers(),
|
||||
permissions: getSettingsPermissions()
|
||||
.filter((permission) => permission.value)
|
||||
.map((permission) => permission.name),
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteRole = () => {
|
||||
const role = getSelectedRole();
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeRole(role.id);
|
||||
|
||||
setSelectedRoleId();
|
||||
};
|
||||
|
||||
const onCheckPermission = (index: number) => {
|
||||
const permissions = getSettingsPermissions();
|
||||
|
||||
permissions[index].value = !permissions[index].value;
|
||||
|
||||
setSettingsPermissions([...permissions]);
|
||||
};
|
||||
|
||||
const onCheckShowInMembers = () => {
|
||||
const show = getSettingsRoleShowInMembers();
|
||||
setSettingsRoleShowInMembers(!show);
|
||||
};
|
||||
|
||||
const mapRole = (role: IRole): JSXElement => {
|
||||
return (
|
||||
<SettingsItem
|
||||
id={role.id}
|
||||
text={role.name ?? ""}
|
||||
active={role.id === getSelectedRoleId()}
|
||||
onClick={onSelectRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapCheckItem = (
|
||||
value: boolean,
|
||||
label: string,
|
||||
index: number,
|
||||
count: number,
|
||||
onCheck: (index: number) => void,
|
||||
): JSXElement => {
|
||||
const top = index === 0;
|
||||
const bottom = index == count - 1;
|
||||
|
||||
return (
|
||||
<li
|
||||
class={`flex flex-row h-16 px-3 border-2 transition-all bg-stone-900 hover:bg-stone-800 border-stone-800 hover:border-stone-700 rounded-sm ${top ? "rounded-t-xl" : ""} ${bottom ? "rounded-b-xl" : ""}`}
|
||||
>
|
||||
<label class="label gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox w-8 h-8 p-2 bg-stone-800 border-2 rounded-full shadow-none"
|
||||
checked={value}
|
||||
onChange={() => onCheck(index)}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const optionsHtml = (): JSXElement => {
|
||||
const role = getSelectedRole();
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-col gap-0 w-full">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
{role.name} Settings
|
||||
</h3>
|
||||
|
||||
<div class="divider py-3">Name</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter name"
|
||||
outline={getSelectedRole()?.name !== getSettingsRoleName()}
|
||||
value={getSettingsRoleName() ?? ""}
|
||||
onChange={setSettingsRoleName}
|
||||
/>
|
||||
<div class="divider py-3">Description</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter description"
|
||||
outline={
|
||||
getSelectedRole()?.description !==
|
||||
getSettingsRoleDescription()
|
||||
}
|
||||
value={getSettingsRoleDescription() ?? ""}
|
||||
onChange={setSettingsRoleDescription}
|
||||
/>
|
||||
<div class="divider py-3">Color</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter color"
|
||||
outline={
|
||||
getSelectedRole()?.color !== getSettingsRoleColor()
|
||||
}
|
||||
value={getSettingsRoleColor() ?? ""}
|
||||
onChange={setSettingsRoleColor}
|
||||
/>
|
||||
<div class="divider py-3">Permissions</div>
|
||||
<ul class="flex flex-col gap-0.5 w-full">
|
||||
{getSettingsPermissions().map(
|
||||
(permission, index, allPermissions) =>
|
||||
mapCheckItem(
|
||||
permission.value,
|
||||
permission.name,
|
||||
index,
|
||||
allPermissions.length,
|
||||
onCheckPermission,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
<div class="divider py-3">Other settings</div>
|
||||
<ul class="flex flex-col gap-0.5 w-full">
|
||||
{mapCheckItem(
|
||||
getSettingsRoleShowInMembers() ?? false,
|
||||
"Show in member list",
|
||||
0,
|
||||
1,
|
||||
onCheckShowInMembers,
|
||||
)}
|
||||
</ul>
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-14"
|
||||
onClick={onSaveRole}
|
||||
>
|
||||
<div class="w-5">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
Save Role
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="bg-stone-950 text-error btn btn-error rounded-xl h-14"
|
||||
onClick={onDeleteRole}
|
||||
>
|
||||
<div class="w-5">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
Delete Role
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex-1 flex flex-row gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-center mb-4">Roles</h3>
|
||||
{roles().map(mapRole)}
|
||||
{roles().length > 0 ? <div class="divider"></div> : undefined}
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-13"
|
||||
onClick={onAddRole}
|
||||
>
|
||||
<div class="w-6">
|
||||
<PlusIcon />
|
||||
</div>
|
||||
Add new Role
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider divider-horizontal"></div>
|
||||
<div class="flex-1">{optionsHtml()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunitySettingsRolesPage;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./CommunitySettingsRolesPage";
|
||||
export * from "./types";
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
interface IPermission {
|
||||
name: string;
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export { type IPermission };
|
||||
|
|
@ -2,7 +2,7 @@ import { createMemo, type Component } from "solid-js";
|
|||
import { Community } from "../../components/Community";
|
||||
import { state } from "../../store/state";
|
||||
import { SidebarItem } from "../../components/SidebarItem";
|
||||
import { HomeIcon, PlusIcon, SettingsIcon } from "../../icons";
|
||||
import { HomeIcon, LeaveIcon, PlusIcon, SettingsIcon } from "../../icons";
|
||||
import {
|
||||
setAddCommunityOpen,
|
||||
setHomeOpen,
|
||||
|
|
@ -15,10 +15,28 @@ import {
|
|||
import {
|
||||
fetchCommunity,
|
||||
getCommunityAvatarUrl,
|
||||
removeCommunityMember,
|
||||
} from "../../services/community";
|
||||
import { resetActiveChannel } from "../../store/channel";
|
||||
import {
|
||||
ContextMenu,
|
||||
IContextMenuItem,
|
||||
useContextMenu,
|
||||
} from "../../components/ContextMenu";
|
||||
import { getLoggedInUser } from "../../store/user";
|
||||
|
||||
const CommunityView: Component = () => {
|
||||
const menu = useContextMenu<string>("community-community-menu");
|
||||
|
||||
const menuItems: IContextMenuItem[] = [
|
||||
{
|
||||
label: "Leave community",
|
||||
icon: LeaveIcon,
|
||||
color: "var(--color-error)",
|
||||
onClick: () => onLeaveCommunity(menu.getData()),
|
||||
},
|
||||
];
|
||||
|
||||
const communityIds = createMemo(() => {
|
||||
const loggedUserId = state.user.loggedUserId;
|
||||
if (!loggedUserId) {
|
||||
|
|
@ -48,6 +66,10 @@ const CommunityView: Component = () => {
|
|||
resetActiveChannel();
|
||||
};
|
||||
|
||||
const onCommunityRightClick = (id: string, e: MouseEvent) => {
|
||||
menu.open(e, id);
|
||||
};
|
||||
|
||||
const onNewClick = () => {
|
||||
setAddCommunityOpen(true);
|
||||
};
|
||||
|
|
@ -56,6 +78,17 @@ const CommunityView: Component = () => {
|
|||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const onLeaveCommunity = (communityId: string | undefined) => {
|
||||
if (!communityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = getLoggedInUser();
|
||||
if (user) {
|
||||
removeCommunityMember(communityId, user.id);
|
||||
}
|
||||
};
|
||||
|
||||
const mapCommunity = (communityId: string) => {
|
||||
const community = state.community.communities[communityId];
|
||||
if (!community) {
|
||||
|
|
@ -69,12 +102,18 @@ const CommunityView: Component = () => {
|
|||
avatar={getCommunityAvatarUrl(community.avatar)}
|
||||
active={community.id === state.community.active}
|
||||
onCommunityClick={onCommunityClick}
|
||||
onCommunityRightClick={onCommunityRightClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col bg-stone-950 w-16 h-full shadow-panel z-30 p-2 gap-2">
|
||||
<ContextMenu
|
||||
visible={menu.getVisible()}
|
||||
position={menu.getPos()}
|
||||
items={menuItems}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={HomeIcon}
|
||||
active={state.app.homeOpen}
|
||||
|
|
|
|||
58
src/views/CreateChannelModalView/CreateChannelModalView.tsx
Normal file
58
src/views/CreateChannelModalView/CreateChannelModalView.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { createSignal, type Component } from "solid-js";
|
||||
import { ICreateChannelModalViewProps } from "./types";
|
||||
import { Input } from "../../components/Input";
|
||||
import { createChannel } from "../../services/channel";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
|
||||
const CreateChannelModalView: Component<ICreateChannelModalViewProps> = (
|
||||
props: ICreateChannelModalViewProps,
|
||||
) => {
|
||||
const [getChannelName, setChannelName] = createSignal("");
|
||||
|
||||
const onCreateChannel = () => {
|
||||
const channelName = getChannelName();
|
||||
if (!channelName || channelName.trim().length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const community = getActiveCommunity();
|
||||
if (!community?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
createChannel(channelName, community.id);
|
||||
|
||||
setChannelName("");
|
||||
|
||||
props.onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dialog
|
||||
ref={props.dialogRef}
|
||||
class="modal outline-none bg-[#00000050]"
|
||||
>
|
||||
<div class="modal-box bg-stone-950 rounded-3xl">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
Create a new Channel
|
||||
</h3>
|
||||
<Input
|
||||
placeholder="Enter name for the new channel"
|
||||
value={getChannelName()}
|
||||
onChange={setChannelName}
|
||||
submitText="Create"
|
||||
onSubmit={onCreateChannel}
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
onClick={props.onClose}
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
></form>
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateChannelModalView;
|
||||
2
src/views/CreateChannelModalView/index.ts
Normal file
2
src/views/CreateChannelModalView/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./CreateChannelModalView";
|
||||
export * from "./types";
|
||||
6
src/views/CreateChannelModalView/types.ts
Normal file
6
src/views/CreateChannelModalView/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface ICreateChannelModalViewProps {
|
||||
dialogRef?: (element: HTMLDialogElement) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export { type ICreateChannelModalViewProps };
|
||||
93
src/views/CreateInviteModalView/CreateInviteModalView.tsx
Normal file
93
src/views/CreateInviteModalView/CreateInviteModalView.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { createSignal, type Component } from "solid-js";
|
||||
import { ICreateInviteModalViewProps } from "./types";
|
||||
import { Input } from "../../components/Input";
|
||||
import { getActiveCommunity } from "../../store/community";
|
||||
import { createCommunityInvite } from "../../services/community";
|
||||
import { PlusIcon } from "../../icons";
|
||||
|
||||
const CreateInviteModalView: Component<ICreateInviteModalViewProps> = (
|
||||
props: ICreateInviteModalViewProps,
|
||||
) => {
|
||||
const [getInviteCount, setInviteCount] = createSignal("");
|
||||
const [getInviteExpiration, setInviteExpiration] = createSignal("");
|
||||
|
||||
const onCreateInvite = () => {
|
||||
const inviteCount = getInviteCount();
|
||||
const inviteExpiration = getInviteExpiration();
|
||||
|
||||
const community = getActiveCommunity();
|
||||
if (!community?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteCountNum = Number(inviteCount);
|
||||
const inviteExpirationNum = new Date(inviteExpiration).getTime();
|
||||
|
||||
createCommunityInvite({
|
||||
communityId: community.id,
|
||||
totalInvites:
|
||||
isNaN(inviteCountNum) || inviteCountNum < 1
|
||||
? undefined
|
||||
: inviteCountNum,
|
||||
expirationDate: isNaN(inviteExpirationNum)
|
||||
? undefined
|
||||
: inviteExpirationNum,
|
||||
});
|
||||
|
||||
setInviteCount("");
|
||||
setInviteExpiration("");
|
||||
|
||||
props.onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dialog
|
||||
ref={props.dialogRef}
|
||||
class="modal outline-none bg-[#00000050]"
|
||||
>
|
||||
<div class="modal-box bg-stone-950 rounded-3xl flex flex-col">
|
||||
<h3 class="text-lg font-bold text-center mb-6">
|
||||
Create a new Invite
|
||||
</h3>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Invites (optional)
|
||||
</h4>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter how many invites to create"
|
||||
value={getInviteCount()}
|
||||
onChange={setInviteCount}
|
||||
/>
|
||||
<div class="divider my-4"></div>
|
||||
<h4 class="text-sm font-semibold mb-1 mx-7 text-left">
|
||||
Expiration (optional)
|
||||
</h4>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder="Enter expiration date for the invite"
|
||||
value={getInviteExpiration()}
|
||||
onChange={setInviteExpiration}
|
||||
/>
|
||||
<div class="divider my-4"></div>
|
||||
<button
|
||||
class="bg-stone-950 btn rounded-xl h-14"
|
||||
onClick={onCreateInvite}
|
||||
>
|
||||
<div class="w-5">
|
||||
<PlusIcon />
|
||||
</div>
|
||||
Create Invite
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
onClick={props.onClose}
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
></form>
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateInviteModalView;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue