From 9153ba841d5103215651f55cc95e2908fbcebc78e089d967e02577f85db5615f Mon Sep 17 00:00:00 2001 From: aslan Date: Sun, 11 Jan 2026 14:17:34 -0500 Subject: [PATCH] Add real time messaging --- package.json | 2 +- src/App.tsx | 10 +- src/api/auth/auth.ts | 7 +- src/api/auth/types.ts | 9 +- src/api/channel/channel.ts | 9 + src/api/channel/types.ts | 20 ++ src/api/message/index.ts | 2 + src/api/message/message.ts | 42 +++ src/api/message/types.ts | 51 +++ src/components/CommunityBar/CommunityBar.tsx | 15 +- src/components/CommunityBar/types.ts | 1 + .../CommunityModal/CommunityModal.tsx | 26 +- .../CommunitySettingsModal.tsx | 26 ++ .../CommunitySettingsModal/index.ts | 2 + .../CommunitySettingsModal/types.ts | 6 + src/components/MessageBar/MessageBar.tsx | 24 +- src/components/MessageBar/index.ts | 1 + src/components/MessageBar/types.ts | 7 + src/services/auth/auth.ts | 9 + src/services/channel/channel.ts | 20 +- src/services/message/index.ts | 1 + src/services/message/message.ts | 56 ++++ src/services/websocket/config.json | 6 + src/services/websocket/index.ts | 2 + src/services/websocket/types.ts | 46 +++ src/services/websocket/websocket.ts | 57 ++++ src/store/actions.ts | 7 +- src/store/app/actions.ts | 7 +- src/store/app/app.ts | 9 + src/store/app/types.ts | 1 + src/store/auth/actions.ts | 7 +- src/store/auth/auth.ts | 7 +- src/store/auth/types.ts | 1 + src/store/channel/actions.ts | 19 ++ src/store/channel/channel.ts | 41 ++- src/store/channel/types.ts | 4 + src/store/community/community.ts | 2 +- src/store/invite/invite.ts | 2 +- src/store/message/actions.ts | 49 +++ src/store/message/index.ts | 3 + src/store/message/message.ts | 59 ++++ src/store/message/types.ts | 15 + src/store/reducers.ts | 2 + src/store/role/actions.ts | 3 +- src/store/role/role.ts | 4 +- src/store/session/session.ts | 2 +- src/store/state.ts | 5 + src/store/types.ts | 2 + src/store/user/user.ts | 2 +- .../MainView.tsx => AppView/AppView.tsx} | 16 +- src/views/AppView/index.ts | 1 + src/views/ChannelView/ChannelView.tsx | 9 + src/views/ChatView/ChatView.tsx | 303 +++++++----------- src/views/LoginView/LoginView.tsx | 12 +- src/views/MainView/index.ts | 1 - src/views/ModalView/ModalView.tsx | 21 ++ src/views/RegisterView/RegisterView.tsx | 23 +- src/views/SettingsView/SettingsView.tsx | 7 - src/views/SettingsView/index.ts | 1 - src/views/StartView/StartView.tsx | 7 + src/views/StartView/index.ts | 1 + 61 files changed, 882 insertions(+), 230 deletions(-) create mode 100644 src/api/message/index.ts create mode 100644 src/api/message/message.ts create mode 100644 src/api/message/types.ts create mode 100644 src/components/CommunitySettingsModal/CommunitySettingsModal.tsx create mode 100644 src/components/CommunitySettingsModal/index.ts create mode 100644 src/components/CommunitySettingsModal/types.ts create mode 100644 src/components/MessageBar/types.ts create mode 100644 src/services/message/index.ts create mode 100644 src/services/message/message.ts create mode 100644 src/services/websocket/config.json create mode 100644 src/services/websocket/index.ts create mode 100644 src/services/websocket/types.ts create mode 100644 src/services/websocket/websocket.ts create mode 100644 src/store/message/actions.ts create mode 100644 src/store/message/index.ts create mode 100644 src/store/message/message.ts create mode 100644 src/store/message/types.ts rename src/views/{MainView/MainView.tsx => AppView/AppView.tsx} (80%) create mode 100644 src/views/AppView/index.ts delete mode 100644 src/views/MainView/index.ts delete mode 100644 src/views/SettingsView/SettingsView.tsx delete mode 100644 src/views/SettingsView/index.ts create mode 100644 src/views/StartView/StartView.tsx create mode 100644 src/views/StartView/index.ts diff --git a/package.json b/package.json index 4a381bc..3324c9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulsar-web", - "version": "0.3.0", + "version": "0.4.0", "description": "", "type": "module", "scripts": { diff --git a/src/App.tsx b/src/App.tsx index e3fdd72..d0ec5e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,17 @@ import { Route, Router } from "@solidjs/router"; import type { Component } from "solid-js"; -import { MainView } from "./views/MainView"; -import { SettingsView } from "./views/SettingsView"; +import { StartView } from "./views/StartView"; +import { AppView } from "./views/AppView"; import { LoginView } from "./views/LoginView"; +import { RegisterView } from "./views/RegisterView"; const App: Component = () => { return ( - - + + + ); }; diff --git a/src/api/auth/auth.ts b/src/api/auth/auth.ts index b466e56..f402811 100644 --- a/src/api/auth/auth.ts +++ b/src/api/auth/auth.ts @@ -2,7 +2,8 @@ import { callApi, HTTP } from "../tools"; import { IFetchLoginRequest, IFetchLoginResponse, - IFetchRefreshResponse, + IFetchRefreshResponseError, + IFetchRefreshResponseSuccess, } from "./types"; const fetchLoginApi = async ( @@ -11,7 +12,9 @@ const fetchLoginApi = async ( return await callApi(HTTP.POST, `auth/login`, request, true); }; -const fetchRefreshApi = async (): Promise => { +const fetchRefreshApi = async (): Promise< + IFetchRefreshResponseError | IFetchRefreshResponseSuccess +> => { return await callApi(HTTP.GET, `auth/refresh`, undefined, true); }; diff --git a/src/api/auth/types.ts b/src/api/auth/types.ts index 02a0edd..1a4aa67 100644 --- a/src/api/auth/types.ts +++ b/src/api/auth/types.ts @@ -8,7 +8,11 @@ interface IFetchLoginResponse { ownerId: string; } -interface IFetchRefreshResponse { +interface IFetchRefreshResponseError { + error: string; +} + +interface IFetchRefreshResponseSuccess { id: string; ownerId: string; token: string; @@ -17,5 +21,6 @@ interface IFetchRefreshResponse { export { type IFetchLoginRequest, type IFetchLoginResponse, - type IFetchRefreshResponse, + type IFetchRefreshResponseError, + type IFetchRefreshResponseSuccess, }; diff --git a/src/api/channel/channel.ts b/src/api/channel/channel.ts index 987baa1..53654f8 100644 --- a/src/api/channel/channel.ts +++ b/src/api/channel/channel.ts @@ -8,6 +8,8 @@ import { IUpdateChannelResponse, IRemoveChannelRequest, IRemoveChannelResponse, + IFetchChannelMessagesRequest, + IFetchChannelMessagesResponse, } from "./types"; const fetchChannelApi = async ( @@ -34,9 +36,16 @@ const removeChannelApi = async ( return await callApi(HTTP.DELETE, `channel/${request.id}`); }; +const fetchChannelMessagesApi = async ( + request: IFetchChannelMessagesRequest, +): Promise => { + return await callApi(HTTP.GET, `channel/${request.id}/messages`); +}; + export { fetchChannelApi, createChannelApi, updateChannelApi, removeChannelApi, + fetchChannelMessagesApi, }; diff --git a/src/api/channel/types.ts b/src/api/channel/types.ts index 7717193..2f451cb 100644 --- a/src/api/channel/types.ts +++ b/src/api/channel/types.ts @@ -36,6 +36,23 @@ interface IRemoveChannelResponse { communityId: string; } +interface IFetchChannelMessagesRequest { + id: string; +} + +interface IFetchChannelMessagesResponse { + id: string; + messages: IFetchChannelMessage[]; +} + +interface IFetchChannelMessage { + id: string; + text: string; + edited: boolean; + ownerId: string; + creationDate: number; +} + export { type IFetchChannel, type IFetchChannelRequest, @@ -46,4 +63,7 @@ export { type IUpdateChannelResponse, type IRemoveChannelResponse, type IRemoveChannelRequest, + type IFetchChannelMessagesRequest, + type IFetchChannelMessagesResponse, + type IFetchChannelMessage, }; diff --git a/src/api/message/index.ts b/src/api/message/index.ts new file mode 100644 index 0000000..d001067 --- /dev/null +++ b/src/api/message/index.ts @@ -0,0 +1,2 @@ +export * from "./message"; +export * from "./types"; diff --git a/src/api/message/message.ts b/src/api/message/message.ts new file mode 100644 index 0000000..442c510 --- /dev/null +++ b/src/api/message/message.ts @@ -0,0 +1,42 @@ +import { callApi, HTTP } from "../tools"; +import { + IFetchMessageRequest, + IFetchMessageResponse, + ICreateMessageRequest, + ICreateMessageResponse, + IUpdateMessageRequest, + IUpdateMessageResponse, + IRemoveMessageRequest, + IRemoveMessageResponse, +} from "./types"; + +const fetchMessageApi = async ( + request: IFetchMessageRequest, +): Promise => { + return await callApi(HTTP.GET, `message/${request.id}`); +}; + +const createMessageApi = async ( + request: ICreateMessageRequest, +): Promise => { + return await callApi(HTTP.POST, `message`, request); +}; + +const updateMessageApi = async ( + request: IUpdateMessageRequest, +): Promise => { + return await callApi(HTTP.PATCH, `message/${request.id}`, request); +}; + +const removeMessageApi = async ( + request: IRemoveMessageRequest, +): Promise => { + return await callApi(HTTP.DELETE, `message/${request.id}`); +}; + +export { + fetchMessageApi, + createMessageApi, + updateMessageApi, + removeMessageApi, +}; diff --git a/src/api/message/types.ts b/src/api/message/types.ts new file mode 100644 index 0000000..32d1e0a --- /dev/null +++ b/src/api/message/types.ts @@ -0,0 +1,51 @@ +interface IFetchMessage { + id: string; + text: string; + editHistory: string[]; + edited: boolean; + ownerId: string; + channelId: string; + creationDate: number; +} + +interface IFetchMessageRequest { + id: string; +} + +interface IFetchMessageResponse extends IFetchMessage {} + +interface ICreateMessageRequest { + text: string; + channelId: string; +} + +interface ICreateMessageResponse extends IFetchMessage {} + +interface IUpdateMessageRequest { + id: string; + text: string; +} + +interface IUpdateMessageResponse extends IFetchMessage {} + +interface IRemoveMessageRequest { + id: string; +} + +interface IRemoveMessageResponse { + id: string; + ownerId: string; + channelId: string; +} + +export { + type IFetchMessage, + type IFetchMessageRequest, + type IFetchMessageResponse, + type ICreateMessageRequest, + type ICreateMessageResponse, + type IUpdateMessageRequest, + type IUpdateMessageResponse, + type IRemoveMessageResponse, + type IRemoveMessageRequest, +}; diff --git a/src/components/CommunityBar/CommunityBar.tsx b/src/components/CommunityBar/CommunityBar.tsx index 9af8b02..95e30cc 100644 --- a/src/components/CommunityBar/CommunityBar.tsx +++ b/src/components/CommunityBar/CommunityBar.tsx @@ -1,5 +1,6 @@ import type { Component } from "solid-js"; import { ICommunityBarProps } from "./types"; +import { SettingsIcon } from "../icons"; const CommunityBar: Component = ( props: ICommunityBarProps, @@ -8,9 +9,17 @@ const CommunityBar: Component = (
-
-

{props.name}

-

{props.description}

+
+
+

{props.name}

+

{props.description}

+
+
+ +
); diff --git a/src/components/CommunityBar/types.ts b/src/components/CommunityBar/types.ts index ab00ff9..b2d651f 100644 --- a/src/components/CommunityBar/types.ts +++ b/src/components/CommunityBar/types.ts @@ -3,6 +3,7 @@ interface ICommunityBarProps { name?: string; description?: string; avatar?: string; + onSettingsClick?: () => void; } export { type ICommunityBarProps }; diff --git a/src/components/CommunityModal/CommunityModal.tsx b/src/components/CommunityModal/CommunityModal.tsx index d5cb2b2..3a33f7a 100644 --- a/src/components/CommunityModal/CommunityModal.tsx +++ b/src/components/CommunityModal/CommunityModal.tsx @@ -5,14 +5,19 @@ import { CommunityActionTypes } from "../../store/community"; import { InviteActionTypes } from "../../store/invite"; const CommunityModal: Component = (props) => { - const [getCommunityName, setCommunityName] = createSignal(""); - const [getInviteId, setInviteId] = createSignal(""); + 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: getCommunityName(), + name: communityName, }, }); @@ -22,9 +27,14 @@ const CommunityModal: Component = (props) => { }; const onJoinCommunity = () => { + const inviteId = getInviteId(); + if (!inviteId || inviteId.trim().length < 1) { + return; + } + dispatch({ type: InviteActionTypes.ACCEPT_INVITE_START, - payload: getInviteId(), + payload: inviteId, }); setInviteId(""); @@ -32,6 +42,12 @@ const CommunityModal: Component = (props) => { props.onClose?.(); }; + const handleEnter = (e: KeyboardEvent, callback: () => void) => { + if (e.key === "Enter") { + callback(); + } + }; + const createCommunityHtml = () => ( <>

@@ -44,6 +60,7 @@ const CommunityModal: Component = (props) => { placeholder="Enter name of the new community" value={getCommunityName()} onInput={(e) => setCommunityName(e.currentTarget.value)} + onKeyDown={(e) => handleEnter(e, onCreateCommunity)} />

diff --git a/src/components/MessageBar/index.ts b/src/components/MessageBar/index.ts index 569e49b..f553fef 100644 --- a/src/components/MessageBar/index.ts +++ b/src/components/MessageBar/index.ts @@ -1 +1,2 @@ export * from "./MessageBar"; +export * from "./types"; diff --git a/src/components/MessageBar/types.ts b/src/components/MessageBar/types.ts new file mode 100644 index 0000000..7d38270 --- /dev/null +++ b/src/components/MessageBar/types.ts @@ -0,0 +1,7 @@ +interface IMessageBarProps { + text: string; + onChangeText?: (text: string) => void; + onSend?: () => void; +} + +export { type IMessageBarProps }; diff --git a/src/services/auth/auth.ts b/src/services/auth/auth.ts index 78b1265..c0d13f0 100644 --- a/src/services/auth/auth.ts +++ b/src/services/auth/auth.ts @@ -12,6 +12,10 @@ const fetchLogin = async (username: string, password: string) => { type: AuthActionTypes.FETCH_LOGIN_FINISH, payload: data, }); + + dispatch({ + type: AuthActionTypes.FETCH_REFRESH_START, + }); }; const fetchRefresh = async () => { @@ -21,6 +25,11 @@ const fetchRefresh = async () => { type: AuthActionTypes.FETCH_REFRESH_FINISH, payload: data, }); + + dispatch({ + type: AuthActionTypes.SET_LOGGED_IN, + payload: "id" in data, + }); }; export { fetchLogin, fetchRefresh }; diff --git a/src/services/channel/channel.ts b/src/services/channel/channel.ts index 773427d..0ac72dd 100644 --- a/src/services/channel/channel.ts +++ b/src/services/channel/channel.ts @@ -3,6 +3,7 @@ import { createChannelApi, updateChannelApi, removeChannelApi, + fetchChannelMessagesApi, } from "../../api/channel"; import { ChannelActionTypes } from "../../store/channel"; import { dispatch } from "../../store/state"; @@ -58,4 +59,21 @@ const removeChannel = async (id: string) => { }); }; -export { fetchChannel, createChannel, updateChannel, removeChannel }; +const fetchChannelMessages = async (id: string) => { + const data = await fetchChannelMessagesApi({ + id: id, + }); + + dispatch({ + type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_FINISH, + payload: data, + }); +}; + +export { + fetchChannel, + createChannel, + updateChannel, + removeChannel, + fetchChannelMessages, +}; diff --git a/src/services/message/index.ts b/src/services/message/index.ts new file mode 100644 index 0000000..9b2c485 --- /dev/null +++ b/src/services/message/index.ts @@ -0,0 +1 @@ +export * from "./message"; diff --git a/src/services/message/message.ts b/src/services/message/message.ts new file mode 100644 index 0000000..2f77422 --- /dev/null +++ b/src/services/message/message.ts @@ -0,0 +1,56 @@ +import { + fetchMessageApi, + createMessageApi, + updateMessageApi, + removeMessageApi, +} from "../../api/message"; +import { MessageActionTypes } from "../../store/message"; +import { dispatch } from "../../store/state"; + +const fetchMessage = async (id: string) => { + const data = await fetchMessageApi({ + id: id, + }); + + dispatch({ + type: MessageActionTypes.FETCH_MESSAGE_FINISH, + payload: data, + }); +}; + +const createMessage = async (text: string, channelId: string) => { + const data = await createMessageApi({ + text: text, + channelId: channelId, + }); + + dispatch({ + type: MessageActionTypes.CREATE_MESSAGE_FINISH, + payload: data, + }); +}; + +const updateMessage = async (id: string, text: string) => { + const data = await updateMessageApi({ + id: id, + text: text, + }); + + dispatch({ + type: MessageActionTypes.UPDATE_MESSAGE_FINISH, + payload: data, + }); +}; + +const removeMessage = async (id: string) => { + const data = await removeMessageApi({ + id: id, + }); + + dispatch({ + type: MessageActionTypes.REMOVE_MESSAGE_FINISH, + payload: data, + }); +}; + +export { fetchMessage, createMessage, updateMessage, removeMessage }; diff --git a/src/services/websocket/config.json b/src/services/websocket/config.json new file mode 100644 index 0000000..0b390ff --- /dev/null +++ b/src/services/websocket/config.json @@ -0,0 +1,6 @@ +{ + "schema": "ws", + "url": "localhost", + "port": 3012, + "path": "ws" +} diff --git a/src/services/websocket/index.ts b/src/services/websocket/index.ts new file mode 100644 index 0000000..771550d --- /dev/null +++ b/src/services/websocket/index.ts @@ -0,0 +1,2 @@ +export * from "./websocket"; +export * from "./types"; diff --git a/src/services/websocket/types.ts b/src/services/websocket/types.ts new file mode 100644 index 0000000..e79a5c1 --- /dev/null +++ b/src/services/websocket/types.ts @@ -0,0 +1,46 @@ +import { IFetchChannelMessage } from "../../api/channel"; + +enum SocketRequestTypes { + PING = "PING", +} + +enum SocketMessageTypes { + NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT", + NEW_MESSAGE = "NEW_MESSAGE", + NEW_CHANNEL = "NEW_CHANNEL", +} + +type SocketRequest = { + type: SocketRequestTypes.PING; +}; + +type SocketMessage = + | { + type: SocketMessageTypes.NEW_ANNOUNCEMENT; + payload: { + title: string; + description: string; + }; + } + | { + type: SocketMessageTypes.NEW_MESSAGE; + payload: { + channelId: string; + message: IFetchChannelMessage; + }; + } + | { + type: SocketMessageTypes.NEW_CHANNEL; + payload: { + id: string; + communityId: string; + name: string; + }; + }; + +export { + SocketRequestTypes, + SocketMessageTypes, + type SocketRequest, + type SocketMessage, +}; diff --git a/src/services/websocket/websocket.ts b/src/services/websocket/websocket.ts new file mode 100644 index 0000000..fca7251 --- /dev/null +++ b/src/services/websocket/websocket.ts @@ -0,0 +1,57 @@ +import { ChannelActionTypes } from "../../store/channel"; +import { dispatch } from "../../store/state"; +import config from "./config.json"; +import { SocketMessage, SocketMessageTypes } from "./types"; + +let connection: WebSocket; + +const connectWs = () => { + connection = new WebSocket( + `${config.schema}://${config.url}:${config.port}/${config.path}`, + ); + + connection.onclose = onCloseWs; + connection.onmessage = onMessageWs; +}; + +const onCloseWs = () => { + setTimeout(connectWs, 10000); +}; + +const onMessageWs = (event: MessageEvent) => { + const message = parseMessage(event.data); + + if (message) { + handleMessage(message); + } +}; + +const parseMessage = (data: string): SocketMessage | null => { + const request = JSON.parse(data) as SocketMessage; + + if (!request.type) { + return null; + } + + return request; +}; + +const handleMessage = (message: SocketMessage) => { + switch (message.type) { + case SocketMessageTypes.NEW_MESSAGE: { + dispatch({ + type: ChannelActionTypes.SET_CHANNEL_MESSAGE, + payload: { + id: message.payload.channelId, + message: message.payload.message, + }, + }); + } + case SocketMessageTypes.NEW_CHANNEL: { + } + case SocketMessageTypes.NEW_ANNOUNCEMENT: { + } + } +}; + +export { connection, connectWs }; diff --git a/src/store/actions.ts b/src/store/actions.ts index bd664d9..51769c9 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -6,6 +6,7 @@ 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 @@ -15,7 +16,8 @@ type ActionTypes = | ChannelActionTypes | RoleActionTypes | SessionActionTypes - | InviteActionTypes; + | InviteActionTypes + | MessageActionTypes; type Action = | AppAction @@ -25,6 +27,7 @@ type Action = | ChannelAction | RoleAction | SessionAction - | InviteAction; + | InviteAction + | MessageAction; export { type Action, type ActionTypes }; diff --git a/src/store/app/actions.ts b/src/store/app/actions.ts index 8c60c2d..9525b14 100644 --- a/src/store/app/actions.ts +++ b/src/store/app/actions.ts @@ -2,11 +2,16 @@ 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_OPEN; payload: boolean } + | { + type: AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN; + payload: boolean; + }; export { AppActionTypes, type AppAction }; diff --git a/src/store/app/app.ts b/src/store/app/app.ts index 9df5573..b9956ef 100644 --- a/src/store/app/app.ts +++ b/src/store/app/app.ts @@ -15,6 +15,15 @@ function appReducer(_state: IAppState, action: AppAction) { 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; } } diff --git a/src/store/app/types.ts b/src/store/app/types.ts index 8091315..a2de7f3 100644 --- a/src/store/app/types.ts +++ b/src/store/app/types.ts @@ -6,6 +6,7 @@ interface IAppState { interface IDialogsOpen { settingsOpen: boolean; addCommunityOpen: boolean; + communitySettingsOpen: boolean; } export { type IAppState, type IDialogsOpen }; diff --git a/src/store/auth/actions.ts b/src/store/auth/actions.ts index c4db806..9e0e4d6 100644 --- a/src/store/auth/actions.ts +++ b/src/store/auth/actions.ts @@ -1,10 +1,12 @@ import { IFetchLoginRequest, IFetchLoginResponse, - IFetchRefreshResponse, + 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", @@ -12,6 +14,7 @@ enum AuthActionTypes { } type AuthAction = + | { type: AuthActionTypes.SET_LOGGED_IN; payload: boolean } | { type: AuthActionTypes.FETCH_LOGIN_START; payload: IFetchLoginRequest } | { type: AuthActionTypes.FETCH_LOGIN_FINISH; @@ -20,7 +23,7 @@ type AuthAction = | { type: AuthActionTypes.FETCH_REFRESH_START } | { type: AuthActionTypes.FETCH_REFRESH_FINISH; - payload: IFetchRefreshResponse; + payload: IFetchRefreshResponseError | IFetchRefreshResponseSuccess; }; export { AuthActionTypes, type AuthAction }; diff --git a/src/store/auth/auth.ts b/src/store/auth/auth.ts index 569b4ed..14685cf 100644 --- a/src/store/auth/auth.ts +++ b/src/store/auth/auth.ts @@ -5,6 +5,9 @@ import { IAuthState } from "./types"; function authReducer(_state: IAuthState, action: AuthAction) { switch (action.type) { + case AuthActionTypes.SET_LOGGED_IN: + setState("auth", "loggedIn", action.payload); + break; case AuthActionTypes.FETCH_LOGIN_START: fetchLogin(action.payload.username, action.payload.password); break; @@ -15,7 +18,9 @@ function authReducer(_state: IAuthState, action: AuthAction) { fetchRefresh(); break; case AuthActionTypes.FETCH_REFRESH_FINISH: - setState("auth", "session", action.payload); + if ("id" in action.payload) { + setState("auth", "session", action.payload); + } break; } } diff --git a/src/store/auth/types.ts b/src/store/auth/types.ts index 21bd7a5..a57272f 100644 --- a/src/store/auth/types.ts +++ b/src/store/auth/types.ts @@ -1,4 +1,5 @@ interface IAuthState { + loggedIn?: boolean; session?: ISession; } diff --git a/src/store/channel/actions.ts b/src/store/channel/actions.ts index 6c29458..c6b97cb 100644 --- a/src/store/channel/actions.ts +++ b/src/store/channel/actions.ts @@ -5,12 +5,16 @@ import { 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", @@ -19,6 +23,8 @@ enum ChannelActionTypes { 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 = @@ -30,6 +36,14 @@ type ChannelAction = 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; @@ -55,6 +69,11 @@ type ChannelAction = | { 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 }; diff --git a/src/store/channel/channel.ts b/src/store/channel/channel.ts index 704f5ed..3205b6e 100644 --- a/src/store/channel/channel.ts +++ b/src/store/channel/channel.ts @@ -3,12 +3,13 @@ import { createChannel, updateChannel, removeChannel, + fetchChannelMessages, } from "../../services/channel"; import { setState } from "../state"; import { ChannelActionTypes, ChannelAction } from "./actions"; import { IChannelState } from "./types"; -function channelReducer(state: IChannelState, action: ChannelAction) { +function channelReducer(_state: IChannelState, action: ChannelAction) { switch (action.type) { case ChannelActionTypes.SET_CHANNEL: setState("channel", "channels", action.payload.id, action.payload); @@ -16,6 +17,25 @@ function channelReducer(state: IChannelState, action: ChannelAction) { case ChannelActionTypes.SET_ACTIVE_CHANNEL: setState("channel", "active", action.payload); break; + case ChannelActionTypes.SET_TEXT: + setState( + "channel", + "channels", + action.payload.id, + "text", + action.payload.text, + ); + break; + case ChannelActionTypes.SET_CHANNEL_MESSAGE: + setState( + "channel", + "channels", + action.payload.id, + "messages", + action.payload.message.id, + action.payload.message, + ); + break; case ChannelActionTypes.FETCH_CHANNEL_START: fetchChannel(action.payload); break; @@ -48,6 +68,25 @@ function channelReducer(state: IChannelState, action: ChannelAction) { 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( + action.payload.messages.map((item) => [item.id, item]), + ); + + const copy = { ...messages, ...newMessages }; + return copy; + }, + ); + break; } } diff --git a/src/store/channel/types.ts b/src/store/channel/types.ts index 1b23130..fc2ff58 100644 --- a/src/store/channel/types.ts +++ b/src/store/channel/types.ts @@ -1,3 +1,5 @@ +import { IMessage } from "../message"; + interface IChannelState { active?: string; channels: Record; @@ -9,6 +11,8 @@ interface IChannel { description?: string; communityId?: string; creationDate?: number; + text?: string; + messages?: Record; } export { type IChannelState, type IChannel }; diff --git a/src/store/community/community.ts b/src/store/community/community.ts index 8a67c0f..fbcde23 100644 --- a/src/store/community/community.ts +++ b/src/store/community/community.ts @@ -12,7 +12,7 @@ import { setState } from "../state"; import { CommunityActionTypes, CommunityAction } from "./actions"; import { ICommunityState } from "./types"; -function communityReducer(state: ICommunityState, action: CommunityAction) { +function communityReducer(_state: ICommunityState, action: CommunityAction) { switch (action.type) { case CommunityActionTypes.SET_COMMUNITY: setState( diff --git a/src/store/invite/invite.ts b/src/store/invite/invite.ts index 1123afd..5fdf5be 100644 --- a/src/store/invite/invite.ts +++ b/src/store/invite/invite.ts @@ -3,7 +3,7 @@ import { setState } from "../state"; import { InviteActionTypes, InviteAction } from "./actions"; import { IInviteState } from "./types"; -function inviteReducer(state: IInviteState, action: InviteAction) { +function inviteReducer(_state: IInviteState, action: InviteAction) { switch (action.type) { case InviteActionTypes.SET_INVITE: setState("invite", "invites", action.payload.id, action.payload); diff --git a/src/store/message/actions.ts b/src/store/message/actions.ts new file mode 100644 index 0000000..23e152e --- /dev/null +++ b/src/store/message/actions.ts @@ -0,0 +1,49 @@ +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 }; diff --git a/src/store/message/index.ts b/src/store/message/index.ts new file mode 100644 index 0000000..a6332db --- /dev/null +++ b/src/store/message/index.ts @@ -0,0 +1,3 @@ +export * from "./message"; +export * from "./actions"; +export * from "./types"; diff --git a/src/store/message/message.ts b/src/store/message/message.ts new file mode 100644 index 0000000..5166923 --- /dev/null +++ b/src/store/message/message.ts @@ -0,0 +1,59 @@ +import { + fetchMessage, + createMessage, + updateMessage, + removeMessage, +} from "../../services/message"; +import { setState } from "../state"; +import { MessageActionTypes, MessageAction } from "./actions"; +import { IMessageState } from "./types"; + +function messageReducer(_state: IMessageState, action: MessageAction) { + switch (action.type) { + case MessageActionTypes.FETCH_MESSAGE_START: + fetchMessage(action.payload); + break; + case MessageActionTypes.FETCH_MESSAGE_FINISH: + setState("message", "message", action.payload); + setState( + "channel", + "channels", + action.payload.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", + action.payload.id, + action.payload, + ); + 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 }; diff --git a/src/store/message/types.ts b/src/store/message/types.ts new file mode 100644 index 0000000..825a759 --- /dev/null +++ b/src/store/message/types.ts @@ -0,0 +1,15 @@ +interface IMessageState { + message?: IMessage; +} + +interface IMessage { + id: string; + text: string; + editHistory?: string[]; + edited: boolean; + ownerId: string; + channelId?: string; + creationDate: number; +} + +export { type IMessageState, type IMessage }; diff --git a/src/store/reducers.ts b/src/store/reducers.ts index ca6376c..3c3a8c8 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -8,6 +8,7 @@ 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); @@ -18,6 +19,7 @@ function reducer(state: IState, action: Action) { 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 }; diff --git a/src/store/role/actions.ts b/src/store/role/actions.ts index 6376028..f33d464 100644 --- a/src/store/role/actions.ts +++ b/src/store/role/actions.ts @@ -1,6 +1,7 @@ import { IFetchCommunityRole } from "../../api/community"; import { ICreateRoleRequest, + IUpdateRoleRequest, IFetchRoleResponse, ICreateRoleResponse, IUpdateRoleResponse, @@ -34,7 +35,7 @@ type RoleAction = type: RoleActionTypes.CREATE_ROLE_FINISH; payload: ICreateRoleResponse; } - | { type: RoleActionTypes.UPDATE_ROLE_START; payload: string } + | { type: RoleActionTypes.UPDATE_ROLE_START; payload: IUpdateRoleRequest } | { type: RoleActionTypes.UPDATE_ROLE_FINISH; payload: IUpdateRoleResponse; diff --git a/src/store/role/role.ts b/src/store/role/role.ts index 7428257..383eb2c 100644 --- a/src/store/role/role.ts +++ b/src/store/role/role.ts @@ -8,7 +8,7 @@ import { setState } from "../state"; import { RoleActionTypes, RoleAction } from "./actions"; import { IRoleState } from "./types"; -function roleReducer(state: IRoleState, action: RoleAction) { +function roleReducer(_state: IRoleState, action: RoleAction) { switch (action.type) { case RoleActionTypes.SET_ROLE: setState("role", "roles", action.payload.id, action.payload); @@ -26,7 +26,7 @@ function roleReducer(state: IRoleState, action: RoleAction) { setState("role", "roles", action.payload.id, action.payload); break; case RoleActionTypes.UPDATE_ROLE_START: - updateRole(action.payload); + updateRole(action.payload.id, action.payload.name); break; case RoleActionTypes.UPDATE_ROLE_FINISH: setState("role", "roles", action.payload.id, action.payload); diff --git a/src/store/session/session.ts b/src/store/session/session.ts index a3133d2..f99d218 100644 --- a/src/store/session/session.ts +++ b/src/store/session/session.ts @@ -3,7 +3,7 @@ import { setState } from "../state"; import { SessionActionTypes, SessionAction } from "./actions"; import { ISessionState } from "./types"; -function sessionReducer(state: ISessionState, action: SessionAction) { +function sessionReducer(_state: ISessionState, action: SessionAction) { switch (action.type) { case SessionActionTypes.SET_SESSION: setState("session", "sessions", action.payload.id, action.payload); diff --git a/src/store/state.ts b/src/store/state.ts index 4710d2f..536fa65 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -9,9 +9,11 @@ const [state, setState] = createStore({ dialogsOpen: { settingsOpen: false, addCommunityOpen: false, + communitySettingsOpen: false, }, }, auth: { + loggedIn: undefined, session: undefined, }, user: { @@ -33,6 +35,9 @@ const [state, setState] = createStore({ invite: { invites: {}, }, + message: { + message: undefined, + }, }); function dispatch(action: Action) { diff --git a/src/store/types.ts b/src/store/types.ts index e0e7ced..fe85706 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -6,6 +6,7 @@ import { IChannelState } from "./channel"; import { IRoleState } from "./role"; import { ISessionState } from "./session"; import { IInviteState } from "./invite"; +import { IMessageState } from "./message"; interface IState { app: IAppState; @@ -16,6 +17,7 @@ interface IState { role: IRoleState; session: ISessionState; invite: IInviteState; + message: IMessageState; } export { type IState }; diff --git a/src/store/user/user.ts b/src/store/user/user.ts index c8511b0..884e576 100644 --- a/src/store/user/user.ts +++ b/src/store/user/user.ts @@ -8,7 +8,7 @@ import { setState } from "../state"; import { UserActionTypes, UserAction } from "./actions"; import { IUserState } from "./types"; -function userReducer(state: IUserState, action: UserAction) { +function userReducer(_state: IUserState, action: UserAction) { switch (action.type) { case UserActionTypes.SET_USER: setState("user", "users", action.payload.id, action.payload); diff --git a/src/views/MainView/MainView.tsx b/src/views/AppView/AppView.tsx similarity index 80% rename from src/views/MainView/MainView.tsx rename to src/views/AppView/AppView.tsx index 0b609d8..27a44b7 100644 --- a/src/views/MainView/MainView.tsx +++ b/src/views/AppView/AppView.tsx @@ -8,8 +8,12 @@ import { AuthActionTypes } from "../../store/auth"; import { HomeView } from "../HomeView"; import { CommunityView } from "../CommunityView"; import { ModalView } from "../ModalView"; +import { useNavigate } from "@solidjs/router"; +import { connectWs } from "../../services/websocket"; + +const AppView: Component = () => { + const navigate = useNavigate(); -const MainView: Component = () => { onMount(() => { dispatch({ type: AuthActionTypes.FETCH_REFRESH_START, @@ -31,6 +35,14 @@ const MainView: Component = () => { } }); + createEffect(() => { + if (state.auth.loggedIn === false) { + navigate("/login"); + } else { + connectWs(); + } + }); + return (
@@ -50,4 +62,4 @@ const MainView: Component = () => { ); }; -export { MainView }; +export { AppView }; diff --git a/src/views/AppView/index.ts b/src/views/AppView/index.ts new file mode 100644 index 0000000..93fb631 --- /dev/null +++ b/src/views/AppView/index.ts @@ -0,0 +1 @@ +export * from "./AppView"; diff --git a/src/views/ChannelView/ChannelView.tsx b/src/views/ChannelView/ChannelView.tsx index edf21ab..694c034 100644 --- a/src/views/ChannelView/ChannelView.tsx +++ b/src/views/ChannelView/ChannelView.tsx @@ -4,6 +4,7 @@ import { CommunityActionTypes, ICommunity } from "../../store/community"; import { ChannelActionTypes } from "../../store/channel"; import { Channel } from "../../components/Channel"; import { CommunityBar } from "../../components/CommunityBar"; +import { AppActionTypes } from "../../store/app"; const ChannelView: Component = () => { const channelIds = createMemo(() => { @@ -34,6 +35,13 @@ const ChannelView: Component = () => { } }); + const onCommunitySettingsClick = () => { + dispatch({ + type: AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN, + payload: true, + }); + }; + const onChannelClick = (id: string) => { dispatch({ type: ChannelActionTypes.FETCH_CHANNEL_START, @@ -70,6 +78,7 @@ const ChannelView: Component = () => { avatar={ "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp" } + onSettingsClick={onCommunitySettingsClick} />
    {channelIds().map(mapChannel)} diff --git a/src/views/ChatView/ChatView.tsx b/src/views/ChatView/ChatView.tsx index 2954fb7..4a0774a 100644 --- a/src/views/ChatView/ChatView.tsx +++ b/src/views/ChatView/ChatView.tsx @@ -1,189 +1,77 @@ -import { createMemo, onMount, type Component } from "solid-js"; +import { createEffect, createMemo, onMount, type Component } from "solid-js"; import { ChannelBar } from "../../components/ChannelBar"; import { MessageBar } from "../../components/MessageBar"; import { Message } from "../../components/Message"; import { dispatch, state } from "../../store/state"; import { UserActionTypes } from "../../store/user"; -import { IChannel } from "../../store/channel"; +import { ChannelActionTypes, IChannel } from "../../store/channel"; +import { MessageActionTypes } from "../../store/message"; const ChatView: Component = () => { - const testMessages = [ - { - messageId: "432432", - message: "Hello this is a test message", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432433", - message: "Hi hi", - userId: "12122", - username: "TestUser", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - { - messageId: "432434", - message: "Nooooo", - userId: "12121", - username: "Aslan", - avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", - }, - ]; + let scrollRef: HTMLUListElement | undefined; + let lastScrollTop = 0; + let autoScroll = true; + + const scrollToBottom = () => { + if (!scrollRef || !autoScroll) { + return; + } + + scrollRef.scrollTop = scrollRef.scrollHeight; + }; + + const handleScroll = () => { + if (!scrollRef) { + return; + } + + if (scrollRef.scrollTop < lastScrollTop) { + autoScroll = false; + } + if ( + scrollRef.scrollTop + scrollRef.clientHeight >= + scrollRef.scrollHeight + ) { + autoScroll = true; + } + + lastScrollTop = scrollRef.scrollTop; + }; const channelInfo = createMemo(() => { const activeChannelId = state.channel.active; - return state.channel.channels[activeChannelId ?? 0]; + if (!activeChannelId) { + return undefined; + } + + autoScroll = true; + + dispatch({ + type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_START, + payload: activeChannelId, + }); + return state.channel.channels[activeChannelId]; + }); + + const messages = createMemo(() => { + const channelId = channelInfo()?.id; + if (!channelId) { + return []; + } + + const channel = state.channel.channels[channelId]; + if (!channel) { + return []; + } + + Object.keys(channel.messages ?? []).length; + + setTimeout(scrollToBottom, 20); + setTimeout(scrollToBottom, 100); + + return channel.messages ?? []; }); - let scrollRef: HTMLUListElement | undefined; onMount(() => { if (scrollRef) { scrollRef.scrollTop = scrollRef.scrollHeight; @@ -197,6 +85,50 @@ const ChatView: Component = () => { }); }; + const onChangeMessageText = (text: string) => { + const channel = channelInfo(); + if (!channel?.id) { + return; + } + + dispatch({ + type: ChannelActionTypes.SET_TEXT, + payload: { + id: channel.id, + text: text, + }, + }); + }; + + const onMessageSend = () => { + autoScroll = true; + + const channel = channelInfo(); + if (!channel?.id) { + return; + } + + const text = channel.text; + if (!text || text.trim().length < 1) { + return; + } + + dispatch({ + type: ChannelActionTypes.SET_TEXT, + payload: { + id: channel.id, + text: "", + }, + }); + dispatch({ + type: MessageActionTypes.CREATE_MESSAGE_START, + payload: { + channelId: channel.id, + text: text, + }, + }); + }; + return (
    @@ -207,20 +139,29 @@ const ChatView: Component = () => { />
      - {testMessages.map((msg) => ( + {Object.entries(messages()).map(([_key, message]) => ( ))}
    - +
    ); diff --git a/src/views/LoginView/LoginView.tsx b/src/views/LoginView/LoginView.tsx index 007e88f..60905a6 100644 --- a/src/views/LoginView/LoginView.tsx +++ b/src/views/LoginView/LoginView.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, type Component } from "solid-js"; +import { createEffect, createSignal, onMount, type Component } from "solid-js"; import { dispatch, state } from "../../store/state"; import { AuthActionTypes } from "../../store/auth"; import { useNavigate } from "@solidjs/router"; @@ -9,6 +9,12 @@ const LoginView: Component = () => { const navigate = useNavigate(); + onMount(() => { + dispatch({ + type: AuthActionTypes.FETCH_REFRESH_START, + }); + }); + const onLogin = () => { dispatch({ type: AuthActionTypes.FETCH_LOGIN_START, @@ -20,8 +26,8 @@ const LoginView: Component = () => { }; createEffect(() => { - if (state.auth.session?.id) { - navigate("/"); + if (state.auth.loggedIn) { + navigate("/app"); } }); diff --git a/src/views/MainView/index.ts b/src/views/MainView/index.ts deleted file mode 100644 index ceb86d7..0000000 --- a/src/views/MainView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MainView"; diff --git a/src/views/ModalView/ModalView.tsx b/src/views/ModalView/ModalView.tsx index 241bd10..94eeb6a 100644 --- a/src/views/ModalView/ModalView.tsx +++ b/src/views/ModalView/ModalView.tsx @@ -2,11 +2,13 @@ import { createEffect, type Component } from "solid-js"; import { dispatch, state } from "../../store/state"; import SettingsModal from "../../components/SettingsModal/SettingsModal"; import CommunityModal from "../../components/CommunityModal/CommunityModal"; +import CommunitySettingsModal from "../../components/CommunitySettingsModal/CommunitySettingsModal"; import { AppActionTypes } from "../../store/app"; const ModalView: Component = () => { let settingsModal: HTMLDialogElement; let communityModal: HTMLDialogElement; + let communitySettingsModal: HTMLDialogElement; createEffect(() => { if (state.app.dialogsOpen.settingsOpen) { @@ -24,6 +26,14 @@ const ModalView: Component = () => { } }); + createEffect(() => { + if (state.app.dialogsOpen.communitySettingsOpen) { + communitySettingsModal.showModal(); + } else { + communitySettingsModal.close(); + } + }); + const onCloseSettings = () => { dispatch({ type: AppActionTypes.SET_SETTINGS_OPEN, @@ -38,6 +48,13 @@ const ModalView: Component = () => { }); }; + const onCloseCommunitySettings = () => { + dispatch({ + type: AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN, + payload: false, + }); + }; + return ( <> { dialogRef={(element) => (communityModal = element)} onClose={onCloseCommunity} /> + (communitySettingsModal = element)} + onClose={onCloseCommunitySettings} + /> ); }; diff --git a/src/views/RegisterView/RegisterView.tsx b/src/views/RegisterView/RegisterView.tsx index ddba295..1ec4bd3 100644 --- a/src/views/RegisterView/RegisterView.tsx +++ b/src/views/RegisterView/RegisterView.tsx @@ -1,6 +1,27 @@ -import type { Component } from "solid-js"; +import { useNavigate } from "@solidjs/router"; +import { createSignal, createEffect, type Component, onMount } from "solid-js"; +import { dispatch, state } from "../../store/state"; +import { AuthActionTypes } from "../../store/auth"; const RegisterView: Component = () => { + const [getUsername, setUsername] = createSignal(""); + const [getEmail, setEmail] = createSignal(""); + const [getPassword, setPassword] = createSignal(""); + + const navigate = useNavigate(); + + onMount(() => { + dispatch({ + type: AuthActionTypes.FETCH_REFRESH_START, + }); + }); + + createEffect(() => { + if (state.auth.loggedIn) { + navigate("/app"); + } + }); + return
    ; }; diff --git a/src/views/SettingsView/SettingsView.tsx b/src/views/SettingsView/SettingsView.tsx deleted file mode 100644 index 2cbe33f..0000000 --- a/src/views/SettingsView/SettingsView.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import type { Component } from "solid-js"; - -const SettingsView: Component = () => { - return
    ; -}; - -export { SettingsView }; diff --git a/src/views/SettingsView/index.ts b/src/views/SettingsView/index.ts deleted file mode 100644 index 0f6ef7b..0000000 --- a/src/views/SettingsView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SettingsView"; diff --git a/src/views/StartView/StartView.tsx b/src/views/StartView/StartView.tsx new file mode 100644 index 0000000..7aed013 --- /dev/null +++ b/src/views/StartView/StartView.tsx @@ -0,0 +1,7 @@ +import { type Component } from "solid-js"; + +const StartView: Component = () => { + return
    ; +}; + +export { StartView }; diff --git a/src/views/StartView/index.ts b/src/views/StartView/index.ts new file mode 100644 index 0000000..80a403f --- /dev/null +++ b/src/views/StartView/index.ts @@ -0,0 +1 @@ +export * from "./StartView";