Add real time messaging

This commit is contained in:
Aslan 2026-01-11 14:17:34 -05:00
parent 0163eab540
commit 9153ba841d
61 changed files with 882 additions and 230 deletions

View file

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

View file

@ -1,15 +1,17 @@
import { Route, Router } from "@solidjs/router"; import { Route, Router } from "@solidjs/router";
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { MainView } from "./views/MainView"; import { StartView } from "./views/StartView";
import { SettingsView } from "./views/SettingsView"; import { AppView } from "./views/AppView";
import { LoginView } from "./views/LoginView"; import { LoginView } from "./views/LoginView";
import { RegisterView } from "./views/RegisterView";
const App: Component = () => { const App: Component = () => {
return ( return (
<Router> <Router>
<Route path="/" component={MainView} /> <Route path="/" component={StartView} />
<Route path="/settings" component={SettingsView} /> <Route path="/app" component={AppView} />
<Route path="/login" component={LoginView} /> <Route path="/login" component={LoginView} />
<Route path="/register" component={RegisterView} />
</Router> </Router>
); );
}; };

View file

@ -2,7 +2,8 @@ import { callApi, HTTP } from "../tools";
import { import {
IFetchLoginRequest, IFetchLoginRequest,
IFetchLoginResponse, IFetchLoginResponse,
IFetchRefreshResponse, IFetchRefreshResponseError,
IFetchRefreshResponseSuccess,
} from "./types"; } from "./types";
const fetchLoginApi = async ( const fetchLoginApi = async (
@ -11,7 +12,9 @@ const fetchLoginApi = async (
return await callApi(HTTP.POST, `auth/login`, request, true); return await callApi(HTTP.POST, `auth/login`, request, true);
}; };
const fetchRefreshApi = async (): Promise<IFetchRefreshResponse> => { const fetchRefreshApi = async (): Promise<
IFetchRefreshResponseError | IFetchRefreshResponseSuccess
> => {
return await callApi(HTTP.GET, `auth/refresh`, undefined, true); return await callApi(HTTP.GET, `auth/refresh`, undefined, true);
}; };

View file

@ -8,7 +8,11 @@ interface IFetchLoginResponse {
ownerId: string; ownerId: string;
} }
interface IFetchRefreshResponse { interface IFetchRefreshResponseError {
error: string;
}
interface IFetchRefreshResponseSuccess {
id: string; id: string;
ownerId: string; ownerId: string;
token: string; token: string;
@ -17,5 +21,6 @@ interface IFetchRefreshResponse {
export { export {
type IFetchLoginRequest, type IFetchLoginRequest,
type IFetchLoginResponse, type IFetchLoginResponse,
type IFetchRefreshResponse, type IFetchRefreshResponseError,
type IFetchRefreshResponseSuccess,
}; };

View file

@ -8,6 +8,8 @@ import {
IUpdateChannelResponse, IUpdateChannelResponse,
IRemoveChannelRequest, IRemoveChannelRequest,
IRemoveChannelResponse, IRemoveChannelResponse,
IFetchChannelMessagesRequest,
IFetchChannelMessagesResponse,
} from "./types"; } from "./types";
const fetchChannelApi = async ( const fetchChannelApi = async (
@ -34,9 +36,16 @@ const removeChannelApi = async (
return await callApi(HTTP.DELETE, `channel/${request.id}`); return await callApi(HTTP.DELETE, `channel/${request.id}`);
}; };
const fetchChannelMessagesApi = async (
request: IFetchChannelMessagesRequest,
): Promise<IFetchChannelMessagesResponse> => {
return await callApi(HTTP.GET, `channel/${request.id}/messages`);
};
export { export {
fetchChannelApi, fetchChannelApi,
createChannelApi, createChannelApi,
updateChannelApi, updateChannelApi,
removeChannelApi, removeChannelApi,
fetchChannelMessagesApi,
}; };

View file

@ -36,6 +36,23 @@ interface IRemoveChannelResponse {
communityId: string; 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 { export {
type IFetchChannel, type IFetchChannel,
type IFetchChannelRequest, type IFetchChannelRequest,
@ -46,4 +63,7 @@ export {
type IUpdateChannelResponse, type IUpdateChannelResponse,
type IRemoveChannelResponse, type IRemoveChannelResponse,
type IRemoveChannelRequest, type IRemoveChannelRequest,
type IFetchChannelMessagesRequest,
type IFetchChannelMessagesResponse,
type IFetchChannelMessage,
}; };

2
src/api/message/index.ts Normal file
View file

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

View file

@ -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<IFetchMessageResponse> => {
return await callApi(HTTP.GET, `message/${request.id}`);
};
const createMessageApi = async (
request: ICreateMessageRequest,
): Promise<ICreateMessageResponse> => {
return await callApi(HTTP.POST, `message`, request);
};
const updateMessageApi = async (
request: IUpdateMessageRequest,
): Promise<IUpdateMessageResponse> => {
return await callApi(HTTP.PATCH, `message/${request.id}`, request);
};
const removeMessageApi = async (
request: IRemoveMessageRequest,
): Promise<IRemoveMessageResponse> => {
return await callApi(HTTP.DELETE, `message/${request.id}`);
};
export {
fetchMessageApi,
createMessageApi,
updateMessageApi,
removeMessageApi,
};

51
src/api/message/types.ts Normal file
View file

@ -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,
};

View file

@ -1,5 +1,6 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { ICommunityBarProps } from "./types"; import { ICommunityBarProps } from "./types";
import { SettingsIcon } from "../icons";
const CommunityBar: Component<ICommunityBarProps> = ( const CommunityBar: Component<ICommunityBarProps> = (
props: ICommunityBarProps, props: ICommunityBarProps,
@ -8,9 +9,17 @@ const CommunityBar: Component<ICommunityBarProps> = (
<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}')]`}
> >
<div class="flex flex-col justify-center bg-stone-800/25 backdrop-blur-md h-16 w-full shadow-bar px-5"> <div class="flex flex-row justify-between items-center bg-stone-800/25 backdrop-blur-md h-16 shadow-bar pl-5 pr-3">
<h2 class="text-sm font-bold">{props.name}</h2> <div class="flex flex-col justify-center">
<p class="text-xs">{props.description}</p> <h2 class="text-sm font-bold">{props.name}</h2>
<p class="text-xs">{props.description}</p>
</div>
<div
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

@ -3,6 +3,7 @@ interface ICommunityBarProps {
name?: string; name?: string;
description?: string; description?: string;
avatar?: string; avatar?: string;
onSettingsClick?: () => void;
} }
export { type ICommunityBarProps }; export { type ICommunityBarProps };

View file

@ -5,14 +5,19 @@ import { CommunityActionTypes } from "../../store/community";
import { InviteActionTypes } from "../../store/invite"; import { InviteActionTypes } from "../../store/invite";
const CommunityModal: Component<ICommunityModalProps> = (props) => { const CommunityModal: Component<ICommunityModalProps> = (props) => {
const [getCommunityName, setCommunityName] = createSignal<string>(""); const [getCommunityName, setCommunityName] = createSignal("");
const [getInviteId, setInviteId] = createSignal<string>(""); const [getInviteId, setInviteId] = createSignal("");
const onCreateCommunity = () => { const onCreateCommunity = () => {
const communityName = getCommunityName();
if (!communityName || communityName.trim().length < 1) {
return;
}
dispatch({ dispatch({
type: CommunityActionTypes.CREATE_COMMUNITY_START, type: CommunityActionTypes.CREATE_COMMUNITY_START,
payload: { payload: {
name: getCommunityName(), name: communityName,
}, },
}); });
@ -22,9 +27,14 @@ const CommunityModal: Component<ICommunityModalProps> = (props) => {
}; };
const onJoinCommunity = () => { const onJoinCommunity = () => {
const inviteId = getInviteId();
if (!inviteId || inviteId.trim().length < 1) {
return;
}
dispatch({ dispatch({
type: InviteActionTypes.ACCEPT_INVITE_START, type: InviteActionTypes.ACCEPT_INVITE_START,
payload: getInviteId(), payload: inviteId,
}); });
setInviteId(""); setInviteId("");
@ -32,6 +42,12 @@ const CommunityModal: Component<ICommunityModalProps> = (props) => {
props.onClose?.(); props.onClose?.();
}; };
const handleEnter = (e: KeyboardEvent, callback: () => void) => {
if (e.key === "Enter") {
callback();
}
};
const createCommunityHtml = () => ( const createCommunityHtml = () => (
<> <>
<h3 class="text-lg font-bold text-center mb-6"> <h3 class="text-lg font-bold text-center mb-6">
@ -44,6 +60,7 @@ const CommunityModal: Component<ICommunityModalProps> = (props) => {
placeholder="Enter name of the new community" placeholder="Enter name of the new community"
value={getCommunityName()} value={getCommunityName()}
onInput={(e) => setCommunityName(e.currentTarget.value)} onInput={(e) => setCommunityName(e.currentTarget.value)}
onKeyDown={(e) => handleEnter(e, onCreateCommunity)}
/> />
</label> </label>
<button <button
@ -68,6 +85,7 @@ const CommunityModal: Component<ICommunityModalProps> = (props) => {
placeholder="Enter invite ID" placeholder="Enter invite ID"
value={getInviteId()} value={getInviteId()}
onInput={(e) => setInviteId(e.currentTarget.value)} onInput={(e) => setInviteId(e.currentTarget.value)}
onKeyDown={(e) => handleEnter(e, onJoinCommunity)}
/> />
</label> </label>
<button <button

View file

@ -0,0 +1,26 @@
import type { Component } from "solid-js";
import { ICommunitySettingsModalProps } from "./types";
const CommunitySettingsModal: Component<ICommunitySettingsModalProps> = (
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">
Community 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 CommunitySettingsModal;

View file

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

View file

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

View file

@ -1,13 +1,31 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { IMessageBarProps } from "./types";
const MessageBar: Component<IMessageBarProps> = (props: IMessageBarProps) => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === "Enter") {
props.onSend?.();
}
};
const MessageBar: Component = () => {
return ( return (
<div class="absolute w-full bottom-0 p-4 z-10"> <div class="absolute w-full bottom-0 p-4 z-10">
<div class="bg-stone-800/25 backdrop-blur-lg h-16 shadow-bar p-2 flex flex-row gap-2 rounded-full"> <div class="bg-stone-800/25 backdrop-blur-lg h-16 shadow-bar p-2 flex flex-row gap-2 rounded-full">
<label class="bg-stone-800/50 backdrop-blur-lg input w-full h-full rounded-full focus:border-none outline-none"> <label class="bg-stone-800/50 backdrop-blur-lg input w-full h-full rounded-full focus:border-none outline-none">
<input type="text" placeholder="Send a message..." /> <input
type="text"
placeholder="Send a message..."
value={props.text}
onInput={(e) =>
props.onChangeText?.(e.currentTarget.value)
}
onKeyDown={handleEnter}
/>
</label> </label>
<button class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral h-full rounded-full"> <button
class="bg-stone-950/50 backdrop-blur-lg btn btn-neutral h-full rounded-full"
onClick={props.onSend}
>
Send Send
</button> </button>
</div> </div>

View file

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

View file

@ -0,0 +1,7 @@
interface IMessageBarProps {
text: string;
onChangeText?: (text: string) => void;
onSend?: () => void;
}
export { type IMessageBarProps };

View file

@ -12,6 +12,10 @@ const fetchLogin = async (username: string, password: string) => {
type: AuthActionTypes.FETCH_LOGIN_FINISH, type: AuthActionTypes.FETCH_LOGIN_FINISH,
payload: data, payload: data,
}); });
dispatch({
type: AuthActionTypes.FETCH_REFRESH_START,
});
}; };
const fetchRefresh = async () => { const fetchRefresh = async () => {
@ -21,6 +25,11 @@ const fetchRefresh = async () => {
type: AuthActionTypes.FETCH_REFRESH_FINISH, type: AuthActionTypes.FETCH_REFRESH_FINISH,
payload: data, payload: data,
}); });
dispatch({
type: AuthActionTypes.SET_LOGGED_IN,
payload: "id" in data,
});
}; };
export { fetchLogin, fetchRefresh }; export { fetchLogin, fetchRefresh };

View file

@ -3,6 +3,7 @@ import {
createChannelApi, createChannelApi,
updateChannelApi, updateChannelApi,
removeChannelApi, removeChannelApi,
fetchChannelMessagesApi,
} from "../../api/channel"; } from "../../api/channel";
import { ChannelActionTypes } from "../../store/channel"; import { ChannelActionTypes } from "../../store/channel";
import { dispatch } from "../../store/state"; 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,
};

View file

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

View file

@ -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 };

View file

@ -0,0 +1,6 @@
{
"schema": "ws",
"url": "localhost",
"port": 3012,
"path": "ws"
}

View file

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

View file

@ -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,
};

View file

@ -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 };

View file

@ -6,6 +6,7 @@ import { ChannelActionTypes, ChannelAction } from "./channel";
import { RoleActionTypes, RoleAction } from "./role"; import { RoleActionTypes, RoleAction } from "./role";
import { SessionActionTypes, SessionAction } from "./session"; import { SessionActionTypes, SessionAction } from "./session";
import { InviteActionTypes, InviteAction } from "./invite"; import { InviteActionTypes, InviteAction } from "./invite";
import { MessageActionTypes, MessageAction } from "./message";
type ActionTypes = type ActionTypes =
| AppActionTypes | AppActionTypes
@ -15,7 +16,8 @@ type ActionTypes =
| ChannelActionTypes | ChannelActionTypes
| RoleActionTypes | RoleActionTypes
| SessionActionTypes | SessionActionTypes
| InviteActionTypes; | InviteActionTypes
| MessageActionTypes;
type Action = type Action =
| AppAction | AppAction
@ -25,6 +27,7 @@ type Action =
| ChannelAction | ChannelAction
| RoleAction | RoleAction
| SessionAction | SessionAction
| InviteAction; | InviteAction
| MessageAction;
export { type Action, type ActionTypes }; export { type Action, type ActionTypes };

View file

@ -2,11 +2,16 @@ enum AppActionTypes {
SET_HOME_OPEN = "SET_HOME_OPEN", SET_HOME_OPEN = "SET_HOME_OPEN",
SET_SETTINGS_OPEN = "SET_SETTINGS_OPEN", SET_SETTINGS_OPEN = "SET_SETTINGS_OPEN",
SET_ADD_COMMUNITY_OPEN = "SET_ADD_COMMUNITY_OPEN", SET_ADD_COMMUNITY_OPEN = "SET_ADD_COMMUNITY_OPEN",
SET_ADD_COMMUNITY_SETTINGS_OPEN = "SET_ADD_COMMUNITY_SETTINGS_OPEN",
} }
type AppAction = type AppAction =
| { type: AppActionTypes.SET_HOME_OPEN; payload: boolean } | { type: AppActionTypes.SET_HOME_OPEN; payload: boolean }
| { type: AppActionTypes.SET_SETTINGS_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 }; export { AppActionTypes, type AppAction };

View file

@ -15,6 +15,15 @@ function appReducer(_state: IAppState, action: AppAction) {
setState("app", "dialogsOpen", "addCommunityOpen", false); setState("app", "dialogsOpen", "addCommunityOpen", false);
setState("app", "dialogsOpen", "addCommunityOpen", action.payload); setState("app", "dialogsOpen", "addCommunityOpen", action.payload);
break; break;
case AppActionTypes.SET_ADD_COMMUNITY_SETTINGS_OPEN:
setState("app", "dialogsOpen", "communitySettingsOpen", false);
setState(
"app",
"dialogsOpen",
"communitySettingsOpen",
action.payload,
);
break;
} }
} }

View file

@ -6,6 +6,7 @@ interface IAppState {
interface IDialogsOpen { interface IDialogsOpen {
settingsOpen: boolean; settingsOpen: boolean;
addCommunityOpen: boolean; addCommunityOpen: boolean;
communitySettingsOpen: boolean;
} }
export { type IAppState, type IDialogsOpen }; export { type IAppState, type IDialogsOpen };

View file

@ -1,10 +1,12 @@
import { import {
IFetchLoginRequest, IFetchLoginRequest,
IFetchLoginResponse, IFetchLoginResponse,
IFetchRefreshResponse, IFetchRefreshResponseError,
IFetchRefreshResponseSuccess,
} from "../../api/auth"; } from "../../api/auth";
enum AuthActionTypes { enum AuthActionTypes {
SET_LOGGED_IN = "SET_LOGGED_IN",
FETCH_LOGIN_START = "FETCH_LOGIN_START", FETCH_LOGIN_START = "FETCH_LOGIN_START",
FETCH_LOGIN_FINISH = "FETCH_LOGIN_FINISH", FETCH_LOGIN_FINISH = "FETCH_LOGIN_FINISH",
FETCH_REFRESH_START = "FETCH_REFRESH_START", FETCH_REFRESH_START = "FETCH_REFRESH_START",
@ -12,6 +14,7 @@ enum AuthActionTypes {
} }
type AuthAction = type AuthAction =
| { type: AuthActionTypes.SET_LOGGED_IN; payload: boolean }
| { type: AuthActionTypes.FETCH_LOGIN_START; payload: IFetchLoginRequest } | { type: AuthActionTypes.FETCH_LOGIN_START; payload: IFetchLoginRequest }
| { | {
type: AuthActionTypes.FETCH_LOGIN_FINISH; type: AuthActionTypes.FETCH_LOGIN_FINISH;
@ -20,7 +23,7 @@ type AuthAction =
| { type: AuthActionTypes.FETCH_REFRESH_START } | { type: AuthActionTypes.FETCH_REFRESH_START }
| { | {
type: AuthActionTypes.FETCH_REFRESH_FINISH; type: AuthActionTypes.FETCH_REFRESH_FINISH;
payload: IFetchRefreshResponse; payload: IFetchRefreshResponseError | IFetchRefreshResponseSuccess;
}; };
export { AuthActionTypes, type AuthAction }; export { AuthActionTypes, type AuthAction };

View file

@ -5,6 +5,9 @@ import { IAuthState } from "./types";
function authReducer(_state: IAuthState, action: AuthAction) { function authReducer(_state: IAuthState, action: AuthAction) {
switch (action.type) { switch (action.type) {
case AuthActionTypes.SET_LOGGED_IN:
setState("auth", "loggedIn", action.payload);
break;
case AuthActionTypes.FETCH_LOGIN_START: case AuthActionTypes.FETCH_LOGIN_START:
fetchLogin(action.payload.username, action.payload.password); fetchLogin(action.payload.username, action.payload.password);
break; break;
@ -15,7 +18,9 @@ function authReducer(_state: IAuthState, action: AuthAction) {
fetchRefresh(); fetchRefresh();
break; break;
case AuthActionTypes.FETCH_REFRESH_FINISH: case AuthActionTypes.FETCH_REFRESH_FINISH:
setState("auth", "session", action.payload); if ("id" in action.payload) {
setState("auth", "session", action.payload);
}
break; break;
} }
} }

View file

@ -1,4 +1,5 @@
interface IAuthState { interface IAuthState {
loggedIn?: boolean;
session?: ISession; session?: ISession;
} }

View file

@ -5,12 +5,16 @@ import {
ICreateChannelResponse, ICreateChannelResponse,
IUpdateChannelResponse, IUpdateChannelResponse,
IRemoveChannelResponse, IRemoveChannelResponse,
IFetchChannelMessagesResponse,
IFetchChannelMessage,
} from "../../api/channel"; } from "../../api/channel";
import { IFetchCommunityChannel } from "../../api/community"; import { IFetchCommunityChannel } from "../../api/community";
enum ChannelActionTypes { enum ChannelActionTypes {
SET_CHANNEL = "SET_CHANNEL", SET_CHANNEL = "SET_CHANNEL",
SET_ACTIVE_CHANNEL = "SET_ACTIVE_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_START = "FETCH_CHANNEL_START",
FETCH_CHANNEL_FINISH = "FETCH_CHANNEL_FINISH", FETCH_CHANNEL_FINISH = "FETCH_CHANNEL_FINISH",
CREATE_CHANNEL_START = "CREATE_CHANNEL_START", CREATE_CHANNEL_START = "CREATE_CHANNEL_START",
@ -19,6 +23,8 @@ enum ChannelActionTypes {
UPDATE_CHANNEL_FINISH = "UPDATE_CHANNEL_FINISH", UPDATE_CHANNEL_FINISH = "UPDATE_CHANNEL_FINISH",
REMOVE_CHANNEL_START = "REMOVE_CHANNEL_START", REMOVE_CHANNEL_START = "REMOVE_CHANNEL_START",
REMOVE_CHANNEL_FINISH = "REMOVE_CHANNEL_FINISH", 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 ChannelAction =
@ -30,6 +36,14 @@ type ChannelAction =
type: ChannelActionTypes.SET_ACTIVE_CHANNEL; type: ChannelActionTypes.SET_ACTIVE_CHANNEL;
payload: string | undefined; 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_START; payload: string }
| { | {
type: ChannelActionTypes.FETCH_CHANNEL_FINISH; type: ChannelActionTypes.FETCH_CHANNEL_FINISH;
@ -55,6 +69,11 @@ type ChannelAction =
| { | {
type: ChannelActionTypes.REMOVE_CHANNEL_FINISH; type: ChannelActionTypes.REMOVE_CHANNEL_FINISH;
payload: IRemoveChannelResponse; payload: IRemoveChannelResponse;
}
| { type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_START; payload: string }
| {
type: ChannelActionTypes.FETCH_CHANNEL_MESSAGES_FINISH;
payload: IFetchChannelMessagesResponse;
}; };
export { ChannelActionTypes, type ChannelAction }; export { ChannelActionTypes, type ChannelAction };

View file

@ -3,12 +3,13 @@ import {
createChannel, createChannel,
updateChannel, updateChannel,
removeChannel, removeChannel,
fetchChannelMessages,
} from "../../services/channel"; } from "../../services/channel";
import { setState } from "../state"; import { setState } from "../state";
import { ChannelActionTypes, ChannelAction } from "./actions"; import { ChannelActionTypes, ChannelAction } from "./actions";
import { IChannelState } from "./types"; import { IChannelState } from "./types";
function channelReducer(state: IChannelState, action: ChannelAction) { function channelReducer(_state: IChannelState, action: ChannelAction) {
switch (action.type) { switch (action.type) {
case ChannelActionTypes.SET_CHANNEL: case ChannelActionTypes.SET_CHANNEL:
setState("channel", "channels", action.payload.id, action.payload); setState("channel", "channels", action.payload.id, action.payload);
@ -16,6 +17,25 @@ function channelReducer(state: IChannelState, action: ChannelAction) {
case ChannelActionTypes.SET_ACTIVE_CHANNEL: case ChannelActionTypes.SET_ACTIVE_CHANNEL:
setState("channel", "active", action.payload); setState("channel", "active", action.payload);
break; 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: case ChannelActionTypes.FETCH_CHANNEL_START:
fetchChannel(action.payload); fetchChannel(action.payload);
break; break;
@ -48,6 +68,25 @@ function channelReducer(state: IChannelState, action: ChannelAction) {
return copy; return copy;
}); });
break; 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;
} }
} }

View file

@ -1,3 +1,5 @@
import { IMessage } from "../message";
interface IChannelState { interface IChannelState {
active?: string; active?: string;
channels: Record<string, IChannel>; channels: Record<string, IChannel>;
@ -9,6 +11,8 @@ interface IChannel {
description?: string; description?: string;
communityId?: string; communityId?: string;
creationDate?: number; creationDate?: number;
text?: string;
messages?: Record<string, IMessage>;
} }
export { type IChannelState, type IChannel }; export { type IChannelState, type IChannel };

View file

@ -12,7 +12,7 @@ import { setState } from "../state";
import { CommunityActionTypes, CommunityAction } from "./actions"; import { CommunityActionTypes, CommunityAction } from "./actions";
import { ICommunityState } from "./types"; import { ICommunityState } from "./types";
function communityReducer(state: ICommunityState, action: CommunityAction) { function communityReducer(_state: ICommunityState, action: CommunityAction) {
switch (action.type) { switch (action.type) {
case CommunityActionTypes.SET_COMMUNITY: case CommunityActionTypes.SET_COMMUNITY:
setState( setState(

View file

@ -3,7 +3,7 @@ import { setState } from "../state";
import { InviteActionTypes, InviteAction } from "./actions"; import { InviteActionTypes, InviteAction } from "./actions";
import { IInviteState } from "./types"; import { IInviteState } from "./types";
function inviteReducer(state: IInviteState, action: InviteAction) { function inviteReducer(_state: IInviteState, action: InviteAction) {
switch (action.type) { switch (action.type) {
case InviteActionTypes.SET_INVITE: case InviteActionTypes.SET_INVITE:
setState("invite", "invites", action.payload.id, action.payload); setState("invite", "invites", action.payload.id, action.payload);

View file

@ -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 };

View file

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

View file

@ -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 };

View file

@ -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 };

View file

@ -8,6 +8,7 @@ import { ChannelAction, channelReducer } from "./channel";
import { RoleAction, roleReducer } from "./role"; import { RoleAction, roleReducer } from "./role";
import { SessionAction, sessionReducer } from "./session"; import { SessionAction, sessionReducer } from "./session";
import { InviteAction, inviteReducer } from "./invite"; import { InviteAction, inviteReducer } from "./invite";
import { MessageAction, messageReducer } from "./message";
function reducer(state: IState, action: Action) { function reducer(state: IState, action: Action) {
appReducer(state.app, action as AppAction); appReducer(state.app, action as AppAction);
@ -18,6 +19,7 @@ function reducer(state: IState, action: Action) {
roleReducer(state.role, action as RoleAction); roleReducer(state.role, action as RoleAction);
sessionReducer(state.session, action as SessionAction); sessionReducer(state.session, action as SessionAction);
inviteReducer(state.invite, action as InviteAction); inviteReducer(state.invite, action as InviteAction);
messageReducer(state.message, action as MessageAction);
} }
export { reducer }; export { reducer };

View file

@ -1,6 +1,7 @@
import { IFetchCommunityRole } from "../../api/community"; import { IFetchCommunityRole } from "../../api/community";
import { import {
ICreateRoleRequest, ICreateRoleRequest,
IUpdateRoleRequest,
IFetchRoleResponse, IFetchRoleResponse,
ICreateRoleResponse, ICreateRoleResponse,
IUpdateRoleResponse, IUpdateRoleResponse,
@ -34,7 +35,7 @@ type RoleAction =
type: RoleActionTypes.CREATE_ROLE_FINISH; type: RoleActionTypes.CREATE_ROLE_FINISH;
payload: ICreateRoleResponse; payload: ICreateRoleResponse;
} }
| { type: RoleActionTypes.UPDATE_ROLE_START; payload: string } | { type: RoleActionTypes.UPDATE_ROLE_START; payload: IUpdateRoleRequest }
| { | {
type: RoleActionTypes.UPDATE_ROLE_FINISH; type: RoleActionTypes.UPDATE_ROLE_FINISH;
payload: IUpdateRoleResponse; payload: IUpdateRoleResponse;

View file

@ -8,7 +8,7 @@ import { setState } from "../state";
import { RoleActionTypes, RoleAction } from "./actions"; import { RoleActionTypes, RoleAction } from "./actions";
import { IRoleState } from "./types"; import { IRoleState } from "./types";
function roleReducer(state: IRoleState, action: RoleAction) { function roleReducer(_state: IRoleState, action: RoleAction) {
switch (action.type) { switch (action.type) {
case RoleActionTypes.SET_ROLE: case RoleActionTypes.SET_ROLE:
setState("role", "roles", action.payload.id, action.payload); 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); setState("role", "roles", action.payload.id, action.payload);
break; break;
case RoleActionTypes.UPDATE_ROLE_START: case RoleActionTypes.UPDATE_ROLE_START:
updateRole(action.payload); updateRole(action.payload.id, action.payload.name);
break; break;
case RoleActionTypes.UPDATE_ROLE_FINISH: case RoleActionTypes.UPDATE_ROLE_FINISH:
setState("role", "roles", action.payload.id, action.payload); setState("role", "roles", action.payload.id, action.payload);

View file

@ -3,7 +3,7 @@ import { setState } from "../state";
import { SessionActionTypes, SessionAction } from "./actions"; import { SessionActionTypes, SessionAction } from "./actions";
import { ISessionState } from "./types"; import { ISessionState } from "./types";
function sessionReducer(state: ISessionState, action: SessionAction) { function sessionReducer(_state: ISessionState, action: SessionAction) {
switch (action.type) { switch (action.type) {
case SessionActionTypes.SET_SESSION: case SessionActionTypes.SET_SESSION:
setState("session", "sessions", action.payload.id, action.payload); setState("session", "sessions", action.payload.id, action.payload);

View file

@ -9,9 +9,11 @@ const [state, setState] = createStore<IState>({
dialogsOpen: { dialogsOpen: {
settingsOpen: false, settingsOpen: false,
addCommunityOpen: false, addCommunityOpen: false,
communitySettingsOpen: false,
}, },
}, },
auth: { auth: {
loggedIn: undefined,
session: undefined, session: undefined,
}, },
user: { user: {
@ -33,6 +35,9 @@ const [state, setState] = createStore<IState>({
invite: { invite: {
invites: {}, invites: {},
}, },
message: {
message: undefined,
},
}); });
function dispatch(action: Action) { function dispatch(action: Action) {

View file

@ -6,6 +6,7 @@ import { IChannelState } from "./channel";
import { IRoleState } from "./role"; import { IRoleState } from "./role";
import { ISessionState } from "./session"; import { ISessionState } from "./session";
import { IInviteState } from "./invite"; import { IInviteState } from "./invite";
import { IMessageState } from "./message";
interface IState { interface IState {
app: IAppState; app: IAppState;
@ -16,6 +17,7 @@ interface IState {
role: IRoleState; role: IRoleState;
session: ISessionState; session: ISessionState;
invite: IInviteState; invite: IInviteState;
message: IMessageState;
} }
export { type IState }; export { type IState };

View file

@ -8,7 +8,7 @@ import { setState } from "../state";
import { UserActionTypes, UserAction } from "./actions"; import { UserActionTypes, UserAction } from "./actions";
import { IUserState } from "./types"; import { IUserState } from "./types";
function userReducer(state: IUserState, action: UserAction) { function userReducer(_state: IUserState, action: UserAction) {
switch (action.type) { switch (action.type) {
case UserActionTypes.SET_USER: case UserActionTypes.SET_USER:
setState("user", "users", action.payload.id, action.payload); setState("user", "users", action.payload.id, action.payload);

View file

@ -8,8 +8,12 @@ import { AuthActionTypes } from "../../store/auth";
import { HomeView } from "../HomeView"; import { HomeView } from "../HomeView";
import { CommunityView } from "../CommunityView"; import { CommunityView } from "../CommunityView";
import { ModalView } from "../ModalView"; import { ModalView } from "../ModalView";
import { useNavigate } from "@solidjs/router";
import { connectWs } from "../../services/websocket";
const AppView: Component = () => {
const navigate = useNavigate();
const MainView: Component = () => {
onMount(() => { onMount(() => {
dispatch({ dispatch({
type: AuthActionTypes.FETCH_REFRESH_START, type: AuthActionTypes.FETCH_REFRESH_START,
@ -31,6 +35,14 @@ const MainView: Component = () => {
} }
}); });
createEffect(() => {
if (state.auth.loggedIn === false) {
navigate("/login");
} else {
connectWs();
}
});
return ( return (
<div class="flex flex-row h-screen"> <div class="flex flex-row h-screen">
<ModalView /> <ModalView />
@ -50,4 +62,4 @@ const MainView: Component = () => {
); );
}; };
export { MainView }; export { AppView };

View file

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

View file

@ -4,6 +4,7 @@ import { CommunityActionTypes, ICommunity } from "../../store/community";
import { ChannelActionTypes } from "../../store/channel"; import { ChannelActionTypes } from "../../store/channel";
import { Channel } from "../../components/Channel"; import { Channel } from "../../components/Channel";
import { CommunityBar } from "../../components/CommunityBar"; import { CommunityBar } from "../../components/CommunityBar";
import { AppActionTypes } from "../../store/app";
const ChannelView: Component = () => { const ChannelView: Component = () => {
const channelIds = createMemo(() => { 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) => { const onChannelClick = (id: string) => {
dispatch({ dispatch({
type: ChannelActionTypes.FETCH_CHANNEL_START, type: ChannelActionTypes.FETCH_CHANNEL_START,
@ -70,6 +78,7 @@ const ChannelView: Component = () => {
avatar={ avatar={
"https://img.daisyui.com/images/profile/demo/yellingcat@192.webp" "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp"
} }
onSettingsClick={onCommunitySettingsClick}
/> />
<ul class="h-full list flex flex-col p-2 gap-1 pt-18 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800"> <ul class="h-full list flex flex-col p-2 gap-1 pt-18 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800">
{channelIds().map(mapChannel)} {channelIds().map(mapChannel)}

View file

@ -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 { ChannelBar } from "../../components/ChannelBar";
import { MessageBar } from "../../components/MessageBar"; import { MessageBar } from "../../components/MessageBar";
import { Message } from "../../components/Message"; import { Message } from "../../components/Message";
import { dispatch, state } from "../../store/state"; import { dispatch, state } from "../../store/state";
import { UserActionTypes } from "../../store/user"; 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 ChatView: Component = () => {
const testMessages = [ let scrollRef: HTMLUListElement | undefined;
{ let lastScrollTop = 0;
messageId: "432432", let autoScroll = true;
message: "Hello this is a test message",
userId: "12121", const scrollToBottom = () => {
username: "Aslan", if (!scrollRef || !autoScroll) {
avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", return;
}, }
{
messageId: "432433", scrollRef.scrollTop = scrollRef.scrollHeight;
message: "Hi hi", };
userId: "12122",
username: "TestUser", const handleScroll = () => {
avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", if (!scrollRef) {
}, return;
{ }
messageId: "432434",
message: "Nooooo", if (scrollRef.scrollTop < lastScrollTop) {
userId: "12121", autoScroll = false;
username: "Aslan", }
avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", if (
}, scrollRef.scrollTop + scrollRef.clientHeight >=
{ scrollRef.scrollHeight
messageId: "432434", ) {
message: "Nooooo", autoScroll = true;
userId: "12121", }
username: "Aslan",
avatar: "https://img.daisyui.com/images/profile/demo/yellingcat@192.webp", lastScrollTop = scrollRef.scrollTop;
}, };
{
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",
},
];
const channelInfo = createMemo<IChannel | undefined>(() => { const channelInfo = createMemo<IChannel | undefined>(() => {
const activeChannelId = state.channel.active; 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(() => { onMount(() => {
if (scrollRef) { if (scrollRef) {
scrollRef.scrollTop = scrollRef.scrollHeight; 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 ( return (
<div class="bg-stone-800 flex-1 z-0 relative"> <div class="bg-stone-800 flex-1 z-0 relative">
<div class="h-full"> <div class="h-full">
@ -207,20 +139,29 @@ const ChatView: Component = () => {
/> />
<ul <ul
ref={scrollRef} ref={scrollRef}
onScrollEnd={handleScroll}
class="h-full list flex flex-col p-2 pt-18 pb-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800" class="h-full list flex flex-col p-2 pt-18 pb-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-500 scrollbar-track-gray-800"
> >
{testMessages.map((msg) => ( {Object.entries(messages()).map(([_key, message]) => (
<Message <Message
messageId={msg.messageId} messageId={message.id}
message={msg.message} message={message.text}
userId={msg.userId} userId={message.ownerId}
username={msg.username} username={
avatar={msg.avatar} state.user.users[message.ownerId].username ?? ""
}
avatar={
"https://img.daisyui.com/images/profile/demo/yellingcat@192.webp"
}
onProfileClick={onProfileClick} onProfileClick={onProfileClick}
/> />
))} ))}
</ul> </ul>
<MessageBar /> <MessageBar
text={channelInfo()?.text ?? ""}
onChangeText={onChangeMessageText}
onSend={onMessageSend}
/>
</div> </div>
</div> </div>
); );

View file

@ -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 { dispatch, state } from "../../store/state";
import { AuthActionTypes } from "../../store/auth"; import { AuthActionTypes } from "../../store/auth";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
@ -9,6 +9,12 @@ const LoginView: Component = () => {
const navigate = useNavigate(); const navigate = useNavigate();
onMount(() => {
dispatch({
type: AuthActionTypes.FETCH_REFRESH_START,
});
});
const onLogin = () => { const onLogin = () => {
dispatch({ dispatch({
type: AuthActionTypes.FETCH_LOGIN_START, type: AuthActionTypes.FETCH_LOGIN_START,
@ -20,8 +26,8 @@ const LoginView: Component = () => {
}; };
createEffect(() => { createEffect(() => {
if (state.auth.session?.id) { if (state.auth.loggedIn) {
navigate("/"); navigate("/app");
} }
}); });

View file

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

View file

@ -2,11 +2,13 @@ import { createEffect, type Component } from "solid-js";
import { dispatch, state } from "../../store/state"; import { dispatch, state } from "../../store/state";
import SettingsModal from "../../components/SettingsModal/SettingsModal"; import SettingsModal from "../../components/SettingsModal/SettingsModal";
import CommunityModal from "../../components/CommunityModal/CommunityModal"; import CommunityModal from "../../components/CommunityModal/CommunityModal";
import CommunitySettingsModal from "../../components/CommunitySettingsModal/CommunitySettingsModal";
import { AppActionTypes } from "../../store/app"; import { AppActionTypes } from "../../store/app";
const ModalView: Component = () => { const ModalView: Component = () => {
let settingsModal: HTMLDialogElement; let settingsModal: HTMLDialogElement;
let communityModal: HTMLDialogElement; let communityModal: HTMLDialogElement;
let communitySettingsModal: HTMLDialogElement;
createEffect(() => { createEffect(() => {
if (state.app.dialogsOpen.settingsOpen) { 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 = () => { const onCloseSettings = () => {
dispatch({ dispatch({
type: AppActionTypes.SET_SETTINGS_OPEN, 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 ( return (
<> <>
<SettingsModal <SettingsModal
@ -48,6 +65,10 @@ const ModalView: Component = () => {
dialogRef={(element) => (communityModal = element)} dialogRef={(element) => (communityModal = element)}
onClose={onCloseCommunity} onClose={onCloseCommunity}
/> />
<CommunitySettingsModal
dialogRef={(element) => (communitySettingsModal = element)}
onClose={onCloseCommunitySettings}
/>
</> </>
); );
}; };

View file

@ -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 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 <div></div>; return <div></div>;
}; };

View file

@ -1,7 +0,0 @@
import type { Component } from "solid-js";
const SettingsView: Component = () => {
return <div></div>;
};
export { SettingsView };

View file

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

View file

@ -0,0 +1,7 @@
import { type Component } from "solid-js";
const StartView: Component = () => {
return <div></div>;
};
export { StartView };

View file

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