Added end to end encryption

This commit is contained in:
Aslan 2026-01-13 17:33:23 -05:00
parent 9153ba841d
commit 575e9e2010
131 changed files with 2289 additions and 1670 deletions

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "pulsar-web", "name": "pulsar-web",
"version": "0.3.0", "version": "0.5.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pulsar-web", "name": "pulsar-web",
"version": "0.3.0", "version": "0.5.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@solidjs/router": "^0.15.4", "@solidjs/router": "^0.15.4",

View file

@ -1,6 +1,6 @@
{ {
"name": "pulsar-web", "name": "pulsar-web",
"version": "0.4.0", "version": "0.5.2",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {

View file

@ -1,21 +1,29 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchRegisterRequest,
IFetchRegisterResponse,
IFetchLoginRequest, IFetchLoginRequest,
IFetchLoginResponse, IFetchLoginResponse,
IFetchRefreshResponseError, IFetchRefreshResponse,
IFetchRefreshResponseSuccess,
} from "./types"; } from "./types";
const fetchRegisterApi = async (
request: IFetchRegisterRequest,
): Promise<IFetchRegisterResponse | IResponseError> => {
return await callApi(HTTP.POST, `auth/register`, request, true);
};
const fetchLoginApi = async ( const fetchLoginApi = async (
request: IFetchLoginRequest, request: IFetchLoginRequest,
): Promise<IFetchLoginResponse> => { ): Promise<IFetchLoginResponse | IResponseError> => {
return await callApi(HTTP.POST, `auth/login`, request, true); return await callApi(HTTP.POST, `auth/login`, request, true);
}; };
const fetchRefreshApi = async (): Promise< const fetchRefreshApi = async (): Promise<
IFetchRefreshResponseError | IFetchRefreshResponseSuccess IFetchRefreshResponse | IResponseError
> => { > => {
return await callApi(HTTP.GET, `auth/refresh`, undefined, true); return await callApi(HTTP.GET, `auth/refresh`, undefined, true);
}; };
export { fetchLoginApi, fetchRefreshApi }; export { fetchRegisterApi, fetchLoginApi, fetchRefreshApi };

View file

@ -1,26 +1,37 @@
import { IResponseSuccess } from "../types";
interface IFetchRegisterRequest {
username: string;
password: string;
email: string;
}
interface IFetchRegisterResponse extends IResponseSuccess {
id: string;
ownerId: string;
}
interface IFetchLoginRequest { interface IFetchLoginRequest {
username: string; username: string;
password: string; password: string;
} }
interface IFetchLoginResponse { interface IFetchLoginResponse extends IResponseSuccess {
id: string; id: string;
ownerId: string; ownerId: string;
} }
interface IFetchRefreshResponseError { interface IFetchRefreshResponse extends IResponseSuccess {
error: string;
}
interface IFetchRefreshResponseSuccess {
id: string; id: string;
ownerId: string; ownerId: string;
token: string; token: string;
storageSecret: string;
} }
export { export {
type IFetchRegisterRequest,
type IFetchRegisterResponse,
type IFetchLoginRequest, type IFetchLoginRequest,
type IFetchLoginResponse, type IFetchLoginResponse,
type IFetchRefreshResponseError, type IFetchRefreshResponse,
type IFetchRefreshResponseSuccess,
}; };

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchChannelRequest, IFetchChannelRequest,
IFetchChannelResponse, IFetchChannelResponse,
@ -14,31 +15,31 @@ import {
const fetchChannelApi = async ( const fetchChannelApi = async (
request: IFetchChannelRequest, request: IFetchChannelRequest,
): Promise<IFetchChannelResponse> => { ): Promise<IFetchChannelResponse | IResponseError> => {
return await callApi(HTTP.GET, `channel/${request.id}`); return await callApi(HTTP.GET, `channel/${request.id}`);
}; };
const createChannelApi = async ( const createChannelApi = async (
request: ICreateChannelRequest, request: ICreateChannelRequest,
): Promise<ICreateChannelResponse> => { ): Promise<ICreateChannelResponse | IResponseError> => {
return await callApi(HTTP.POST, `channel`, request); return await callApi(HTTP.POST, `channel`, request);
}; };
const updateChannelApi = async ( const updateChannelApi = async (
request: IUpdateChannelRequest, request: IUpdateChannelRequest,
): Promise<IUpdateChannelResponse> => { ): Promise<IUpdateChannelResponse | IResponseError> => {
return await callApi(HTTP.PATCH, `channel/${request.id}`, request); return await callApi(HTTP.PATCH, `channel/${request.id}`, request);
}; };
const removeChannelApi = async ( const removeChannelApi = async (
request: IRemoveChannelRequest, request: IRemoveChannelRequest,
): Promise<IRemoveChannelResponse> => { ): Promise<IRemoveChannelResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `channel/${request.id}`); return await callApi(HTTP.DELETE, `channel/${request.id}`);
}; };
const fetchChannelMessagesApi = async ( const fetchChannelMessagesApi = async (
request: IFetchChannelMessagesRequest, request: IFetchChannelMessagesRequest,
): Promise<IFetchChannelMessagesResponse> => { ): Promise<IFetchChannelMessagesResponse | IResponseError> => {
return await callApi(HTTP.GET, `channel/${request.id}/messages`); return await callApi(HTTP.GET, `channel/${request.id}/messages`);
}; };

View file

@ -1,3 +1,5 @@
import { IResponseSuccess } from "../types";
interface IFetchChannel { interface IFetchChannel {
id: string; id: string;
name: string; name: string;
@ -10,14 +12,14 @@ interface IFetchChannelRequest {
id: string; id: string;
} }
interface IFetchChannelResponse extends IFetchChannel {} interface IFetchChannelResponse extends IResponseSuccess, IFetchChannel {}
interface ICreateChannelRequest { interface ICreateChannelRequest {
name: string; name: string;
communityId: string; communityId: string;
} }
interface ICreateChannelResponse extends IFetchChannel {} interface ICreateChannelResponse extends IResponseSuccess, IFetchChannel {}
interface IUpdateChannelRequest { interface IUpdateChannelRequest {
id: string; id: string;
@ -25,13 +27,13 @@ interface IUpdateChannelRequest {
description?: string; description?: string;
} }
interface IUpdateChannelResponse extends IFetchChannel {} interface IUpdateChannelResponse extends IResponseSuccess, IFetchChannel {}
interface IRemoveChannelRequest { interface IRemoveChannelRequest {
id: string; id: string;
} }
interface IRemoveChannelResponse { interface IRemoveChannelResponse extends IResponseSuccess {
id: string; id: string;
communityId: string; communityId: string;
} }
@ -40,7 +42,7 @@ interface IFetchChannelMessagesRequest {
id: string; id: string;
} }
interface IFetchChannelMessagesResponse { interface IFetchChannelMessagesResponse extends IResponseSuccess {
id: string; id: string;
messages: IFetchChannelMessage[]; messages: IFetchChannelMessage[];
} }
@ -48,6 +50,7 @@ interface IFetchChannelMessagesResponse {
interface IFetchChannelMessage { interface IFetchChannelMessage {
id: string; id: string;
text: string; text: string;
iv: string;
edited: boolean; edited: boolean;
ownerId: string; ownerId: string;
creationDate: number; creationDate: number;

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchCommunityRequest, IFetchCommunityRequest,
IFetchCommunityResponse, IFetchCommunityResponse,
@ -20,49 +21,49 @@ import {
const fetchCommunityApi = async ( const fetchCommunityApi = async (
request: IFetchCommunityRequest, request: IFetchCommunityRequest,
): Promise<IFetchCommunityResponse> => { ): Promise<IFetchCommunityResponse | IResponseError> => {
return await callApi(HTTP.GET, `community/${request.id}`); return await callApi(HTTP.GET, `community/${request.id}`);
}; };
const createCommunityApi = async ( const createCommunityApi = async (
request: ICreateCommunityRequest, request: ICreateCommunityRequest,
): Promise<ICreateCommunityResponse> => { ): Promise<ICreateCommunityResponse | IResponseError> => {
return await callApi(HTTP.POST, `community`, request); return await callApi(HTTP.POST, `community`, request);
}; };
const updateCommunityApi = async ( const updateCommunityApi = async (
request: IUpdateCommunityRequest, request: IUpdateCommunityRequest,
): Promise<IUpdateCommunityResponse> => { ): Promise<IUpdateCommunityResponse | IResponseError> => {
return await callApi(HTTP.PATCH, `community/${request.id}`, request); return await callApi(HTTP.PATCH, `community/${request.id}`, request);
}; };
const removeCommunityApi = async ( const removeCommunityApi = async (
request: IRemoveCommunityRequest, request: IRemoveCommunityRequest,
): Promise<IRemoveCommunityResponse> => { ): Promise<IRemoveCommunityResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `community/${request.id}`); return await callApi(HTTP.DELETE, `community/${request.id}`);
}; };
const fetchCommunityChannelsApi = async ( const fetchCommunityChannelsApi = async (
request: IFetchCommunityChannelsRequest, request: IFetchCommunityChannelsRequest,
): Promise<IFetchCommunityChannelsResponse> => { ): Promise<IFetchCommunityChannelsResponse | IResponseError> => {
return await callApi(HTTP.GET, `community/${request.id}/channels`); return await callApi(HTTP.GET, `community/${request.id}/channels`);
}; };
const fetchCommunityRolesApi = async ( const fetchCommunityRolesApi = async (
request: IFetchCommunityRolesRequest, request: IFetchCommunityRolesRequest,
): Promise<IFetchCommunityRolesResponse> => { ): Promise<IFetchCommunityRolesResponse | IResponseError> => {
return await callApi(HTTP.GET, `community/${request.id}/roles`); return await callApi(HTTP.GET, `community/${request.id}/roles`);
}; };
const fetchCommunityMembersApi = async ( const fetchCommunityMembersApi = async (
request: IFetchCommunityMembersRequest, request: IFetchCommunityMembersRequest,
): Promise<IFetchCommunityMembersResponse> => { ): Promise<IFetchCommunityMembersResponse | IResponseError> => {
return await callApi(HTTP.GET, `community/${request.id}/members`); return await callApi(HTTP.GET, `community/${request.id}/members`);
}; };
const fetchCommunityInvitesApi = async ( const fetchCommunityInvitesApi = async (
request: IFetchCommunityInvitesRequest, request: IFetchCommunityInvitesRequest,
): Promise<IFetchCommunityInvitesResponse> => { ): Promise<IFetchCommunityInvitesResponse | IResponseError> => {
return await callApi(HTTP.GET, `community/${request.id}/invites`); return await callApi(HTTP.GET, `community/${request.id}/invites`);
}; };

View file

@ -1,3 +1,5 @@
import { IResponseSuccess } from "../types";
interface IFetchCommunity { interface IFetchCommunity {
id: string; id: string;
name: string; name: string;
@ -10,13 +12,13 @@ interface IFetchCommunityRequest {
id: string; id: string;
} }
interface IFetchCommunityResponse extends IFetchCommunity {} interface IFetchCommunityResponse extends IResponseSuccess, IFetchCommunity {}
interface ICreateCommunityRequest { interface ICreateCommunityRequest {
name: string; name: string;
} }
interface ICreateCommunityResponse extends IFetchCommunity {} interface ICreateCommunityResponse extends IResponseSuccess, IFetchCommunity {}
interface IUpdateCommunityRequest { interface IUpdateCommunityRequest {
id: string; id: string;
@ -24,13 +26,13 @@ interface IUpdateCommunityRequest {
description?: string; description?: string;
} }
interface IUpdateCommunityResponse extends IFetchCommunity {} interface IUpdateCommunityResponse extends IResponseSuccess, IFetchCommunity {}
interface IRemoveCommunityRequest { interface IRemoveCommunityRequest {
id: string; id: string;
} }
interface IRemoveCommunityResponse { interface IRemoveCommunityResponse extends IResponseSuccess {
id: string; id: string;
} }
@ -38,7 +40,7 @@ interface IFetchCommunityChannelsRequest {
id: string; id: string;
} }
interface IFetchCommunityChannelsResponse { interface IFetchCommunityChannelsResponse extends IResponseSuccess {
id: string; id: string;
channels: IFetchCommunityChannel[]; channels: IFetchCommunityChannel[];
} }
@ -52,7 +54,7 @@ interface IFetchCommunityRolesRequest {
id: string; id: string;
} }
interface IFetchCommunityRolesResponse { interface IFetchCommunityRolesResponse extends IResponseSuccess {
id: string; id: string;
roles: IFetchCommunityRole[]; roles: IFetchCommunityRole[];
} }
@ -66,7 +68,7 @@ interface IFetchCommunityMembersRequest {
id: string; id: string;
} }
interface IFetchCommunityMembersResponse { interface IFetchCommunityMembersResponse extends IResponseSuccess {
id: string; id: string;
members: IFetchCommunityMember[]; members: IFetchCommunityMember[];
} }
@ -80,7 +82,7 @@ interface IFetchCommunityInvitesRequest {
id: string; id: string;
} }
interface IFetchCommunityInvitesResponse { interface IFetchCommunityInvitesResponse extends IResponseSuccess {
id: string; id: string;
invites: IFetchCommunityInvite[]; invites: IFetchCommunityInvite[];
} }

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchInviteRequest, IFetchInviteRequest,
IFetchInviteResponse, IFetchInviteResponse,
@ -10,19 +11,19 @@ import {
const fetchInviteApi = async ( const fetchInviteApi = async (
request: IFetchInviteRequest, request: IFetchInviteRequest,
): Promise<IFetchInviteResponse> => { ): Promise<IFetchInviteResponse | IResponseError> => {
return await callApi(HTTP.GET, `invite/${request.id}`); return await callApi(HTTP.GET, `invite/${request.id}`);
}; };
const removeInviteApi = async ( const removeInviteApi = async (
request: IRemoveInviteRequest, request: IRemoveInviteRequest,
): Promise<IRemoveInviteResponse> => { ): Promise<IRemoveInviteResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `invite/${request.id}`); return await callApi(HTTP.DELETE, `invite/${request.id}`);
}; };
const acceptInviteApi = async ( const acceptInviteApi = async (
request: IAcceptInviteRequest, request: IAcceptInviteRequest,
): Promise<IAcceptInviteResponse> => { ): Promise<IAcceptInviteResponse | IResponseError> => {
return await callApi(HTTP.GET, `invite/${request.id}/accept`); return await callApi(HTTP.GET, `invite/${request.id}/accept`);
}; };

View file

@ -1,3 +1,5 @@
import { IResponseSuccess } from "../types";
interface IFetchInvite { interface IFetchInvite {
id: string; id: string;
communityId: string; communityId: string;
@ -14,13 +16,13 @@ interface IFetchInviteRequest {
id: string; id: string;
} }
interface IFetchInviteResponse extends IFetchInvite {} interface IFetchInviteResponse extends IResponseSuccess, IFetchInvite {}
interface IRemoveInviteRequest { interface IRemoveInviteRequest {
id: string; id: string;
} }
interface IRemoveInviteResponse { interface IRemoveInviteResponse extends IResponseSuccess {
id: string; id: string;
userId: string; userId: string;
} }
@ -29,7 +31,7 @@ interface IAcceptInviteRequest {
id: string; id: string;
} }
interface IAcceptInviteResponse { interface IAcceptInviteResponse extends IResponseSuccess {
id: string; id: string;
userId: string; userId: string;
userName: string; userName: string;

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchMessageRequest, IFetchMessageRequest,
IFetchMessageResponse, IFetchMessageResponse,
@ -12,25 +13,25 @@ import {
const fetchMessageApi = async ( const fetchMessageApi = async (
request: IFetchMessageRequest, request: IFetchMessageRequest,
): Promise<IFetchMessageResponse> => { ): Promise<IFetchMessageResponse | IResponseError> => {
return await callApi(HTTP.GET, `message/${request.id}`); return await callApi(HTTP.GET, `message/${request.id}`);
}; };
const createMessageApi = async ( const createMessageApi = async (
request: ICreateMessageRequest, request: ICreateMessageRequest,
): Promise<ICreateMessageResponse> => { ): Promise<ICreateMessageResponse | IResponseError> => {
return await callApi(HTTP.POST, `message`, request); return await callApi(HTTP.POST, `message`, request);
}; };
const updateMessageApi = async ( const updateMessageApi = async (
request: IUpdateMessageRequest, request: IUpdateMessageRequest,
): Promise<IUpdateMessageResponse> => { ): Promise<IUpdateMessageResponse | IResponseError> => {
return await callApi(HTTP.PATCH, `message/${request.id}`, request); return await callApi(HTTP.PATCH, `message/${request.id}`, request);
}; };
const removeMessageApi = async ( const removeMessageApi = async (
request: IRemoveMessageRequest, request: IRemoveMessageRequest,
): Promise<IRemoveMessageResponse> => { ): Promise<IRemoveMessageResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `message/${request.id}`); return await callApi(HTTP.DELETE, `message/${request.id}`);
}; };

View file

@ -1,6 +1,9 @@
import { IResponseSuccess } from "../types";
interface IFetchMessage { interface IFetchMessage {
id: string; id: string;
text: string; text: string;
iv: string;
editHistory: string[]; editHistory: string[];
edited: boolean; edited: boolean;
ownerId: string; ownerId: string;
@ -12,27 +15,28 @@ interface IFetchMessageRequest {
id: string; id: string;
} }
interface IFetchMessageResponse extends IFetchMessage {} interface IFetchMessageResponse extends IResponseSuccess, IFetchMessage {}
interface ICreateMessageRequest { interface ICreateMessageRequest {
text: string; text: string;
iv: string;
channelId: string; channelId: string;
} }
interface ICreateMessageResponse extends IFetchMessage {} interface ICreateMessageResponse extends IResponseSuccess, IFetchMessage {}
interface IUpdateMessageRequest { interface IUpdateMessageRequest {
id: string; id: string;
text: string; text: string;
} }
interface IUpdateMessageResponse extends IFetchMessage {} interface IUpdateMessageResponse extends IResponseSuccess, IFetchMessage {}
interface IRemoveMessageRequest { interface IRemoveMessageRequest {
id: string; id: string;
} }
interface IRemoveMessageResponse { interface IRemoveMessageResponse extends IResponseSuccess {
id: string; id: string;
ownerId: string; ownerId: string;
channelId: string; channelId: string;

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchRoleRequest, IFetchRoleRequest,
IFetchRoleResponse, IFetchRoleResponse,
@ -12,25 +13,25 @@ import {
const fetchRoleApi = async ( const fetchRoleApi = async (
request: IFetchRoleRequest, request: IFetchRoleRequest,
): Promise<IFetchRoleResponse> => { ): Promise<IFetchRoleResponse | IResponseError> => {
return await callApi(HTTP.GET, `role/${request.id}`); return await callApi(HTTP.GET, `role/${request.id}`);
}; };
const createRoleApi = async ( const createRoleApi = async (
request: ICreateRoleRequest, request: ICreateRoleRequest,
): Promise<ICreateRoleResponse> => { ): Promise<ICreateRoleResponse | IResponseError> => {
return await callApi(HTTP.POST, `role`, request); return await callApi(HTTP.POST, `role`, request);
}; };
const updateRoleApi = async ( const updateRoleApi = async (
request: IUpdateRoleRequest, request: IUpdateRoleRequest,
): Promise<IUpdateRoleResponse> => { ): Promise<IUpdateRoleResponse | IResponseError> => {
return await callApi(HTTP.PATCH, `role/${request.id}`, request); return await callApi(HTTP.PATCH, `role/${request.id}`, request);
}; };
const removeRoleApi = async ( const removeRoleApi = async (
request: IRemoveRoleRequest, request: IRemoveRoleRequest,
): Promise<IRemoveRoleResponse> => { ): Promise<IRemoveRoleResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `role/${request.id}`); return await callApi(HTTP.DELETE, `role/${request.id}`);
}; };

View file

@ -1,3 +1,5 @@
import { IResponseSuccess } from "../types";
interface IFetchRole { interface IFetchRole {
id: string; id: string;
name: string; name: string;
@ -11,27 +13,27 @@ interface IFetchRoleRequest {
id: string; id: string;
} }
interface IFetchRoleResponse extends IFetchRole {} interface IFetchRoleResponse extends IResponseSuccess, IFetchRole {}
interface ICreateRoleRequest { interface ICreateRoleRequest {
name: string; name: string;
communityId: string; communityId: string;
} }
interface ICreateRoleResponse extends IFetchRole {} interface ICreateRoleResponse extends IResponseSuccess, IFetchRole {}
interface IUpdateRoleRequest { interface IUpdateRoleRequest {
id: string; id: string;
name?: string; name?: string;
} }
interface IUpdateRoleResponse extends IFetchRole {} interface IUpdateRoleResponse extends IResponseSuccess, IFetchRole {}
interface IRemoveRoleRequest { interface IRemoveRoleRequest {
id: string; id: string;
} }
interface IRemoveRoleResponse { interface IRemoveRoleResponse extends IResponseSuccess {
id: string; id: string;
communityId: string; communityId: string;
} }

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchSessionRequest, IFetchSessionRequest,
IFetchSessionResponse, IFetchSessionResponse,
@ -8,13 +9,13 @@ import {
const fetchSessionApi = async ( const fetchSessionApi = async (
request: IFetchSessionRequest, request: IFetchSessionRequest,
): Promise<IFetchSessionResponse> => { ): Promise<IFetchSessionResponse | IResponseError> => {
return await callApi(HTTP.GET, `session/${request.id}`); return await callApi(HTTP.GET, `session/${request.id}`);
}; };
const removeSessionApi = async ( const removeSessionApi = async (
request: IRemoveSessionRequest, request: IRemoveSessionRequest,
): Promise<IRemoveSessionResponse> => { ): Promise<IRemoveSessionResponse | IResponseError> => {
return await callApi(HTTP.DELETE, `session/${request.id}`); return await callApi(HTTP.DELETE, `session/${request.id}`);
}; };

View file

@ -1,20 +1,25 @@
import { IResponseSuccess } from "../types";
interface IFetchSession { interface IFetchSession {
id: string; id: string;
userId: string; userId: string;
name: string;
userAgent: string;
creationDate: number; creationDate: number;
refreshDate: number;
} }
interface IFetchSessionRequest { interface IFetchSessionRequest {
id: string; id: string;
} }
interface IFetchSessionResponse extends IFetchSession {} interface IFetchSessionResponse extends IResponseSuccess, IFetchSession {}
interface IRemoveSessionRequest { interface IRemoveSessionRequest {
id: string; id: string;
} }
interface IRemoveSessionResponse { interface IRemoveSessionResponse extends IResponseSuccess {
id: string; id: string;
userId: string; userId: string;
} }

9
src/api/types.ts Normal file
View file

@ -0,0 +1,9 @@
interface IResponseError {
error: string;
}
interface IResponseSuccess {
error: null;
}
export { type IResponseError, type IResponseSuccess };

View file

@ -1,3 +1,5 @@
import { IResponseSuccess } from "../types";
interface IFetchUser { interface IFetchUser {
id: string; id: string;
username: string; username: string;
@ -8,7 +10,7 @@ interface IFetchUser {
lastLogin: number; lastLogin: number;
} }
interface IFetchLoggedUserResponse { interface IFetchLoggedUserResponse extends IResponseSuccess {
id: string; id: string;
} }
@ -16,13 +18,13 @@ interface IFetchUserRequest {
id: string; id: string;
} }
interface IFetchUserResponse extends IFetchUser {} interface IFetchUserResponse extends IResponseSuccess, IFetchUser {}
interface IFetchUserSessionsRequest { interface IFetchUserSessionsRequest {
id: string; id: string;
} }
interface IFetchUserSessionsResponse { interface IFetchUserSessionsResponse extends IResponseSuccess {
id: string; id: string;
sessions: IFetchUserSession[]; sessions: IFetchUserSession[];
} }
@ -30,13 +32,17 @@ interface IFetchUserSessionsResponse {
interface IFetchUserSession { interface IFetchUserSession {
id: string; id: string;
userId: string; userId: string;
name: string;
userAgent: string;
creationDate: number;
refreshDate: number;
} }
interface IFetchUserCommunitiesRequest { interface IFetchUserCommunitiesRequest {
id: string; id: string;
} }
interface IFetchUserCommunitiesResponse { interface IFetchUserCommunitiesResponse extends IResponseSuccess {
id: string; id: string;
communities: IFetchUserCommunity[]; communities: IFetchUserCommunity[];
} }

View file

@ -1,4 +1,5 @@
import { callApi, HTTP } from "../tools"; import { callApi, HTTP } from "../tools";
import { IResponseError } from "../types";
import { import {
IFetchLoggedUserResponse, IFetchLoggedUserResponse,
IFetchUserRequest, IFetchUserRequest,
@ -9,25 +10,27 @@ import {
IFetchUserCommunitiesResponse, IFetchUserCommunitiesResponse,
} from "./types"; } from "./types";
const fetchLoggedUserApi = async (): Promise<IFetchLoggedUserResponse> => { const fetchLoggedUserApi = async (): Promise<
IFetchLoggedUserResponse | IResponseError
> => {
return await callApi(HTTP.GET, `user/logged`); return await callApi(HTTP.GET, `user/logged`);
}; };
const fetchUserApi = async ( const fetchUserApi = async (
request: IFetchUserRequest, request: IFetchUserRequest,
): Promise<IFetchUserResponse> => { ): Promise<IFetchUserResponse | IResponseError> => {
return await callApi(HTTP.GET, `user/${request.id}`); return await callApi(HTTP.GET, `user/${request.id}`);
}; };
const fetchUserSessionsApi = async ( const fetchUserSessionsApi = async (
request: IFetchUserSessionsRequest, request: IFetchUserSessionsRequest,
): Promise<IFetchUserSessionsResponse> => { ): Promise<IFetchUserSessionsResponse | IResponseError> => {
return await callApi(HTTP.GET, `user/${request.id}/sessions`); return await callApi(HTTP.GET, `user/${request.id}/sessions`);
}; };
const fetchUserCommunitiesApi = async ( const fetchUserCommunitiesApi = async (
request: IFetchUserCommunitiesRequest, request: IFetchUserCommunitiesRequest,
): Promise<IFetchUserCommunitiesResponse> => { ): Promise<IFetchUserCommunitiesResponse | IResponseError> => {
return await callApi(HTTP.GET, `user/${request.id}/communities`); return await callApi(HTTP.GET, `user/${request.id}/communities`);
}; };

View file

@ -8,7 +8,7 @@ const Community: Component<ICommunityProps> = (props: ICommunityProps) => {
onClick={() => props.onCommunityClick?.(props.id)} onClick={() => props.onCommunityClick?.(props.id)}
> >
<div <div
class={`w-full transition-[border-radius] duration-300 outline-stone-300 ${props.active ? "rounded-lg outline-3 hover:outline-3" : "rounded-4xl hover:outline-2"}`} 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"}`}
> >
<img src={props.avatar} /> <img src={props.avatar} />
</div> </div>

View file

@ -1,10 +1,36 @@
import type { Component } from "solid-js"; import { type Component, type JSXElement } from "solid-js";
import { ICommunityBarProps } from "./types"; import { ICommunityBarProps } from "./types";
import { SettingsIcon } from "../icons"; import { SettingsIcon } from "../../icons";
import { state } from "../../store/state";
const CommunityBar: Component<ICommunityBarProps> = ( const CommunityBar: Component<ICommunityBarProps> = (
props: ICommunityBarProps, props: ICommunityBarProps,
) => { ) => {
const settingsIconHtml = (): JSXElement | undefined => {
const activeCommunityId = state.community.active;
if (!activeCommunityId) {
return undefined;
}
const community = state.community.communities[activeCommunityId];
if (!community) {
return undefined;
}
if (false) {
return undefined;
}
return (
<div
class="bg-stone-950/25 cursor-pointer rounded-full w-10 h-10 p-2 hover:bg-stone-950/75 transition-transform duration-300 hover:rotate-30"
onClick={props.onSettingsClick}
>
<SettingsIcon />
</div>
);
};
return ( return (
<div <div
class={`absolute w-full top-0 z-10 bg-cover bg-top bg-no-repeat bg-[url('${props.avatar}')]`} class={`absolute w-full top-0 z-10 bg-cover bg-top bg-no-repeat bg-[url('${props.avatar}')]`}
@ -14,12 +40,7 @@ const CommunityBar: Component<ICommunityBarProps> = (
<h2 class="text-sm font-bold">{props.name}</h2> <h2 class="text-sm font-bold">{props.name}</h2>
<p class="text-xs">{props.description}</p> <p class="text-xs">{props.description}</p>
</div> </div>
<div {settingsIconHtml()}
class="bg-stone-950/25 cursor-pointer rounded-full w-10 h-10 p-2 hover:bg-stone-950/75"
onClick={props.onSettingsClick}
>
<SettingsIcon />
</div>
</div> </div>
</div> </div>
); );

View file

@ -1,119 +0,0 @@
import { createSignal, type Component } from "solid-js";
import { ICommunityModalProps } from "./types";
import { dispatch } from "../../store/state";
import { CommunityActionTypes } from "../../store/community";
import { InviteActionTypes } from "../../store/invite";
const CommunityModal: Component<ICommunityModalProps> = (props) => {
const [getCommunityName, setCommunityName] = createSignal("");
const [getInviteId, setInviteId] = createSignal("");
const onCreateCommunity = () => {
const communityName = getCommunityName();
if (!communityName || communityName.trim().length < 1) {
return;
}
dispatch({
type: CommunityActionTypes.CREATE_COMMUNITY_START,
payload: {
name: communityName,
},
});
setCommunityName("");
props.onClose?.();
};
const onJoinCommunity = () => {
const inviteId = getInviteId();
if (!inviteId || inviteId.trim().length < 1) {
return;
}
dispatch({
type: InviteActionTypes.ACCEPT_INVITE_START,
payload: inviteId,
});
setInviteId("");
props.onClose?.();
};
const handleEnter = (e: KeyboardEvent, callback: () => void) => {
if (e.key === "Enter") {
callback();
}
};
const createCommunityHtml = () => (
<>
<h3 class="text-lg font-bold text-center mb-6">
Create a new Community
</h3>
<div class="bg-stone-800 h-16 p-2 flex flex-row gap-2 rounded-2xl">
<label class="bg-stone-800 input w-full h-full rounded-xl focus:border-none outline-none">
<input
type="text"
placeholder="Enter name of the new community"
value={getCommunityName()}
onInput={(e) => setCommunityName(e.currentTarget.value)}
onKeyDown={(e) => handleEnter(e, onCreateCommunity)}
/>
</label>
<button
class="bg-stone-950 btn btn-neutral h-full rounded-xl"
onClick={onCreateCommunity}
>
Create
</button>
</div>
</>
);
const joinCommunityHtml = () => (
<>
<h3 class="text-lg font-bold text-center mb-6">
Join an existing Community
</h3>
<div class="bg-stone-800 h-16 p-2 flex flex-row gap-2 rounded-2xl">
<label class="bg-stone-800 input w-full h-full rounded-xl focus:border-none outline-none">
<input
type="text"
placeholder="Enter invite ID"
value={getInviteId()}
onInput={(e) => setInviteId(e.currentTarget.value)}
onKeyDown={(e) => handleEnter(e, onJoinCommunity)}
/>
</label>
<button
class="bg-stone-950 btn btn-neutral h-full rounded-xl"
onClick={onJoinCommunity}
>
Join
</button>
</div>
</>
);
return (
<div>
<dialog ref={props.dialogRef} class="modal bg-[#00000050]">
<div class="modal-box bg-stone-950 rounded-3xl">
{createCommunityHtml()}
<div class="divider my-8"></div>
{joinCommunityHtml()}
</div>
<form
onClick={props.onClose}
method="dialog"
class="modal-backdrop"
></form>
</dialog>
</div>
);
};
export default CommunityModal;

View file

@ -1,2 +0,0 @@
export * from "./CommunityModal";
export * from "./types";

View file

@ -1,6 +0,0 @@
interface ICommunityModalProps {
dialogRef?: (element: HTMLDialogElement) => void;
onClose?: () => void;
}
export { type ICommunityModalProps };

View file

@ -1,2 +0,0 @@
export * from "./CommunitySettingsModal";
export * from "./types";

View file

@ -1,6 +0,0 @@
interface ICommunitySettingsModalProps {
dialogRef?: (element: HTMLDialogElement) => void;
onClose?: () => void;
}
export { type ICommunitySettingsModalProps };

View file

@ -5,7 +5,7 @@ import { Dynamic } from "solid-js/web";
const HomeCard: Component<IHomeCardProps> = (props: IHomeCardProps) => { const HomeCard: Component<IHomeCardProps> = (props: IHomeCardProps) => {
return ( return (
<a class="w-60 cursor-pointer" onClick={props.onClick}> <a class="w-60 cursor-pointer" onClick={props.onClick}>
<div class="card border-2 bg-stone-800 border-stone-500 hover:border-stone-100 w-60 h-60"> <div class="card outline-2 bg-stone-800 outline-stone-500 hover:outline-stone-100 w-60 h-60">
<div class="flex flex-col h-full gap-1 m-6"> <div class="flex flex-col h-full gap-1 m-6">
<div class="w-20"> <div class="w-20">
<Dynamic component={props.icon} /> <Dynamic component={props.icon} />

View file

@ -1,5 +1,5 @@
import { JSXElement } from "solid-js"; import { JSXElement } from "solid-js";
import { IconParameters } from "../icons"; import { IconParameters } from "../../icons";
interface IHomeCardProps { interface IHomeCardProps {
title: string; title: string;

View file

@ -0,0 +1,40 @@
import { type Component, type JSXElement } from "solid-js";
import { IInputProps } from "./types";
const Input: Component<IInputProps> = (props: IInputProps) => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === "Enter") {
props.onSubmit?.();
}
};
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>
);
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" : ""}`}
>
<label
class={`bg-stone-800 input px-5 w-full h-full focus:border-none outline-none ${props.rounded ? "rounded-full" : "rounded-xl"}`}
>
<input
type={props.type ? props.type : "text"}
placeholder={props.placeholder}
value={props.value}
onInput={(e) => props.onChange?.(e.currentTarget.value)}
onKeyDown={handleEnter}
/>
</label>
{props.submitText ? submitHtml() : undefined}
</div>
);
};
export { Input };

View file

@ -0,0 +1,2 @@
export * from "./Input";
export * from "./types";

View file

@ -0,0 +1,12 @@
interface IInputProps {
value: string;
type?: "text" | "password" | "email";
outline?: boolean;
rounded?: boolean;
placeholder?: string;
submitText?: string;
onChange?: (value: string) => void;
onSubmit?: () => void;
}
export { type IInputProps };

View file

@ -13,8 +13,14 @@ const Message: Component<IMessageProps> = (props: IMessageProps) => {
</div> </div>
</div> </div>
<div> <div>
<div>{props.username}</div> <div class="font-bold">{props.username}</div>
{props.decryptionStatus ? (
<p class="list-col-wrap text-xs">{props.message}</p> <p class="list-col-wrap text-xs">{props.message}</p>
) : (
<p class="list-col-wrap text-xs italic">
Decryption failed
</p>
)}
</div> </div>
</li> </li>
); );

View file

@ -4,6 +4,7 @@ interface IMessageProps {
userId: string; userId: string;
username: string; username: string;
avatar: string; avatar: string;
decryptionStatus: boolean;
onProfileClick?: (userId: string) => void; onProfileClick?: (userId: string) => void;
} }

View file

@ -1,5 +1,6 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { IMessageBarProps } from "./types"; import { IMessageBarProps } from "./types";
import { UpIcon } from "../../icons";
const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => { const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
const handleEnter = (e: KeyboardEvent) => { const handleEnter = (e: KeyboardEvent) => {
@ -23,10 +24,12 @@ const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
/> />
</label> </label>
<button <button
class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral h-full rounded-full" class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral w-12 p-0 h-full rounded-full"
onClick={props.onSend} onClick={props.onSend}
> >
Send <div class="w-5">
<UpIcon />
</div>
</button> </button>
</div> </div>
</div> </div>

View file

@ -0,0 +1,64 @@
import type { Component, JSXElement } from "solid-js";
import { IRichSettingsItemProps } from "./types";
import { Dynamic } from "solid-js/web";
const RichSettingsItem: Component<IRichSettingsItemProps> = (
props: IRichSettingsItemProps,
) => {
const pictureHtml = (): JSXElement => {
if (props.avatar) {
return (
<div class="avatar">
<div class="w-12 rounded-lg">
<img src={props.avatar} />
</div>
</div>
);
}
if (props.icon) {
return (
<div
class={`bg-stone-700 w-12 rounded-lg p-${props.iconPadding ? props.iconPadding : 1}`}
>
<Dynamic component={props.icon} />
</div>
);
}
return undefined;
};
const infoHtml = (): JSXElement | undefined => {
if (!props.info) {
return undefined;
}
return (
<>
<div class="flex-1"></div>
<div class="badge badge-primary badge-outline p-4 mr-11">
{props.info}
</div>
</>
);
};
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"}`}
>
<input type="checkbox" />
<div class="collapse-title font-semibold flex flex-row items-center gap-4 p-1">
{pictureHtml()}
{props.title}
{infoHtml()}
</div>
<div class="collapse-content text-sm font-semibold">
<div class="mt-2">{props.children}</div>
</div>
</div>
);
};
export { RichSettingsItem };

View file

@ -0,0 +1,2 @@
export * from "./RichSettingsItem";
export * from "./types";

View file

@ -0,0 +1,17 @@
import { JSXElement } from "solid-js";
import { IconParameters } from "../../icons";
interface IRichSettingsItemProps {
id: string;
title: string;
text?: string;
info?: string;
avatar?: string;
icon?: (props: IconParameters) => JSXElement;
iconPadding?: number;
active: boolean;
children?: JSXElement;
onClick?: (id: string) => void;
}
export { type IRichSettingsItemProps };

View file

@ -0,0 +1,17 @@
import type { Component } from "solid-js";
import { ISettingsItemProps } from "./types";
const SettingsItem: Component<ISettingsItemProps> = (
props: 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"}`}
onClick={() => props.onClick?.(props.id)}
>
{props.text}
</div>
);
};
export { SettingsItem };

View file

@ -0,0 +1,2 @@
export * from "./SettingsItem";
export * from "./types";

View file

@ -0,0 +1,8 @@
interface ISettingsItemProps {
id: string;
text: string;
active: boolean;
onClick?: (id: string) => void;
}
export { type ISettingsItemProps };

View file

@ -1,22 +0,0 @@
import type { Component } from "solid-js";
import { ISettingsModalProps } from "./types";
const SettingsModal: Component<ISettingsModalProps> = (props) => {
return (
<div>
<dialog ref={props.dialogRef} class="modal bg-[#00000050]">
<div class="modal-box bg-stone-950 rounded-3xl">
<h3 class="text-lg font-bold text-center">Settings</h3>
<p class="py-4 text-center">Not implemented yet</p>
</div>
<form
onClick={props.onClose}
method="dialog"
class="modal-backdrop"
></form>
</dialog>
</div>
);
};
export default SettingsModal;

View file

@ -1,2 +0,0 @@
export * from "./SettingsModal";
export * from "./types";

View file

@ -5,9 +5,20 @@ import { Dynamic } from "solid-js/web";
const SidebarItem: Component<ISidebarItemProps> = ( const SidebarItem: Component<ISidebarItemProps> = (
props: ISidebarItemProps, props: ISidebarItemProps,
) => { ) => {
const rotateCss = (): string => {
switch (props.hoverRotate) {
case 30:
return "hover:rotate-30";
case 45:
return "hover:rotate-45";
}
return "";
};
return ( return (
<div <div
class={`bg-stone-800 w-full p-2 cursor-pointer transition-[border-radius] duration-300 outline-stone-300 ${props.active ? "rounded-lg outline-3 hover:outline-3" : "rounded-4xl hover:outline-2"}`} class={`bg-stone-800 w-full p-2 cursor-pointer transition-all duration-300 ${rotateCss()} hover:outline-stone-300 ${props.active ? "rounded-lg outline-3 outline-stone-300 hover:outline-3" : "outline-transparent rounded-4xl outline-2"}`}
onClick={() => props.onClick?.()} onClick={() => props.onClick?.()}
> >
<Dynamic component={props.icon} /> <Dynamic component={props.icon} />

View file

@ -1,9 +1,10 @@
import { JSXElement } from "solid-js"; import { JSXElement } from "solid-js";
import { IconParameters } from "../icons"; import { IconParameters } from "../../icons";
interface ISidebarItemProps { interface ISidebarItemProps {
icon: (props: IconParameters) => JSXElement; icon: (props: IconParameters) => JSXElement;
active: boolean; active: boolean;
hoverRotate?: 30 | 45;
onClick?: () => void; onClick?: () => void;
} }

View file

@ -1,7 +0,0 @@
import HomeIcon from "./HomeIcon";
import SettingsIcon from "./SettingsIcon";
import PlusIcon from "./PlusIcon";
import type { IconParameters } from "./types";
export { IconParameters, HomeIcon, SettingsIcon, PlusIcon };

34
src/icons/DeviceIcon.tsx Normal file
View file

@ -0,0 +1,34 @@
import type { Component } from "solid-js";
import {
IconParameters,
defaultStrokeIconParameters as defaults,
} from "./types";
const DeviceIcon: 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 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
/>
</svg>
);
};
export default DeviceIcon;

34
src/icons/MinusIcon.tsx Normal file
View file

@ -0,0 +1,34 @@
import type { Component } from "solid-js";
import {
IconParameters,
defaultStrokeIconParameters as defaults,
} from "./types";
const MinusIcon: 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="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
);
};
export default MinusIcon;

31
src/icons/TrashIcon.tsx Normal file
View file

@ -0,0 +1,31 @@
import type { Component } from "solid-js";
import { IconParameters, defaultFillIconParameters as defaults } from "./types";
const TrashIcon: 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="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
clip-rule="evenodd"
/>
</svg>
);
};
export default TrashIcon;

34
src/icons/UpIcon.tsx Normal file
View file

@ -0,0 +1,34 @@
import type { Component } from "solid-js";
import {
IconParameters,
defaultStrokeIconParameters as defaults,
} from "./types";
const UpIcon: 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 10.5 12 3m0 0 7.5 7.5M12 3v18"
/>
</svg>
);
};
export default UpIcon;

20
src/icons/index.ts Normal file
View file

@ -0,0 +1,20 @@
import HomeIcon from "./HomeIcon";
import SettingsIcon from "./SettingsIcon";
import PlusIcon from "./PlusIcon";
import MinusIcon from "./MinusIcon";
import DeviceIcon from "./DeviceIcon";
import TrashIcon from "./TrashIcon";
import UpIcon from "./UpIcon";
import type { IconParameters } from "./types";
export {
IconParameters,
HomeIcon,
SettingsIcon,
PlusIcon,
MinusIcon,
DeviceIcon,
TrashIcon,
UpIcon,
};

View file

@ -1,6 +1,36 @@
import { fetchLoginApi, fetchRefreshApi } from "../../api/auth"; import {
import { AuthActionTypes } from "../../store/auth"; fetchRegisterApi,
import { dispatch } from "../../store/state"; fetchLoginApi,
fetchRefreshApi,
} from "../../api/auth";
import {
setLoggedIn,
setRegisterSuccess,
setAuthSession,
setAuthSessionKey,
setAuthSessionIV,
} from "../../store/auth";
import { state } from "../../store/state";
import { hexToBytes } from "../crypto";
const fetchRegister = async (
username: string,
password: string,
email: string,
) => {
const data = await fetchRegisterApi({
username: username,
password: password,
email: email,
});
if (typeof data.error === "string") {
setRegisterSuccess(false);
return;
}
setRegisterSuccess(true);
};
const fetchLogin = async (username: string, password: string) => { const fetchLogin = async (username: string, password: string) => {
const data = await fetchLoginApi({ const data = await fetchLoginApi({
@ -8,28 +38,42 @@ const fetchLogin = async (username: string, password: string) => {
password: password, password: password,
}); });
dispatch({ if (typeof data.error === "string") {
type: AuthActionTypes.FETCH_LOGIN_FINISH, return;
payload: data, }
});
dispatch({ setAuthSession(data);
type: AuthActionTypes.FETCH_REFRESH_START,
}); fetchRefresh();
}; };
const fetchRefresh = async () => { const fetchRefresh = async () => {
const data = await fetchRefreshApi(); const data = await fetchRefreshApi();
dispatch({ if (typeof data.error === "string") {
type: AuthActionTypes.FETCH_REFRESH_FINISH, setLoggedIn(false);
payload: data,
});
dispatch({ return;
type: AuthActionTypes.SET_LOGGED_IN, }
payload: "id" in data,
}); setAuthSession(data);
setLoggedIn(true);
if (
!state.auth.session?.storageSecret ||
state.auth.session.storageSecret.length !== 89
) {
return;
}
const [keyHex, ivHex] = state.auth.session.storageSecret.split(";");
const key = hexToBytes(keyHex);
const iv = hexToBytes(ivHex);
if (key && iv) {
setAuthSessionKey(new Uint8Array(key));
setAuthSessionIV(new Uint8Array(iv));
}
}; };
export { fetchLogin, fetchRefresh }; export { fetchRegister, fetchLogin, fetchRefresh };

View file

@ -5,18 +5,24 @@ import {
removeChannelApi, removeChannelApi,
fetchChannelMessagesApi, fetchChannelMessagesApi,
} from "../../api/channel"; } from "../../api/channel";
import { ChannelActionTypes } from "../../store/channel"; import {
import { dispatch } from "../../store/state"; deleteChannel,
setChannel,
setChannelMessages,
} from "../../store/channel";
import { IMessage } from "../../store/message";
import { decryptMessage } from "../message";
const fetchChannel = async (id: string) => { const fetchChannel = async (id: string) => {
const data = await fetchChannelApi({ const data = await fetchChannelApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: ChannelActionTypes.FETCH_CHANNEL_FINISH, return;
payload: data, }
});
setChannel(data);
}; };
const createChannel = async (name: string, communityId: string) => { const createChannel = async (name: string, communityId: string) => {
@ -25,10 +31,11 @@ const createChannel = async (name: string, communityId: string) => {
communityId: communityId, communityId: communityId,
}); });
dispatch({ if (typeof data.error === "string") {
type: ChannelActionTypes.CREATE_CHANNEL_FINISH, return;
payload: data, }
});
setChannel(data);
}; };
const updateChannel = async ( const updateChannel = async (
@ -42,10 +49,11 @@ const updateChannel = async (
description: description, description: description,
}); });
dispatch({ if (typeof data.error === "string") {
type: ChannelActionTypes.UPDATE_CHANNEL_FINISH, return;
payload: data, }
});
setChannel(data);
}; };
const removeChannel = async (id: string) => { const removeChannel = async (id: string) => {
@ -53,21 +61,52 @@ const removeChannel = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: ChannelActionTypes.REMOVE_CHANNEL_FINISH, return;
payload: data, }
});
deleteChannel(data.id);
}; };
const fetchChannelMessages = async (id: string) => { const fetchChannelMessages = async (id: string, communityId: string) => {
const data = await fetchChannelMessagesApi({ const data = await fetchChannelMessagesApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_FINISH, return;
payload: data, }
const messages: IMessage[] = [];
for (const message of data.messages) {
try {
const decrypted = await decryptMessage(
communityId,
message.text,
message.iv,
);
if (decrypted) {
messages.push({
...message,
text: decrypted,
decryptionStatus: true,
}); });
} else {
messages.push({
...message,
decryptionStatus: false,
});
}
} catch {
messages.push({
...message,
decryptionStatus: false,
});
}
}
setChannelMessages(data.id, messages);
}; };
export { export {

View file

@ -8,22 +8,33 @@ import {
fetchCommunityMembersApi, fetchCommunityMembersApi,
fetchCommunityInvitesApi, fetchCommunityInvitesApi,
} from "../../api/community"; } from "../../api/community";
import { CommunityActionTypes } from "../../store/community"; import { setChannel } from "../../store/channel";
import { ChannelActionTypes } from "../../store/channel"; import {
import { RoleActionTypes } from "../../store/role"; deleteCommunity,
import { UserActionTypes } from "../../store/user"; setCommunity,
import { dispatch, state } from "../../store/state"; setCommunityChannels,
import { InviteActionTypes } from "../../store/invite"; setCommunityEncryptionKey,
setCommunityInvites,
setCommunityMembers,
setCommunityRoles,
} from "../../store/community";
import { setInvite } from "../../store/invite";
import { setRole } from "../../store/role";
import { state } from "../../store/state";
import { setUser } from "../../store/user";
import { DB_STORE, dbLoadEncrypted } from "../database";
import { fetchUserCommunities } from "../user";
const fetchCommunity = async (id: string) => { const fetchCommunity = async (id: string) => {
const data = await fetchCommunityApi({ const data = await fetchCommunityApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.FETCH_COMMUNITY_FINISH, return;
payload: data, }
});
setCommunity(data);
}; };
const createCommunity = async (name: string) => { const createCommunity = async (name: string) => {
@ -31,16 +42,14 @@ const createCommunity = async (name: string) => {
name: name, name: name,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.CREATE_COMMUNITY_FINISH, return;
payload: data, }
});
setCommunity(data);
if (state.user.loggedUserId) { if (state.user.loggedUserId) {
dispatch({ fetchUserCommunities(state.user.loggedUserId);
type: UserActionTypes.FETCH_USER_COMMUNITIES_START,
payload: state.user.loggedUserId,
});
} }
}; };
@ -55,16 +64,14 @@ const updateCommunity = async (
description: description, description: description,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.UPDATE_COMMUNITY_FINISH, return;
payload: data, }
});
setCommunity(data);
if (state.user.loggedUserId) { if (state.user.loggedUserId) {
dispatch({ fetchUserCommunities(state.user.loggedUserId);
type: UserActionTypes.FETCH_USER_COMMUNITIES_START,
payload: state.user.loggedUserId,
});
} }
}; };
@ -73,16 +80,14 @@ const removeCommunity = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.REMOVE_COMMUNITY_FINISH, return;
payload: data, }
});
deleteCommunity(data.id);
if (state.user.loggedUserId) { if (state.user.loggedUserId) {
dispatch({ fetchUserCommunities(state.user.loggedUserId);
type: UserActionTypes.FETCH_USER_COMMUNITIES_START,
payload: state.user.loggedUserId,
});
} }
}; };
@ -91,16 +96,14 @@ const fetchCommunityChannels = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.FETCH_COMMUNITY_CHANNELS_FINISH, return;
payload: data, }
});
setCommunityChannels(data.id, data.channels);
data.channels.forEach((channel) => { data.channels.forEach((channel) => {
dispatch({ setChannel(channel);
type: ChannelActionTypes.SET_CHANNEL,
payload: channel,
});
}); });
}; };
@ -109,16 +112,14 @@ const fetchCommunityRoles = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.FETCH_COMMUNITY_ROLES_FINISH, return;
payload: data, }
});
setCommunityRoles(data.id, data.roles);
data.roles.forEach((role) => { data.roles.forEach((role) => {
dispatch({ setRole(role);
type: RoleActionTypes.SET_ROLE,
payload: role,
});
}); });
}; };
@ -127,16 +128,14 @@ const fetchCommunityMembers = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.FETCH_COMMUNITY_MEMBERS_FINISH, return;
payload: data, }
});
setCommunityMembers(data.id, data.members);
data.members.forEach((member) => { data.members.forEach((member) => {
dispatch({ setUser(member);
type: UserActionTypes.SET_USER,
payload: member,
});
}); });
}; };
@ -145,15 +144,34 @@ const fetchCommunityInvites = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: CommunityActionTypes.FETCH_COMMUNITY_INVITES_FINISH, return;
payload: data, }
});
setCommunityInvites(data.id, data.invites);
data.invites.forEach((invite) => { data.invites.forEach((invite) => {
dispatch({ setInvite(invite);
type: InviteActionTypes.SET_INVITE, });
payload: invite, };
const loadCommunityCryptoStates = async () => {
if (!state.user.loggedUserId) {
return;
}
const communities = state.user.users[state.user.loggedUserId]?.communities;
if (!communities) {
return;
}
communities.forEach((communityId) => {
dbLoadEncrypted<string>(
DB_STORE.COMMUNITY_ENCRYPTION_KEYS,
communityId,
).then((communityEncryptionKey) => {
if (communityEncryptionKey) {
setCommunityEncryptionKey(communityId, communityEncryptionKey);
}
}); });
}); });
}; };
@ -167,4 +185,5 @@ export {
fetchCommunityRoles, fetchCommunityRoles,
fetchCommunityMembers, fetchCommunityMembers,
fetchCommunityInvites, fetchCommunityInvites,
loadCommunityCryptoStates,
}; };

View file

@ -0,0 +1,95 @@
import { ICryptoEncrypted, ICryptoData } from "./types";
const importKey = async (key: Uint8Array<ArrayBuffer>): Promise<CryptoKey> => {
return await crypto.subtle.importKey(
"raw",
key,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"],
);
};
const deriveKey = async (password: string): Promise<CryptoKey> => {
const salt = "NEXLINK_FIXED_SALT";
const encoder = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
"PBKDF2",
false,
["deriveKey"],
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: encoder.encode(salt),
iterations: 100000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
};
const encryptData = async <T>(
cryptoData: ICryptoData<T>,
): Promise<ArrayBuffer> => {
const encoded = new TextEncoder().encode(JSON.stringify(cryptoData.data));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: cryptoData.iv },
cryptoData.key,
encoded,
);
return encrypted;
};
const decryptData = async <T>(cryptoData: ICryptoEncrypted): Promise<T> => {
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: cryptoData.iv },
cryptoData.key,
cryptoData.encryptedData,
);
const decoded = JSON.parse(new TextDecoder().decode(decrypted));
return decoded as T;
};
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;
};
export {
importKey,
deriveKey,
encryptData,
decryptData,
generateIv,
hexToBytes,
bytesToHex,
};

View file

@ -0,0 +1 @@
export * from "./crypto";

View file

@ -0,0 +1,13 @@
interface ICryptoEncrypted {
key: CryptoKey;
iv: Uint8Array<ArrayBuffer>;
encryptedData: ArrayBuffer;
}
interface ICryptoData<T> {
key: CryptoKey;
iv: Uint8Array<ArrayBuffer>;
data: T;
}
export { type ICryptoEncrypted, type ICryptoData };

View file

@ -0,0 +1,114 @@
import { state } from "../../store/state";
import { decryptData, encryptData, importKey } from "../crypto";
import { DB_STORE, IDatabaseItem } from "./types";
const DB_VERSION = 1;
const openDB = async (name: string, store: string): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (
!db.objectStoreNames.contains(
DB_STORE.COMMUNITY_ENCRYPTION_KEYS,
)
) {
db.createObjectStore(DB_STORE.COMMUNITY_ENCRYPTION_KEYS, {
keyPath: "id",
});
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
const getDB = async (store: string): Promise<IDBDatabase> => {
return await openDB("pulsardb", store);
};
const dbSave = async (store: string, item: IDatabaseItem): Promise<void> => {
const db = await getDB(store);
return new Promise((resolve, reject) => {
const tx = db.transaction(store, "readwrite");
tx.objectStore(store).put(item);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
};
const dbLoad = async (
store: string,
id: IDBValidKey,
): Promise<IDatabaseItem | undefined> => {
const db = await getDB(store);
return new Promise((resolve, reject) => {
const tx = db.transaction(store, "readonly");
const req = tx.objectStore(store).get(id);
req.onsuccess = () => resolve(req.result as IDatabaseItem | undefined);
req.onerror = () => reject(req.error);
});
};
const dbSaveEncrypted = async <T>(store: string, id: IDBValidKey, item: T) => {
const key = state.auth.session?.key;
const iv = state.auth.session?.iv;
if (!key || !iv) {
return;
}
const importedKey = await importKey(key);
const encrypted = await encryptData<T>({
key: importedKey,
iv: iv,
data: item,
});
await dbSave(store, {
id: id,
data: encrypted,
});
};
const dbLoadEncrypted = async <T>(
store: string,
id: IDBValidKey,
): Promise<T | undefined> => {
const key = state.auth.session?.key;
const iv = state.auth.session?.iv;
if (!key || !iv) {
return;
}
const item = await dbLoad(store, id);
if (!item) {
return;
}
const importedKey = await importKey(key);
const decrypted = await decryptData<T>({
key: importedKey,
iv: iv,
encryptedData: item.data,
});
return decrypted;
};
export {
openDB,
getDB,
dbSave,
dbLoad,
importKey,
dbSaveEncrypted,
dbLoadEncrypted,
};

View file

@ -0,0 +1,2 @@
export * from "./database";
export * from "./types";

View file

@ -0,0 +1,10 @@
enum DB_STORE {
COMMUNITY_ENCRYPTION_KEYS = "COMMUNITY_ENCRYPTION_KEYS",
}
interface IDatabaseItem {
id: IDBValidKey;
data: ArrayBuffer;
}
export { DB_STORE, type IDatabaseItem };

View file

@ -3,19 +3,20 @@ import {
removeInviteApi, removeInviteApi,
acceptInviteApi, acceptInviteApi,
} from "../../api/invite"; } from "../../api/invite";
import { InviteActionTypes } from "../../store/invite"; import { deleteInvite, setInvite } from "../../store/invite";
import { dispatch, state } from "../../store/state"; import { state } from "../../store/state";
import { UserActionTypes } from "../../store/user"; import { fetchUserCommunities } from "../user";
const fetchInvite = async (id: string) => { const fetchInvite = async (id: string) => {
const data = await fetchInviteApi({ const data = await fetchInviteApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: InviteActionTypes.FETCH_INVITE_FINISH, return;
payload: data, }
});
setInvite(data);
}; };
const removeInvite = async (id: string) => { const removeInvite = async (id: string) => {
@ -23,10 +24,11 @@ const removeInvite = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: InviteActionTypes.REMOVE_INVITE_FINISH, return;
payload: data, }
});
deleteInvite(data.id);
}; };
const acceptInvite = async (id: string) => { const acceptInvite = async (id: string) => {
@ -34,16 +36,12 @@ const acceptInvite = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: InviteActionTypes.ACCEPT_INVITE_FINISH, return;
payload: data, }
});
if (state.user.loggedUserId) { if (state.user.loggedUserId) {
dispatch({ fetchUserCommunities(state.user.loggedUserId);
type: UserActionTypes.FETCH_USER_COMMUNITIES_START,
payload: state.user.loggedUserId,
});
} }
}; };

View file

@ -4,29 +4,62 @@ import {
updateMessageApi, updateMessageApi,
removeMessageApi, removeMessageApi,
} from "../../api/message"; } from "../../api/message";
import { MessageActionTypes } from "../../store/message"; import { deleteMessage, setMessage } from "../../store/message";
import { dispatch } from "../../store/state"; import { state } from "../../store/state";
import {
bytesToHex,
decryptData,
encryptData,
generateIv,
hexToBytes,
} from "../crypto";
const fetchMessage = async (id: string) => { const fetchMessage = async (id: string, communityId: string) => {
const data = await fetchMessageApi({ const data = await fetchMessageApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: MessageActionTypes.FETCH_MESSAGE_FINISH, return;
payload: data, }
try {
const decrypted = await decryptMessage(communityId, data.text, data.iv);
if (!decrypted) {
return;
}
setMessage({
...data,
text: decrypted,
}); });
} catch {}
}; };
const createMessage = async (text: string, channelId: string) => { const createMessage = async (
communityId: string,
channelId: string,
text: string,
) => {
const encrypted = await encryptMessage(communityId, text);
if (!encrypted) {
return;
}
const [encryptedMessage, iv] = encrypted;
const data = await createMessageApi({ const data = await createMessageApi({
text: text,
channelId: channelId, channelId: channelId,
iv: iv,
text: encryptedMessage,
}); });
dispatch({ if (typeof data.error === "string") {
type: MessageActionTypes.CREATE_MESSAGE_FINISH, return;
payload: data, }
setMessage({
...data,
text: text,
}); });
}; };
@ -36,10 +69,11 @@ const updateMessage = async (id: string, text: string) => {
text: text, text: text,
}); });
dispatch({ if (typeof data.error === "string") {
type: MessageActionTypes.UPDATE_MESSAGE_FINISH, return;
payload: data, }
});
setMessage(data);
}; };
const removeMessage = async (id: string) => { const removeMessage = async (id: string) => {
@ -47,10 +81,60 @@ const removeMessage = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: MessageActionTypes.REMOVE_MESSAGE_FINISH, return;
payload: data, }
deleteMessage();
};
const encryptMessage = async (
communityId: string,
text: string,
): Promise<[string, string] | undefined> => {
const key = state.community.communities[communityId]?.derivedKey;
if (!key) {
return;
}
const iv = generateIv();
const encrypted = await encryptData<string>({
key: key,
iv: iv,
data: text,
});
return [bytesToHex(encrypted), bytesToHex(iv.buffer)];
};
const decryptMessage = async (
communityId: string,
text: string,
iv: string,
): Promise<string | undefined> => {
const key = state.community.communities[communityId]?.derivedKey;
if (!key) {
return;
}
const ivBytes = hexToBytes(iv);
const textBytes = hexToBytes(text);
if (!ivBytes || !textBytes) {
return;
}
return await decryptData({
key: key,
iv: new Uint8Array(ivBytes),
encryptedData: textBytes,
}); });
}; };
export { fetchMessage, createMessage, updateMessage, removeMessage }; export {
fetchMessage,
createMessage,
updateMessage,
removeMessage,
encryptMessage,
decryptMessage,
};

View file

@ -4,18 +4,18 @@ import {
updateRoleApi, updateRoleApi,
removeRoleApi, removeRoleApi,
} from "../../api/role"; } from "../../api/role";
import { RoleActionTypes } from "../../store/role"; import { deleteRole, setRole } from "../../store/role";
import { dispatch } from "../../store/state";
const fetchRole = async (id: string) => { const fetchRole = async (id: string) => {
const data = await fetchRoleApi({ const data = await fetchRoleApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: RoleActionTypes.FETCH_ROLE_FINISH, return;
payload: data, }
});
setRole(data);
}; };
const createRole = async (name: string, communityId: string) => { const createRole = async (name: string, communityId: string) => {
@ -24,10 +24,11 @@ const createRole = async (name: string, communityId: string) => {
communityId: communityId, communityId: communityId,
}); });
dispatch({ if (typeof data.error === "string") {
type: RoleActionTypes.CREATE_ROLE_FINISH, return;
payload: data, }
});
setRole(data);
}; };
const updateRole = async (id: string, name?: string) => { const updateRole = async (id: string, name?: string) => {
@ -36,10 +37,11 @@ const updateRole = async (id: string, name?: string) => {
name: name, name: name,
}); });
dispatch({ if (typeof data.error === "string") {
type: RoleActionTypes.UPDATE_ROLE_FINISH, return;
payload: data, }
});
setRole(data);
}; };
const removeRole = async (id: string) => { const removeRole = async (id: string) => {
@ -47,10 +49,11 @@ const removeRole = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: RoleActionTypes.REMOVE_ROLE_FINISH, return;
payload: data, }
});
deleteRole(data.id);
}; };
export { fetchRole, createRole, updateRole, removeRole }; export { fetchRole, createRole, updateRole, removeRole };

View file

@ -1,16 +1,16 @@
import { fetchSessionApi, removeSessionApi } from "../../api/session"; import { fetchSessionApi, removeSessionApi } from "../../api/session";
import { SessionActionTypes } from "../../store/session"; import { deleteSession, setSession } from "../../store/session";
import { dispatch } from "../../store/state";
const fetchSession = async (id: string) => { const fetchSession = async (id: string) => {
const data = await fetchSessionApi({ const data = await fetchSessionApi({
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: SessionActionTypes.FETCH_SESSION_FINISH, return;
payload: data, }
});
setSession(data);
}; };
const removeSession = async (id: string) => { const removeSession = async (id: string) => {
@ -18,10 +18,11 @@ const removeSession = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: SessionActionTypes.REMOVE_SESSION_FINISH, return;
payload: data, }
});
deleteSession(data.id);
}; };
export { fetchSession, removeSession }; export { fetchSession, removeSession };

View file

@ -4,18 +4,23 @@ import {
fetchUserSessionsApi, fetchUserSessionsApi,
fetchUserCommunitiesApi, fetchUserCommunitiesApi,
} from "../../api/user"; } from "../../api/user";
import { UserActionTypes } from "../../store/user"; import { setCommunity } from "../../store/community";
import { CommunityActionTypes } from "../../store/community"; import { setSession } from "../../store/session";
import { dispatch } from "../../store/state"; import {
import { SessionActionTypes } from "../../store/session"; setLoggedUserId,
setUser,
setUserCommunities,
setUserSessions,
} from "../../store/user";
const fetchLoggedUser = async () => { const fetchLoggedUser = async () => {
const data = await fetchLoggedUserApi(); const data = await fetchLoggedUserApi();
dispatch({ if (typeof data.error === "string") {
type: UserActionTypes.FETCH_LOGGED_USER_ID_FINISH, return;
payload: data, }
});
setLoggedUserId(data.id);
}; };
const fetchUser = async (id: string) => { const fetchUser = async (id: string) => {
@ -23,10 +28,11 @@ const fetchUser = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: UserActionTypes.FETCH_USER_FINISH, return;
payload: data, }
});
setUser(data);
}; };
const fetchUserSessions = async (id: string) => { const fetchUserSessions = async (id: string) => {
@ -34,16 +40,14 @@ const fetchUserSessions = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: UserActionTypes.FETCH_USER_SESSIONS_FINISH, return;
payload: data, }
});
setUserSessions(data);
data.sessions.forEach((session) => { data.sessions.forEach((session) => {
dispatch({ setSession(session);
type: SessionActionTypes.SET_SESSION,
payload: session,
});
}); });
}; };
@ -52,16 +56,14 @@ const fetchUserCommunities = async (id: string) => {
id: id, id: id,
}); });
dispatch({ if (typeof data.error === "string") {
type: UserActionTypes.FETCH_USER_COMMUNITIES_FINISH, return;
payload: data, }
});
setUserCommunities(data);
data.communities.forEach((community) => { data.communities.forEach((community) => {
dispatch({ setCommunity(community);
type: CommunityActionTypes.SET_COMMUNITY,
payload: community,
});
}); });
}; };

View file

@ -5,9 +5,12 @@ enum SocketRequestTypes {
} }
enum SocketMessageTypes { enum SocketMessageTypes {
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT", ANNOUNCEMENT = "ANNOUNCEMENT",
NEW_MESSAGE = "NEW_MESSAGE", SET_MESSAGE = "SET_MESSAGE",
NEW_CHANNEL = "NEW_CHANNEL", DELETE_MESSAGE = "DELETE_MESSAGE",
UPDATE_CHANNELS = "UPDATE_CHANNELS",
UPDATE_ROLES = "UPDATE_ROLES",
UPDATE_MEMBERS = "UPDATE_MEMBERS",
} }
type SocketRequest = { type SocketRequest = {
@ -16,25 +19,42 @@ type SocketRequest = {
type SocketMessage = type SocketMessage =
| { | {
type: SocketMessageTypes.NEW_ANNOUNCEMENT; type: SocketMessageTypes.ANNOUNCEMENT;
payload: { payload: {
title: string; title: string;
description: string; description: string;
}; };
} }
| { | {
type: SocketMessageTypes.NEW_MESSAGE; type: SocketMessageTypes.SET_MESSAGE;
payload: { payload: {
channelId: string; channelId: string;
message: IFetchChannelMessage; message: IFetchChannelMessage;
}; };
} }
| { | {
type: SocketMessageTypes.NEW_CHANNEL; type: SocketMessageTypes.DELETE_MESSAGE;
payload: {
channelId: string;
messageId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_CHANNELS;
payload: {
communityId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_ROLES;
payload: {
communityId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_MEMBERS;
payload: { payload: {
id: string;
communityId: string; communityId: string;
name: string;
}; };
}; };

View file

@ -1,5 +1,11 @@
import { ChannelActionTypes } from "../../store/channel"; import { deleteChannelMessage, setChannelMessage } from "../../store/channel";
import { dispatch } from "../../store/state"; import { state } from "../../store/state";
import {
fetchCommunityChannels,
fetchCommunityMembers,
fetchCommunityRoles,
} from "../community";
import { decryptMessage } from "../message";
import config from "./config.json"; import config from "./config.json";
import { SocketMessage, SocketMessageTypes } from "./types"; import { SocketMessage, SocketMessageTypes } from "./types";
@ -38,18 +44,62 @@ const parseMessage = (data: string): SocketMessage | null => {
const handleMessage = (message: SocketMessage) => { const handleMessage = (message: SocketMessage) => {
switch (message.type) { switch (message.type) {
case SocketMessageTypes.NEW_MESSAGE: { case SocketMessageTypes.SET_MESSAGE: {
dispatch({ try {
type: ChannelActionTypes.SET_CHANNEL_MESSAGE, const communityId =
payload: { state.channel.channels[message.payload.channelId]
id: message.payload.channelId, ?.communityId;
message: message.payload.message, if (!communityId) {
}, break;
}
decryptMessage(
communityId,
message.payload.message.text,
message.payload.message.iv,
).then((decrypted) => {
if (decrypted) {
setChannelMessage(message.payload.channelId, {
...message.payload.message,
text: decrypted,
decryptionStatus: true,
});
} else {
setChannelMessage(message.payload.channelId, {
...message.payload.message,
decryptionStatus: false,
}); });
} }
case SocketMessageTypes.NEW_CHANNEL: { });
} catch {
setChannelMessage(message.payload.channelId, {
...message.payload.message,
decryptionStatus: false,
});
} }
case SocketMessageTypes.NEW_ANNOUNCEMENT: {
break;
}
case SocketMessageTypes.DELETE_MESSAGE: {
deleteChannelMessage(
message.payload.channelId,
message.payload.messageId,
);
break;
}
case SocketMessageTypes.UPDATE_CHANNELS: {
fetchCommunityChannels(message.payload.communityId);
break;
}
case SocketMessageTypes.UPDATE_ROLES: {
fetchCommunityRoles(message.payload.communityId);
break;
}
case SocketMessageTypes.UPDATE_MEMBERS: {
fetchCommunityMembers(message.payload.communityId);
break;
}
case SocketMessageTypes.ANNOUNCEMENT: {
break;
} }
} }
}; };

View file

@ -1,33 +0,0 @@
import { AppActionTypes, AppAction } from "./app";
import { AuthActionTypes, AuthAction } from "./auth";
import { UserActionTypes, UserAction } from "./user";
import { CommunityActionTypes, CommunityAction } from "./community";
import { ChannelActionTypes, ChannelAction } from "./channel";
import { RoleActionTypes, RoleAction } from "./role";
import { SessionActionTypes, SessionAction } from "./session";
import { InviteActionTypes, InviteAction } from "./invite";
import { MessageActionTypes, MessageAction } from "./message";
type ActionTypes =
| AppActionTypes
| AuthActionTypes
| UserActionTypes
| CommunityActionTypes
| ChannelActionTypes
| RoleActionTypes
| SessionActionTypes
| InviteActionTypes
| MessageActionTypes;
type Action =
| AppAction
| AuthAction
| UserAction
| CommunityAction
| ChannelAction
| RoleAction
| SessionAction
| InviteAction
| MessageAction;
export { type Action, type ActionTypes };

View file

@ -1,17 +0,0 @@
enum AppActionTypes {
SET_HOME_OPEN = "SET_HOME_OPEN",
SET_SETTINGS_OPEN = "SET_SETTINGS_OPEN",
SET_ADD_COMMUNITY_OPEN = "SET_ADD_COMMUNITY_OPEN",
SET_ADD_COMMUNITY_SETTINGS_OPEN = "SET_ADD_COMMUNITY_SETTINGS_OPEN",
}
type AppAction =
| { type: AppActionTypes.SET_HOME_OPEN; payload: boolean }
| { type: AppActionTypes.SET_SETTINGS_OPEN; payload: boolean }
| { type: AppActionTypes.SET_ADD_COMMUNITY_OPEN; payload: boolean }
| {
type: AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN;
payload: boolean;
};
export { AppActionTypes, type AppAction };

View file

@ -1,30 +1,27 @@
import { setState } from "../state"; import { setState } from "../state";
import { AppActionTypes, AppAction } from "./actions";
import { IAppState } from "./types";
function appReducer(_state: IAppState, action: AppAction) { const setHomeOpen = (value: boolean) => {
switch (action.type) { setState("app", "homeOpen", value);
case AppActionTypes.SET_HOME_OPEN: };
setState("app", "homeOpen", action.payload);
break; const setSettingsOpen = (value: boolean) => {
case AppActionTypes.SET_SETTINGS_OPEN:
setState("app", "dialogsOpen", "settingsOpen", false); setState("app", "dialogsOpen", "settingsOpen", false);
setState("app", "dialogsOpen", "settingsOpen", action.payload); setState("app", "dialogsOpen", "settingsOpen", value);
break; };
case AppActionTypes.SET_ADD_COMMUNITY_OPEN:
setState("app", "dialogsOpen", "addCommunityOpen", false);
setState("app", "dialogsOpen", "addCommunityOpen", action.payload);
break;
case AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN:
setState("app", "dialogsOpen", "communitySettingsOpen", false);
setState(
"app",
"dialogsOpen",
"communitySettingsOpen",
action.payload,
);
break;
}
}
export { appReducer }; const setAddCommunityOpen = (value: boolean) => {
setState("app", "dialogsOpen", "addCommunityOpen", false);
setState("app", "dialogsOpen", "addCommunityOpen", value);
};
const setCommunitySettingsOpen = (value: boolean) => {
setState("app", "dialogsOpen", "communitySettingsOpen", false);
setState("app", "dialogsOpen", "communitySettingsOpen", value);
};
export {
setHomeOpen,
setSettingsOpen,
setAddCommunityOpen,
setCommunitySettingsOpen,
};

View file

@ -1,3 +1,2 @@
export * from "./app"; export * from "./app";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,29 +0,0 @@
import {
IFetchLoginRequest,
IFetchLoginResponse,
IFetchRefreshResponseError,
IFetchRefreshResponseSuccess,
} from "../../api/auth";
enum AuthActionTypes {
SET_LOGGED_IN = "SET_LOGGED_IN",
FETCH_LOGIN_START = "FETCH_LOGIN_START",
FETCH_LOGIN_FINISH = "FETCH_LOGIN_FINISH",
FETCH_REFRESH_START = "FETCH_REFRESH_START",
FETCH_REFRESH_FINISH = "FETCH_REFRESH_FINISH",
}
type AuthAction =
| { type: AuthActionTypes.SET_LOGGED_IN; payload: boolean }
| { type: AuthActionTypes.FETCH_LOGIN_START; payload: IFetchLoginRequest }
| {
type: AuthActionTypes.FETCH_LOGIN_FINISH;
payload: IFetchLoginResponse;
}
| { type: AuthActionTypes.FETCH_REFRESH_START }
| {
type: AuthActionTypes.FETCH_REFRESH_FINISH;
payload: IFetchRefreshResponseError | IFetchRefreshResponseSuccess;
};
export { AuthActionTypes, type AuthAction };

View file

@ -1,28 +1,45 @@
import { fetchLogin, fetchRefresh } from "../../services/auth";
import { setState } from "../state"; import { setState } from "../state";
import { AuthActionTypes, AuthAction } from "./actions"; import { IAuthSession } from "./types";
import { IAuthState } from "./types";
function authReducer(_state: IAuthState, action: AuthAction) { const setRegisterSuccess = (value: boolean) => {
switch (action.type) { setState("auth", "registerSuccess", value);
case AuthActionTypes.SET_LOGGED_IN: };
setState("auth", "loggedIn", action.payload);
break;
case AuthActionTypes.FETCH_LOGIN_START:
fetchLogin(action.payload.username, action.payload.password);
break;
case AuthActionTypes.FETCH_LOGIN_FINISH:
setState("auth", "session", action.payload);
break;
case AuthActionTypes.FETCH_REFRESH_START:
fetchRefresh();
break;
case AuthActionTypes.FETCH_REFRESH_FINISH:
if ("id" in action.payload) {
setState("auth", "session", action.payload);
}
break;
}
}
export { authReducer }; const resetRegisterSuccess = () => {
setState("auth", "registerSuccess", undefined);
};
const setLoggedIn = (value: boolean) => {
setState("auth", "loggedIn", value);
};
const resetLoggedIn = () => {
setState("auth", "loggedIn", undefined);
};
const setAuthSession = (session: IAuthSession) => {
setState("auth", "session", session);
};
const resetAuthSession = () => {
setState("auth", "session", undefined);
};
const setAuthSessionKey = (key: Uint8Array<ArrayBuffer>) => {
setState("auth", "session", "key", key);
};
const setAuthSessionIV = (iv: Uint8Array<ArrayBuffer>) => {
setState("auth", "session", "iv", iv);
};
export {
setRegisterSuccess,
resetRegisterSuccess,
setLoggedIn,
resetLoggedIn,
setAuthSession,
resetAuthSession,
setAuthSessionKey,
setAuthSessionIV,
};

View file

@ -1,3 +1,2 @@
export * from "./auth"; export * from "./auth";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,12 +1,16 @@
interface IAuthState { interface IAuthState {
registerSuccess?: boolean;
loggedIn?: boolean; loggedIn?: boolean;
session?: ISession; session?: IAuthSession;
} }
interface ISession { interface IAuthSession {
id?: string; id?: string;
ownerId?: string; ownerId?: string;
token?: string; token?: string;
storageSecret?: string;
key?: Uint8Array<ArrayBuffer>;
iv?: Uint8Array<ArrayBuffer>;
} }
export { type IAuthState, type ISession }; export { type IAuthState, type IAuthSession };

View file

@ -1,79 +0,0 @@
import {
ICreateChannelRequest,
IUpdateChannelRequest,
IFetchChannelResponse,
ICreateChannelResponse,
IUpdateChannelResponse,
IRemoveChannelResponse,
IFetchChannelMessagesResponse,
IFetchChannelMessage,
} from "../../api/channel";
import { IFetchCommunityChannel } from "../../api/community";
enum ChannelActionTypes {
SET_CHANNEL = "SET_CHANNEL",
SET_ACTIVE_CHANNEL = "SET_ACTIVE_CHANNEL",
SET_TEXT = "SET_TEXT",
SET_CHANNEL_MESSAGE = "SET_CHANNEL_MESSAGE",
FETCH_CHANNEL_START = "FETCH_CHANNEL_START",
FETCH_CHANNEL_FINISH = "FETCH_CHANNEL_FINISH",
CREATE_CHANNEL_START = "CREATE_CHANNEL_START",
CREATE_CHANNEL_FINISH = "CREATE_CHANNEL_FINISH",
UPDATE_CHANNEL_START = "UPDATE_CHANNEL_START",
UPDATE_CHANNEL_FINISH = "UPDATE_CHANNEL_FINISH",
REMOVE_CHANNEL_START = "REMOVE_CHANNEL_START",
REMOVE_CHANNEL_FINISH = "REMOVE_CHANNEL_FINISH",
FETCH_CHANNEL_MESSAGES_START = "FETCH_CHANNEL_MESSAGES_START",
FETCH_CHANNEL_MESSAGES_FINISH = "FETCH_CHANNEL_MESSAGES_FINISH",
}
type ChannelAction =
| {
type: ChannelActionTypes.SET_CHANNEL;
payload: IFetchCommunityChannel;
}
| {
type: ChannelActionTypes.SET_ACTIVE_CHANNEL;
payload: string | undefined;
}
| {
type: ChannelActionTypes.SET_TEXT;
payload: { id: string; text: string };
}
| {
type: ChannelActionTypes.SET_CHANNEL_MESSAGE;
payload: { id: string; message: IFetchChannelMessage };
}
| { type: ChannelActionTypes.FETCH_CHANNEL_START; payload: string }
| {
type: ChannelActionTypes.FETCH_CHANNEL_FINISH;
payload: IFetchChannelResponse;
}
| {
type: ChannelActionTypes.CREATE_CHANNEL_START;
payload: ICreateChannelRequest;
}
| {
type: ChannelActionTypes.CREATE_CHANNEL_FINISH;
payload: ICreateChannelResponse;
}
| {
type: ChannelActionTypes.UPDATE_CHANNEL_START;
payload: IUpdateChannelRequest;
}
| {
type: ChannelActionTypes.UPDATE_CHANNEL_FINISH;
payload: IUpdateChannelResponse;
}
| { type: ChannelActionTypes.REMOVE_CHANNEL_START; payload: string }
| {
type: ChannelActionTypes.REMOVE_CHANNEL_FINISH;
payload: IRemoveChannelResponse;
}
| { type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_START; payload: string }
| {
type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_FINISH;
payload: IFetchChannelMessagesResponse;
};
export { ChannelActionTypes, type ChannelAction };

View file

@ -1,93 +1,60 @@
import { import { IMessage } from "../message";
fetchChannel,
createChannel,
updateChannel,
removeChannel,
fetchChannelMessages,
} from "../../services/channel";
import { setState } from "../state"; import { setState } from "../state";
import { ChannelActionTypes, ChannelAction } from "./actions"; import { IChannel } from "./types";
import { IChannelState } from "./types";
function channelReducer(_state: IChannelState, action: ChannelAction) { const setChannel = (channel: IChannel) => {
switch (action.type) { setState("channel", "channels", channel.id, channel);
case ChannelActionTypes.SET_CHANNEL: };
setState("channel", "channels", action.payload.id, action.payload);
break; const deleteChannel = (channelId: string) => {
case ChannelActionTypes.SET_ACTIVE_CHANNEL: setState("channel", "channels", channelId, undefined);
setState("channel", "active", action.payload); };
break;
case ChannelActionTypes.SET_TEXT: const setActiveChannel = (channelId: string) => {
setState( setState("channel", "active", channelId);
"channel", };
"channels",
action.payload.id, const resetActiveChannel = () => {
"text", setState("channel", "active", undefined);
action.payload.text, };
);
break; const setText = (channelId: string, text: string) => {
case ChannelActionTypes.SET_CHANNEL_MESSAGE: setState("channel", "channels", channelId, "text", text);
setState( };
"channel",
"channels", const setChannelMessage = (channelId: string, message: IMessage) => {
action.payload.id, setState("channel", "channels", channelId, "messages", message.id, message);
"messages", };
action.payload.message.id,
action.payload.message, const setChannelMessages = (channelId: string, addMessages: IMessage[]) => {
); setState("channel", "channels", channelId, "messages", (messages) => {
break;
case ChannelActionTypes.FETCH_CHANNEL_START:
fetchChannel(action.payload);
break;
case ChannelActionTypes.FETCH_CHANNEL_FINISH:
setState("channel", "channels", action.payload.id, action.payload);
break;
case ChannelActionTypes.CREATE_CHANNEL_START:
createChannel(action.payload.name, action.payload.communityId);
break;
case ChannelActionTypes.CREATE_CHANNEL_FINISH:
setState("channel", "channels", action.payload.id, action.payload);
break;
case ChannelActionTypes.UPDATE_CHANNEL_START:
updateChannel(
action.payload.id,
action.payload.name,
action.payload.description,
);
break;
case ChannelActionTypes.UPDATE_CHANNEL_FINISH:
setState("channel", "channels", action.payload.id, action.payload);
break;
case ChannelActionTypes.REMOVE_CHANNEL_START:
removeChannel(action.payload);
break;
case ChannelActionTypes.REMOVE_CHANNEL_FINISH:
setState("channel", "channels", (channels) => {
const copy = { ...channels };
delete copy[action.payload.id];
return copy;
});
break;
case ChannelActionTypes.FETCH_CHANNEL_MESSAGES_START:
fetchChannelMessages(action.payload);
break;
case ChannelActionTypes.FETCH_CHANNEL_MESSAGES_FINISH:
setState(
"channel",
"channels",
action.payload.id,
"messages",
(messages) => {
const newMessages = Object.fromEntries( const newMessages = Object.fromEntries(
action.payload.messages.map((item) => [item.id, item]), addMessages.map((item) => [item.id, item]),
); );
const copy = { ...messages, ...newMessages }; const copy = { ...messages, ...newMessages };
return copy; return copy;
}, });
); };
break;
}
}
export { channelReducer }; const deleteChannelMessage = (channelId: string, messageId: string) => {
setState(
"channel",
"channels",
channelId,
"messages",
messageId,
undefined,
);
};
export {
setChannel,
deleteChannel,
setActiveChannel,
resetActiveChannel,
setText,
setChannelMessage,
setChannelMessages,
deleteChannelMessage,
};

View file

@ -1,3 +1,2 @@
export * from "./channel"; export * from "./channel";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -2,7 +2,7 @@ import { IMessage } from "../message";
interface IChannelState { interface IChannelState {
active?: string; active?: string;
channels: Record<string, IChannel>; channels: Record<string, IChannel | undefined>;
} }
interface IChannel { interface IChannel {
@ -12,7 +12,7 @@ interface IChannel {
communityId?: string; communityId?: string;
creationDate?: number; creationDate?: number;
text?: string; text?: string;
messages?: Record<string, IMessage>; messages?: Record<string, IMessage | undefined>;
} }
export { type IChannelState, type IChannel }; export { type IChannelState, type IChannel };

View file

@ -1,104 +0,0 @@
import {
ICreateCommunityRequest,
IUpdateCommunityRequest,
IFetchCommunityResponse,
ICreateCommunityResponse,
IUpdateCommunityResponse,
IRemoveCommunityResponse,
IFetchCommunityChannelsResponse,
IFetchCommunityRolesResponse,
IFetchCommunityMembersResponse,
IFetchCommunityInvitesResponse,
} from "../../api/community";
import { IFetchUserCommunity } from "../../api/user";
enum CommunityActionTypes {
SET_COMMUNITY = "SET_COMMUNITY",
SET_ACTIVE_COMMUNITY = "SET_ACTIVE_COMMUNITY",
FETCH_COMMUNITY_START = "FETCH_COMMUNITY_START",
FETCH_COMMUNITY_FINISH = "FETCH_COMMUNITY_FINISH",
CREATE_COMMUNITY_START = "CREATE_COMMUNITY_START",
CREATE_COMMUNITY_FINISH = "CREATE_COMMUNITY_FINISH",
UPDATE_COMMUNITY_START = "UPDATE_COMMUNITY_START",
UPDATE_COMMUNITY_FINISH = "UPDATE_COMMUNITY_FINISH",
REMOVE_COMMUNITY_START = "REMOVE_COMMUNITY_START",
REMOVE_COMMUNITY_FINISH = "REMOVE_COMMUNITY_FINISH",
FETCH_COMMUNITY_CHANNELS_START = "FETCH_COMMUNITY_CHANNELS_START",
FETCH_COMMUNITY_CHANNELS_FINISH = "FETCH_COMMUNITY_CHANNELS_FINISH",
FETCH_COMMUNITY_ROLES_START = "FETCH_COMMUNITY_ROLES_START",
FETCH_COMMUNITY_ROLES_FINISH = "FETCH_COMMUNITY_ROLES_FINISH",
FETCH_COMMUNITY_MEMBERS_START = "FETCH_COMMUNITY_MEMBERS_START",
FETCH_COMMUNITY_MEMBERS_FINISH = "FETCH_COMMUNITY_MEMBERS_FINISH",
FETCH_COMMUNITY_INVITES_START = "FETCH_COMMUNITY_INVITES_START",
FETCH_COMMUNITY_INVITES_FINISH = "FETCH_COMMUNITY_INVITES_FINISH",
}
type CommunityAction =
| {
type: CommunityActionTypes.SET_COMMUNITY;
payload: IFetchUserCommunity;
}
| {
type: CommunityActionTypes.SET_ACTIVE_COMMUNITY;
payload: string | undefined;
}
| { type: CommunityActionTypes.FETCH_COMMUNITY_START; payload: string }
| {
type: CommunityActionTypes.FETCH_COMMUNITY_FINISH;
payload: IFetchCommunityResponse;
}
| {
type: CommunityActionTypes.CREATE_COMMUNITY_START;
payload: ICreateCommunityRequest;
}
| {
type: CommunityActionTypes.CREATE_COMMUNITY_FINISH;
payload: ICreateCommunityResponse;
}
| {
type: CommunityActionTypes.UPDATE_COMMUNITY_START;
payload: IUpdateCommunityRequest;
}
| {
type: CommunityActionTypes.UPDATE_COMMUNITY_FINISH;
payload: IUpdateCommunityResponse;
}
| { type: CommunityActionTypes.REMOVE_COMMUNITY_START; payload: string }
| {
type: CommunityActionTypes.REMOVE_COMMUNITY_FINISH;
payload: IRemoveCommunityResponse;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_CHANNELS_START;
payload: string;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_CHANNELS_FINISH;
payload: IFetchCommunityChannelsResponse;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_ROLES_START;
payload: string;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_ROLES_FINISH;
payload: IFetchCommunityRolesResponse;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_MEMBERS_START;
payload: string;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_MEMBERS_FINISH;
payload: IFetchCommunityMembersResponse;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_INVITES_START;
payload: string;
}
| {
type: CommunityActionTypes.FETCH_COMMUNITY_INVITES_FINISH;
payload: IFetchCommunityInvitesResponse;
};
export { CommunityActionTypes, type CommunityAction };

View file

@ -1,110 +1,81 @@
import { import { deriveKey } from "../../services/crypto";
fetchCommunity, import { IChannel } from "../channel";
createCommunity, import { IInvite } from "../invite";
updateCommunity, import { IRole } from "../role";
removeCommunity,
fetchCommunityChannels,
fetchCommunityRoles,
fetchCommunityMembers,
fetchCommunityInvites,
} from "../../services/community";
import { setState } from "../state"; import { setState } from "../state";
import { CommunityActionTypes, CommunityAction } from "./actions"; import { IUser } from "../user";
import { ICommunityState } from "./types"; import { ICommunity } from "./types";
function communityReducer(_state: ICommunityState, action: CommunityAction) { const setCommunity = (community: ICommunity) => {
switch (action.type) { setState("community", "communities", community.id, community);
case CommunityActionTypes.SET_COMMUNITY: };
setState(
"community",
"communities",
action.payload.id,
action.payload,
);
break;
case CommunityActionTypes.SET_ACTIVE_COMMUNITY:
setState("community", "active", action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_START:
fetchCommunity(action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_FINISH:
setState(
"community",
"communities",
action.payload.id,
action.payload,
);
break;
case CommunityActionTypes.CREATE_COMMUNITY_START:
createCommunity(action.payload.name);
break;
case CommunityActionTypes.CREATE_COMMUNITY_FINISH:
setState(
"community",
"communities",
action.payload.id,
action.payload,
);
break;
case CommunityActionTypes.UPDATE_COMMUNITY_START:
updateCommunity(
action.payload.id,
action.payload.name,
action.payload.description,
);
break;
case CommunityActionTypes.UPDATE_COMMUNITY_FINISH:
setState(
"community",
"communities",
action.payload.id,
action.payload,
);
break;
case CommunityActionTypes.REMOVE_COMMUNITY_START:
removeCommunity(action.payload);
break;
case CommunityActionTypes.REMOVE_COMMUNITY_FINISH:
setState("community", "communities", (communities) => {
const copy = { ...communities };
delete copy[action.payload.id];
return copy;
});
break;
case CommunityActionTypes.FETCH_COMMUNITY_CHANNELS_START:
fetchCommunityChannels(action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_CHANNELS_FINISH:
setState("community", "communities", action.payload.id, {
channels: action.payload.channels.map((channel) => channel.id),
});
break;
case CommunityActionTypes.FETCH_COMMUNITY_ROLES_START:
fetchCommunityRoles(action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_ROLES_FINISH:
setState("community", "communities", action.payload.id, {
roles: action.payload.roles.map((role) => role.id),
});
break;
case CommunityActionTypes.FETCH_COMMUNITY_MEMBERS_START:
fetchCommunityMembers(action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_MEMBERS_FINISH:
setState("community", "communities", action.payload.id, {
members: action.payload.members.map((member) => member.id),
});
break;
case CommunityActionTypes.FETCH_COMMUNITY_INVITES_START:
fetchCommunityInvites(action.payload);
break;
case CommunityActionTypes.FETCH_COMMUNITY_INVITES_FINISH:
setState("community", "communities", action.payload.id, {
invites: action.payload.invites.map((invite) => invite.id),
});
break;
}
}
export { communityReducer }; const deleteCommunity = (communityId: string) => {
setState("community", "communities", communityId, undefined);
};
const setActiveCommunity = (communityId: string) => {
setState("community", "active", communityId);
};
const resetActiveCommunity = () => {
setState("community", "active", undefined);
};
const setCommunityChannels = (communityId: string, channels: IChannel[]) => {
setState("community", "communities", communityId, {
channels: channels.map((channel) => channel.id),
});
};
const setCommunityRoles = (communityId: string, roles: IRole[]) => {
setState("community", "communities", communityId, {
roles: roles.map((role) => role.id),
});
};
const setCommunityMembers = (communityId: string, members: IUser[]) => {
setState("community", "communities", communityId, {
members: members.map((member) => member.id),
});
};
const setCommunityInvites = (communityId: string, invites: IInvite[]) => {
setState("community", "communities", communityId, {
invites: invites.map((invite) => invite.id),
});
};
const setCommunityEncryptionKey = (
communityId: string,
encryptionKey: string,
) => {
setState(
"community",
"communities",
communityId,
"encryptionKey",
encryptionKey,
);
deriveKey(encryptionKey).then((derivedKey) =>
setCommunityDerivedKey(communityId, derivedKey),
);
};
const setCommunityDerivedKey = (communityId: string, derivedKey: CryptoKey) => {
setState("community", "communities", communityId, "derivedKey", derivedKey);
};
export {
setCommunity,
deleteCommunity,
setActiveCommunity,
resetActiveCommunity,
setCommunityChannels,
setCommunityRoles,
setCommunityMembers,
setCommunityInvites,
setCommunityEncryptionKey,
setCommunityDerivedKey,
};

View file

@ -1,3 +1,2 @@
export * from "./community"; export * from "./community";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,6 +1,6 @@
interface ICommunityState { interface ICommunityState {
active?: string; active?: string;
communities: Record<string, ICommunity>; communities: Record<string, ICommunity | undefined>;
} }
interface ICommunity { interface ICommunity {
@ -13,6 +13,8 @@ interface ICommunity {
roles?: string[]; roles?: string[];
members?: string[]; members?: string[];
invites?: string[]; invites?: string[];
encryptionKey?: string;
derivedKey?: CryptoKey;
} }
export { type ICommunityState, type ICommunity }; export { type ICommunityState, type ICommunity };

View file

@ -1,39 +0,0 @@
import { IFetchCommunityInvite } from "../../api/community";
import {
IFetchInviteResponse,
IRemoveInviteResponse,
IAcceptInviteResponse,
} from "../../api/invite";
enum InviteActionTypes {
SET_INVITE = "SET_INVITE",
FETCH_INVITE_START = "FETCH_INVITE_START",
FETCH_INVITE_FINISH = "FETCH_INVITE_FINISH",
REMOVE_INVITE_START = "REMOVE_INVITE_START",
REMOVE_INVITE_FINISH = "REMOVE_INVITE_FINISH",
ACCEPT_INVITE_START = "ACCEPT_INVITE_START",
ACCEPT_INVITE_FINISH = "ACCEPT_INVITE_FINISH",
}
type InviteAction =
| {
type: InviteActionTypes.SET_INVITE;
payload: IFetchCommunityInvite;
}
| { type: InviteActionTypes.FETCH_INVITE_START; payload: string }
| {
type: InviteActionTypes.FETCH_INVITE_FINISH;
payload: IFetchInviteResponse;
}
| { type: InviteActionTypes.REMOVE_INVITE_START; payload: string }
| {
type: InviteActionTypes.REMOVE_INVITE_FINISH;
payload: IRemoveInviteResponse;
}
| { type: InviteActionTypes.ACCEPT_INVITE_START; payload: string }
| {
type: InviteActionTypes.ACCEPT_INVITE_FINISH;
payload: IAcceptInviteResponse;
};
export { InviteActionTypes, type InviteAction };

View file

@ -1,3 +1,2 @@
export * from "./invite"; export * from "./invite";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,35 +1,12 @@
import { fetchInvite, removeInvite, acceptInvite } from "../../services/invite";
import { setState } from "../state"; import { setState } from "../state";
import { InviteActionTypes, InviteAction } from "./actions"; import { IInvite } from "./types";
import { IInviteState } from "./types";
function inviteReducer(_state: IInviteState, action: InviteAction) { const setInvite = (invite: IInvite) => {
switch (action.type) { setState("invite", "invites", invite.id, invite);
case InviteActionTypes.SET_INVITE: };
setState("invite", "invites", action.payload.id, action.payload);
break;
case InviteActionTypes.FETCH_INVITE_START:
fetchInvite(action.payload);
break;
case InviteActionTypes.FETCH_INVITE_FINISH:
setState("invite", "invites", action.payload.id, action.payload);
break;
case InviteActionTypes.REMOVE_INVITE_START:
removeInvite(action.payload);
break;
case InviteActionTypes.REMOVE_INVITE_FINISH:
setState("invite", "invites", (invites) => {
const copy = { ...invites };
delete copy[action.payload.id];
return copy;
});
break;
case InviteActionTypes.ACCEPT_INVITE_START:
acceptInvite(action.payload);
break;
case InviteActionTypes.ACCEPT_INVITE_FINISH:
break;
}
}
export { inviteReducer }; const deleteInvite = (inviteId: string) => {
setState("invite", "invites", inviteId, undefined);
};
export { setInvite, deleteInvite };

View file

@ -1,5 +1,5 @@
interface IInviteState { interface IInviteState {
invites: Record<string, IInvite>; invites: Record<string, IInvite | undefined>;
} }
interface IInvite { interface IInvite {

View file

@ -1,49 +0,0 @@
import {
ICreateMessageRequest,
IUpdateMessageRequest,
IFetchMessageResponse,
ICreateMessageResponse,
IUpdateMessageResponse,
IRemoveMessageResponse,
} from "../../api/message";
enum MessageActionTypes {
FETCH_MESSAGE_START = "FETCH_MESSAGE_START",
FETCH_MESSAGE_FINISH = "FETCH_MESSAGE_FINISH",
CREATE_MESSAGE_START = "CREATE_MESSAGE_START",
CREATE_MESSAGE_FINISH = "CREATE_MESSAGE_FINISH",
UPDATE_MESSAGE_START = "UPDATE_MESSAGE_START",
UPDATE_MESSAGE_FINISH = "UPDATE_MESSAGE_FINISH",
REMOVE_MESSAGE_START = "REMOVE_MESSAGE_START",
REMOVE_MESSAGE_FINISH = "REMOVE_MESSAGE_FINISH",
}
type MessageAction =
| { type: MessageActionTypes.FETCH_MESSAGE_START; payload: string }
| {
type: MessageActionTypes.FETCH_MESSAGE_FINISH;
payload: IFetchMessageResponse;
}
| {
type: MessageActionTypes.CREATE_MESSAGE_START;
payload: ICreateMessageRequest;
}
| {
type: MessageActionTypes.CREATE_MESSAGE_FINISH;
payload: ICreateMessageResponse;
}
| {
type: MessageActionTypes.UPDATE_MESSAGE_START;
payload: IUpdateMessageRequest;
}
| {
type: MessageActionTypes.UPDATE_MESSAGE_FINISH;
payload: IUpdateMessageResponse;
}
| { type: MessageActionTypes.REMOVE_MESSAGE_START; payload: string }
| {
type: MessageActionTypes.REMOVE_MESSAGE_FINISH;
payload: IRemoveMessageResponse;
};
export { MessageActionTypes, type MessageAction };

View file

@ -1,3 +1,2 @@
export * from "./message"; export * from "./message";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,59 +1,22 @@
import {
fetchMessage,
createMessage,
updateMessage,
removeMessage,
} from "../../services/message";
import { setState } from "../state"; import { setState } from "../state";
import { MessageActionTypes, MessageAction } from "./actions"; import { IMessage } from "./types";
import { IMessageState } from "./types";
function messageReducer(_state: IMessageState, action: MessageAction) { const setMessage = (message: IMessage) => {
switch (action.type) { setState("message", "message", message);
case MessageActionTypes.FETCH_MESSAGE_START: if (message.channelId) {
fetchMessage(action.payload);
break;
case MessageActionTypes.FETCH_MESSAGE_FINISH:
setState("message", "message", action.payload);
setState( setState(
"channel", "channel",
"channels", "channels",
action.payload.channelId, message.channelId,
action.payload,
);
break;
case MessageActionTypes.CREATE_MESSAGE_START:
createMessage(action.payload.text, action.payload.channelId);
break;
case MessageActionTypes.CREATE_MESSAGE_FINISH:
setState("message", "message", action.payload);
setState(
"channel",
"channels",
action.payload.channelId,
"messages", "messages",
action.payload.id, message.id,
action.payload, message,
); );
break;
case MessageActionTypes.UPDATE_MESSAGE_START:
updateMessage(action.payload.id, action.payload.text);
break;
case MessageActionTypes.UPDATE_MESSAGE_FINISH:
setState("message", "message", action.payload);
setState(
"channel",
"channels",
action.payload.channelId,
action.payload,
);
break;
case MessageActionTypes.REMOVE_MESSAGE_START:
removeMessage(action.payload);
break;
case MessageActionTypes.REMOVE_MESSAGE_FINISH:
break;
} }
} };
export { messageReducer }; const deleteMessage = () => {
setState("message", "message", undefined);
};
export { setMessage, deleteMessage };

View file

@ -5,11 +5,13 @@ interface IMessageState {
interface IMessage { interface IMessage {
id: string; id: string;
text: string; text: string;
iv: string;
editHistory?: string[]; editHistory?: string[];
edited: boolean; edited: boolean;
ownerId: string; ownerId: string;
channelId?: string; channelId?: string;
creationDate: number; creationDate: number;
decryptionStatus?: boolean;
} }
export { type IMessageState, type IMessage }; export { type IMessageState, type IMessage };

View file

@ -1,25 +0,0 @@
import { IState } from "./types";
import { Action } from "./actions";
import { AppAction, appReducer } from "./app";
import { AuthAction, authReducer } from "./auth";
import { UserAction, userReducer } from "./user";
import { CommunityAction, communityReducer } from "./community";
import { ChannelAction, channelReducer } from "./channel";
import { RoleAction, roleReducer } from "./role";
import { SessionAction, sessionReducer } from "./session";
import { InviteAction, inviteReducer } from "./invite";
import { MessageAction, messageReducer } from "./message";
function reducer(state: IState, action: Action) {
appReducer(state.app, action as AppAction);
authReducer(state.auth, action as AuthAction);
userReducer(state.user, action as UserAction);
communityReducer(state.community, action as CommunityAction);
channelReducer(state.channel, action as ChannelAction);
roleReducer(state.role, action as RoleAction);
sessionReducer(state.session, action as SessionAction);
inviteReducer(state.invite, action as InviteAction);
messageReducer(state.message, action as MessageAction);
}
export { reducer };

View file

@ -1,49 +0,0 @@
import { IFetchCommunityRole } from "../../api/community";
import {
ICreateRoleRequest,
IUpdateRoleRequest,
IFetchRoleResponse,
ICreateRoleResponse,
IUpdateRoleResponse,
IRemoveRoleResponse,
} from "../../api/role";
enum RoleActionTypes {
SET_ROLE = "SET_ROLE",
FETCH_ROLE_START = "FETCH_ROLE_START",
FETCH_ROLE_FINISH = "FETCH_ROLE_FINISH",
CREATE_ROLE_START = "CREATE_ROLE_START",
CREATE_ROLE_FINISH = "CREATE_ROLE_FINISH",
UPDATE_ROLE_START = "UPDATE_ROLE_START",
UPDATE_ROLE_FINISH = "UPDATE_ROLE_FINISH",
REMOVE_ROLE_START = "REMOVE_ROLE_START",
REMOVE_ROLE_FINISH = "REMOVE_ROLE_FINISH",
}
type RoleAction =
| {
type: RoleActionTypes.SET_ROLE;
payload: IFetchCommunityRole;
}
| { type: RoleActionTypes.FETCH_ROLE_START; payload: string }
| {
type: RoleActionTypes.FETCH_ROLE_FINISH;
payload: IFetchRoleResponse;
}
| { type: RoleActionTypes.CREATE_ROLE_START; payload: ICreateRoleRequest }
| {
type: RoleActionTypes.CREATE_ROLE_FINISH;
payload: ICreateRoleResponse;
}
| { type: RoleActionTypes.UPDATE_ROLE_START; payload: IUpdateRoleRequest }
| {
type: RoleActionTypes.UPDATE_ROLE_FINISH;
payload: IUpdateRoleResponse;
}
| { type: RoleActionTypes.REMOVE_ROLE_START; payload: string }
| {
type: RoleActionTypes.REMOVE_ROLE_FINISH;
payload: IRemoveRoleResponse;
};
export { RoleActionTypes, type RoleAction };

View file

@ -1,3 +1,2 @@
export * from "./role"; export * from "./role";
export * from "./actions";
export * from "./types"; export * from "./types";

View file

@ -1,47 +1,12 @@
import {
fetchRole,
createRole,
updateRole,
removeRole,
} from "../../services/role";
import { setState } from "../state"; import { setState } from "../state";
import { RoleActionTypes, RoleAction } from "./actions"; import { IRole } from "./types";
import { IRoleState } from "./types";
function roleReducer(_state: IRoleState, action: RoleAction) { const setRole = (role: IRole) => {
switch (action.type) { setState("role", "roles", role.id, role);
case RoleActionTypes.SET_ROLE: };
setState("role", "roles", action.payload.id, action.payload);
break;
case RoleActionTypes.FETCH_ROLE_START:
fetchRole(action.payload);
break;
case RoleActionTypes.FETCH_ROLE_FINISH:
setState("role", "roles", action.payload.id, action.payload);
break;
case RoleActionTypes.CREATE_ROLE_START:
createRole(action.payload.name, action.payload.communityId);
break;
case RoleActionTypes.CREATE_ROLE_FINISH:
setState("role", "roles", action.payload.id, action.payload);
break;
case RoleActionTypes.UPDATE_ROLE_START:
updateRole(action.payload.id, action.payload.name);
break;
case RoleActionTypes.UPDATE_ROLE_FINISH:
setState("role", "roles", action.payload.id, action.payload);
break;
case RoleActionTypes.REMOVE_ROLE_START:
removeRole(action.payload);
break;
case RoleActionTypes.REMOVE_ROLE_FINISH:
setState("role", "roles", (roles) => {
const copy = { ...roles };
delete copy[action.payload.id];
return copy;
});
break;
}
}
export { roleReducer }; const deleteRole = (roleId: string) => {
setState("role", "roles", roleId, undefined);
};
export { setRole, deleteRole };

View file

@ -1,5 +1,5 @@
interface IRoleState { interface IRoleState {
roles: Record<string, IRole>; roles: Record<string, IRole | undefined>;
} }
interface IRole { interface IRole {

Some files were not shown because too many files have changed in this diff Show more