diff --git a/src/app/content/components/MessageItem.tsx b/src/app/content/components/MessageItem.tsx index bfe7089..c5ee485 100644 --- a/src/app/content/components/MessageItem.tsx +++ b/src/app/content/components/MessageItem.tsx @@ -1,19 +1,22 @@ import { type FC } from 'react' import { FrownIcon, ThumbsUpIcon } from 'lucide-react' +import { Badge } from '@/components/ui/Badge' import LikeButton from './LikeButton' import FormatDate from './FormatDate' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar' import { Markdown } from '@/components/ui/Markdown' -import { type Message } from '@/domain/MessageList' +import { type NormalMessage } from '@/domain/MessageList' +import { cn } from '@/utils' export interface MessageItemProps { - data: Message + data: NormalMessage index?: number like: boolean hate: boolean onLikeChange?: (checked: boolean) => void onHateChange?: (checked: boolean) => void + className?: string } const MessageItem: FC = (props) => { @@ -24,7 +27,10 @@ const MessageItem: FC = (props) => { props.onHateChange?.(checked) } return ( -
+
{props.data.username.at(0)} @@ -65,4 +71,5 @@ const MessageItem: FC = (props) => { } MessageItem.displayName = 'MessageItem' + export default MessageItem diff --git a/src/app/content/components/MessageList.tsx b/src/app/content/components/MessageList.tsx index 1d88139..f368618 100644 --- a/src/app/content/components/MessageList.tsx +++ b/src/app/content/components/MessageList.tsx @@ -1,11 +1,12 @@ import { FC, useRef, type ReactElement } from 'react' import { type MessageItemProps } from './MessageItem' +import { type PromptItemProps } from './PromptItem' import { ScrollArea } from '@/components/ui/ScrollArea' import { Virtuoso } from 'react-virtuoso' export interface MessageListProps { - children?: Array> + children?: Array> } const MessageList: FC = ({ children }) => { const scrollParentRef = useRef(null) @@ -16,7 +17,7 @@ const MessageList: FC = ({ children }) => { initialTopMostItemIndex={{ index: 'LAST', align: 'end' }} data={children} customScrollParent={scrollParentRef.current!} - itemContent={(_: any, item: ReactElement) => item} + itemContent={(_: any, item: ReactElement) => item} /> ) diff --git a/src/app/content/components/PromptItem.tsx b/src/app/content/components/PromptItem.tsx new file mode 100644 index 0000000..d17dae0 --- /dev/null +++ b/src/app/content/components/PromptItem.tsx @@ -0,0 +1,29 @@ +import { Avatar, AvatarFallback } from '@/components/ui/Avatar' +import { Badge } from '@/components/ui/Badge' +import { PromptMessage } from '@/domain/MessageList' +import { cn } from '@/utils' +import { AvatarImage } from '@radix-ui/react-avatar' +import { FC } from 'react' + +export interface PromptItemProps { + data: PromptMessage + className?: string +} + +const PromptItem: FC = ({ data, className }) => { + return ( +
+ + + + {data.username.at(0)} + + {data.body} + +
+ ) +} + +PromptItem.displayName = 'PromptItem' + +export default PromptItem diff --git a/src/app/content/index.tsx b/src/app/content/index.tsx index 4573c7c..a46603f 100644 --- a/src/app/content/index.tsx +++ b/src/app/content/index.tsx @@ -11,14 +11,15 @@ import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Stora import { PeerRoomImpl } from '@/domain/impls/PeerRoom' import '@/assets/styles/tailwind.css' import { createElement } from '@/utils' +import { ToastImpl } from '@/domain/impls/Toast' export default defineContentScript({ cssInjectionMode: 'ui', matches: ['*://*.example.com/*', '*://*.v2ex.com/*'], async main(ctx) { const store = Remesh.store({ - externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl], - inspectors: __DEV__ ? [RemeshLogger()] : [] + externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl], + inspectors: !__DEV__ ? [RemeshLogger()] : [] }) const ui = await createShadowRootUi(ctx, { @@ -32,11 +33,11 @@ export default defineContentScript({ const root = createRoot(app) root.render( - - - - - + // + + + + // ) return root }, diff --git a/src/app/content/views/Footer/index.tsx b/src/app/content/views/Footer/index.tsx index 1d4ef4e..5365ec1 100644 --- a/src/app/content/views/Footer/index.tsx +++ b/src/app/content/views/Footer/index.tsx @@ -32,7 +32,7 @@ const Footer: FC = () => { } return ( -
+
{ const siteInfo = getSiteInfo() const roomDomain = useRemeshDomain(RoomDomain()) - const peerList = useRemeshQuery(roomDomain.query.PeerListQuery()) + const userList = useRemeshQuery(roomDomain.query.UserListQuery()) + console.log('userList', [...userList], userList.length) return (
@@ -24,7 +26,8 @@ const Header: FC = () => { @@ -45,7 +48,7 @@ const Header: FC = () => {
-
Online {peerList.length}
+
Online {userList.length}
) } diff --git a/src/app/content/views/Main/index.tsx b/src/app/content/views/Main/index.tsx index 62e9c44..121a6d8 100644 --- a/src/app/content/views/Main/index.tsx +++ b/src/app/content/views/Main/index.tsx @@ -3,8 +3,9 @@ import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react' import MessageList from '../../components/MessageList' import MessageItem from '../../components/MessageItem' +import PromptItem from '../../components/PromptItem' import UserInfoDomain from '@/domain/UserInfo' -import RoomDomain from '@/domain/Room' +import RoomDomain, { MessageType } from '@/domain/Room' const Main: FC = () => { const send = useRemeshSend() @@ -12,11 +13,16 @@ const Main: FC = () => { const userInfoDomain = useRemeshDomain(UserInfoDomain()) const _messageList = useRemeshQuery(roomDomain.query.MessageListQuery()) const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery()) - const messageList = _messageList.map((message) => ({ - ...message, - like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id), - hate: message.hateUsers.some((hateUser) => hateUser.userId === userInfo?.id) - })) + const messageList = _messageList.map((message) => { + if (message.type === MessageType.Normal) { + return { + ...message, + like: message.likeUsers.some((likeUser) => likeUser.userId === userInfo?.id), + hate: message.hateUsers.some((hateUser) => hateUser.userId === userInfo?.id) + } + } + return message + }) const handleLikeChange = (messageId: string) => { send(roomDomain.command.SendLikeMessageCommand(messageId)) @@ -28,17 +34,25 @@ const Main: FC = () => { return ( - {messageList.map((message, index) => ( - handleLikeChange(message.id)} - onHateChange={() => handleHateChange(message.id)} - > - ))} + {messageList.map((message, index) => + message.type === MessageType.Normal ? ( + handleLikeChange(message.id)} + onHateChange={() => handleHateChange(message.id)} + > + ) : ( + + ) + )} ) } diff --git a/src/app/content/views/Setup/index.tsx b/src/app/content/views/Setup/index.tsx index f915753..677211f 100644 --- a/src/app/content/views/Setup/index.tsx +++ b/src/app/content/views/Setup/index.tsx @@ -1,7 +1,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar' import { Button } from '@/components/ui/Button' import { MAX_AVATAR_SIZE } from '@/constants/config' -import MessageListDomain, { Message } from '@/domain/MessageList' +import MessageListDomain, { Message, MessageType } from '@/domain/MessageList' import UserInfoDomain, { UserInfo } from '@/domain/UserInfo' import { checkSystemDarkMode, generateRandomAvatar, generateRandomName } from '@/utils' import { UserIcon } from 'lucide-react' @@ -46,6 +46,7 @@ const generateMessage = async (userInfo: UserInfo): Promise => { id: nanoid(), body: mockTextList.shift()!, date: Date.now(), + type: MessageType.Normal, userId, username, userAvatar, diff --git a/src/app/options/components/AvatarSelect.tsx b/src/app/options/components/AvatarSelect.tsx index 2d7a150..511e64b 100644 --- a/src/app/options/components/AvatarSelect.tsx +++ b/src/app/options/components/AvatarSelect.tsx @@ -1,6 +1,6 @@ +import React from 'react' import { type ChangeEvent } from 'react' import { ImagePlusIcon } from 'lucide-react' -import React from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar' import { Label } from '@/components/ui/Label' import { cn, compressImage } from '@/utils' diff --git a/src/app/options/components/ProfileForm.tsx b/src/app/options/components/ProfileForm.tsx index 15c4e04..626174c 100644 --- a/src/app/options/components/ProfileForm.tsx +++ b/src/app/options/components/ProfileForm.tsx @@ -1,7 +1,6 @@ import * as v from 'valibot' import { useForm } from 'react-hook-form' import { valibotResolver } from '@hookform/resolvers/valibot' -import { toast } from 'sonner' import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react' import { nanoid } from 'nanoid' import { useEffect } from 'react' @@ -15,6 +14,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup' import { Label } from '@/components/ui/Label' import { RefreshCcwIcon } from 'lucide-react' import { MAX_AVATAR_SIZE } from '@/constants/config' +import ToastDomain from '@/domain/Toast' const defaultUserInfo: UserInfo = { id: nanoid(), @@ -52,6 +52,8 @@ const formSchema = v.object({ const ProfileForm = () => { const send = useRemeshSend() + const toastDomain = useRemeshDomain(ToastDomain()) + const userInfoDomain = useRemeshDomain(UserInfoDomain()) const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery()) @@ -67,15 +69,15 @@ const ProfileForm = () => { const handleSubmit = (userInfo: UserInfo) => { send(userInfoDomain.command.UpdateUserInfoCommand(userInfo)) - toast.success('Saved successfully!') + send(toastDomain.command.SuccessCommand('Saved successfully!')) } const handleWarning = (error: Error) => { - toast.warning(error.message) + send(toastDomain.command.WarningCommand(error.message)) } const handleError = (error: Error) => { - toast.error(error.message) + send(toastDomain.command.ErrorCommand(error.message)) } const handleRefreshAvatar = async () => { diff --git a/src/app/options/main.tsx b/src/app/options/main.tsx index d1291ee..629835c 100644 --- a/src/app/options/main.tsx +++ b/src/app/options/main.tsx @@ -6,9 +6,10 @@ import { RemeshLogger } from 'remesh-logger' import App from './App' import { BrowserSyncStorageImpl } from '@/domain/impls/Storage' import '@/assets/styles/tailwind.css' +import { ToastImpl } from '@/domain/impls/Toast' const store = Remesh.store({ - externs: [BrowserSyncStorageImpl], + externs: [BrowserSyncStorageImpl, ToastImpl], inspectors: [RemeshLogger()] }) diff --git a/src/domain/MessageList.ts b/src/domain/MessageList.ts index 6791b27..df7cd65 100644 --- a/src/domain/MessageList.ts +++ b/src/domain/MessageList.ts @@ -3,13 +3,19 @@ import { ListModule } from 'remesh/modules/list' import { IndexDBStorageExtern } from '@/domain/externs/Storage' import StorageEffect from '@/domain/modules/StorageEffect' +export enum MessageType { + Normal = 'normal', + Prompt = 'prompt' +} + export interface MessageUser { userId: string username: string userAvatar: string } -export interface Message extends MessageUser { +export interface NormalMessage extends MessageUser { + type: MessageType.Normal id: string body: string date: number @@ -17,6 +23,15 @@ export interface Message extends MessageUser { hateUsers: MessageUser[] } +export interface PromptMessage extends MessageUser { + type: MessageType.Prompt + id: string + body: string + date: number +} + +export type Message = NormalMessage | PromptMessage + export const STORAGE_KEY = `MESSAGE_LIST` const MessageListDomain = Remesh.domain({ diff --git a/src/domain/Room.ts b/src/domain/Room.ts index c561f05..d001353 100644 --- a/src/domain/Room.ts +++ b/src/domain/Room.ts @@ -1,36 +1,48 @@ import { Remesh } from 'remesh' -import { map, merge, switchMap, tap, defer, of, EMPTY, mergeMap } from 'rxjs' -import { type MessageUser } from './MessageList' +import { map, merge, switchMap, tap, of, EMPTY, mergeMap } from 'rxjs' +import { NormalMessage, type MessageUser } from './MessageList' import { PeerRoomExtern } from '@/domain/externs/PeerRoom' -import MessageListDomain from '@/domain/MessageList' +import MessageListDomain, { MessageType } from '@/domain/MessageList' import UserInfoDomain from '@/domain/UserInfo' -import { callbackToObservable, desert } from '@/utils' +import { callbackToObservable, desert, upsert } from '@/utils' import { nanoid } from 'nanoid' -import StatusModule from './modules/Status' +import StatusModule from '@/domain/modules/Status' -export enum MessageType { +export { MessageType } + +export enum SendType { Like = 'like', Hate = 'hate', - Text = 'text' + Text = 'text', + UserSync = 'userSync' +} + +export interface SyncUserMessage extends MessageUser { + type: SendType.UserSync + id: string + peerId: string + joinTime: number } export interface LikeMessage extends MessageUser { - type: MessageType.Like + type: SendType.Like id: string } export interface HateMessage extends MessageUser { - type: MessageType.Hate + type: SendType.Hate id: string } export interface TextMessage extends MessageUser { - type: MessageType.Text + type: SendType.Text id: string body: string } -export type RoomMessage = LikeMessage | HateMessage | TextMessage +export type RoomMessage = SyncUserMessage | LikeMessage | HateMessage | TextMessage + +export type RoomUser = MessageUser & { peerId: string; joinTime: number } const RoomDomain = Remesh.domain({ name: 'RoomDomain', @@ -41,35 +53,51 @@ const RoomDomain = Remesh.domain({ const MessageListQuery = messageListDomain.query.ListQuery - const RoomStatusState = StatusModule(domain, { + const RoomStatusModule = StatusModule(domain, { name: 'Room.RoomStatusModule' }) - const PeerListState = domain.state({ - name: 'Room.PeerListState', - default: [peerRoom.selfId] + const UserListState = domain.state({ + name: 'RoomUserListState', + default: [] }) - const PeerListQuery = domain.query({ - name: 'Room.PeerListQuery', + const UserListQuery = domain.query({ + name: 'Room.UserListQuery', impl: ({ get }) => { - return get(PeerListState()) + return get(UserListState()) } }) const JoinRoomCommand = domain.command({ name: 'RoomJoinRoomCommand', - impl: (_, roomId: string) => { + impl: ({ get }, roomId: string) => { peerRoom.joinRoom(roomId) - return [JoinRoomEvent(roomId), RoomStatusState.command.SetFinishedCommand()] + const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + return [ + JoinRoomEvent(roomId), + RoomStatusModule.command.SetFinishedCommand(), + UpdateUserListCommand({ + type: 'create', + user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar } + }) + ] } }) const LeaveRoomCommand = domain.command({ name: 'RoomLeaveRoomCommand', - impl: (_, roomId: string) => { + impl: ({ get }, roomId: string) => { peerRoom.leaveRoom() - return [LeaveRoomEvent(roomId), RoomStatusState.command.SetInitialCommand()] + const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + return [ + LeaveRoomEvent(roomId), + RoomStatusModule.command.SetInitialCommand(), + UpdateUserListCommand({ + type: 'delete', + user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar } + }) + ] } }) @@ -78,31 +106,29 @@ const RoomDomain = Remesh.domain({ impl: ({ get }, message: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! const id = nanoid() + const date = Date.now() return [ messageListDomain.command.CreateItemCommand({ id, + type: MessageType.Normal, body: message, - date: Date.now(), + date, userId, username, userAvatar, likeUsers: [], hateUsers: [] }), - SendTextMessageEvent({ id, body: message, userId, username, userAvatar, type: MessageType.Text }) + SendTextMessageEvent({ id, body: message, userId, username, userAvatar, type: SendType.Text }) ] } }) - const SendTextMessageEvent = domain.event({ - name: 'RoomSendTextMessageEvent' - }) - const SendLikeMessageCommand = domain.command({ name: 'RoomSendLikeMessageCommand', impl: ({ get }, messageId: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const _message = get(messageListDomain.query.ItemQuery(messageId)) + const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage return [ messageListDomain.command.UpdateItemCommand({ ..._message, @@ -116,20 +142,22 @@ const RoomDomain = Remesh.domain({ 'userId' ) }), - SendLikeMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Like }) + SendLikeMessageEvent({ + id: messageId, + userId, + username, + userAvatar, + type: SendType.Like + }) ] } }) - const SendLikeMessageEvent = domain.event({ - name: 'RoomSendLikeMessageEvent' - }) - const SendHateMessageCommand = domain.command({ name: 'RoomSendHateMessageCommand', impl: ({ get }, messageId: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const _message = get(messageListDomain.query.ItemQuery(messageId)) + const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage return [ messageListDomain.command.UpdateItemCommand({ @@ -144,12 +172,44 @@ const RoomDomain = Remesh.domain({ 'userId' ) }), - SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Hate }) + SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: SendType.Hate }) ] } }) - const SendHateMessageEvent = domain.event({ + const SendUserSyncMessageCommand = domain.command({ + name: 'RoomSendUserSyncMessageCommand', + impl: ({ get }, targetPeerId: string) => { + const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + const joinTime = get(UserListQuery()).find((u) => u.peerId === peerRoom.selfId)?.joinTime || Date.now() + return [ + SendUserSyncMessageEvent({ + id: nanoid(), + peerId: peerRoom.selfId, + targetPeerId, + userId, + joinTime, + username, + userAvatar, + type: SendType.UserSync + }) + ] + } + }) + + const SendUserSyncMessageEvent = domain.event({ + name: 'RoomSendUserSyncMessageEvent' + }) + + const SendTextMessageEvent = domain.event({ + name: 'RoomSendTextMessageEvent' + }) + + const SendLikeMessageEvent = domain.event({ + name: 'RoomSendLikeMessageEvent' + }) + + const SendHateMessageEvent = domain.event({ name: 'RoomSendHateMessageEvent' }) @@ -173,17 +233,15 @@ const RoomDomain = Remesh.domain({ name: 'RoomOnLeaveRoomEvent' }) - const UpdatePeerListCommand = domain.command({ - name: 'RoomUpdatePeerListCommand', - impl: ({ get }, action: { type: 'create' | 'delete'; peerId: string }) => { - const peerList = get(PeerListState()) + const UpdateUserListCommand = domain.command({ + name: 'RoomUpdateUserListCommand', + impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => { + const userList = get(UserListState()) if (action.type === 'create') { - return [PeerListState().new([...new Set(peerList).add(action.peerId)])] + return [UserListState().new(upsert(userList, action.user, 'peerId'))] + } else { + return [UserListState().new(userList.filter(({ peerId }) => peerId !== action.user.peerId))] } - if (action.type === 'delete') { - return [PeerListState().new(peerList.filter((peerId) => peerId == action.peerId))] - } - return null } }) @@ -204,7 +262,7 @@ const RoomDomain = Remesh.domain({ impl: ({ fromEvent }) => { const likeMessage$ = fromEvent(SendLikeMessageEvent).pipe( tap(async (message) => { - peerRoom.sendMessage(message) + return peerRoom.sendMessage(message) }) ) return merge(likeMessage$).pipe(map(() => null)) @@ -223,6 +281,20 @@ const RoomDomain = Remesh.domain({ } }) + domain.effect({ + name: 'RoomSendUserSyncMessageEffect', + impl: ({ fromEvent }) => { + const userSyncMessage$ = fromEvent(SendUserSyncMessageEvent).pipe( + tap(async (message) => { + console.log('sendMessage', message) + + peerRoom.sendMessage(message, message.targetPeerId) + }) + ) + return merge(userSyncMessage$).pipe(map(() => null)) + } + }) + domain.effect({ name: 'RoomOnMessageEffect', impl: ({ fromEvent, get }) => { @@ -235,27 +307,44 @@ const RoomDomain = Remesh.domain({ const commandEvent$ = (() => { switch (message.type) { - case 'text': + case SendType.UserSync: { + const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.selfId)! + if (self.joinTime > message.joinTime) { + return EMPTY + } + return of( + UpdateUserListCommand({ type: 'create', user: message }), + messageListDomain.command.CreateItemCommand({ + ...message, + id: nanoid(), + body: `"${message.username}" joined the chat`, + type: MessageType.Prompt, + date: Date.now() + }) + ) + } + case SendType.Text: return of( messageListDomain.command.CreateItemCommand({ ...message, + type: MessageType.Normal, date: Date.now(), likeUsers: [], hateUsers: [] }) ) - case 'like': - case 'hate': { + case SendType.Like: + case SendType.Hate: { if (!get(messageListDomain.query.HasItemQuery(message.id))) { return EMPTY } - const _message = get(messageListDomain.query.ItemQuery(message.id)) - const users = message.type === 'like' ? 'likeUsers' : 'hateUsers' + const _message = get(messageListDomain.query.ItemQuery(message.id)) as NormalMessage + const type = message.type === 'like' ? 'likeUsers' : 'hateUsers' return of( messageListDomain.command.UpdateItemCommand({ ..._message, - [users]: desert( - _message[users], + [type]: desert( + _message[type], { userId: message.userId, username: message.username, @@ -280,12 +369,20 @@ const RoomDomain = Remesh.domain({ domain.effect({ name: 'RoomOnJoinRoomEffect', - impl: ({ fromEvent }) => { + impl: ({ fromEvent, get }) => { const onJoinRoom$ = fromEvent(JoinRoomEvent).pipe( switchMap(() => callbackToObservable(peerRoom.onJoinRoom.bind(peerRoom))), - map((peerId) => { + mergeMap((peerId) => { console.log('onJoinRoom', peerId) - return [UpdatePeerListCommand({ type: 'create', peerId }), OnJoinRoomEvent(peerId)] + const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + return [ + SendUserSyncMessageCommand(peerId), + UpdateUserListCommand({ + type: 'create', + user: { peerId, joinTime: Date.now(), userId, username, userAvatar } + }), + OnJoinRoomEvent(peerId) + ] }) ) return onJoinRoom$ @@ -294,12 +391,27 @@ const RoomDomain = Remesh.domain({ domain.effect({ name: 'RoomOnLeaveRoomEffect', - impl: ({ fromEvent }) => { + impl: ({ fromEvent, get }) => { const onLeaveRoom$ = fromEvent(JoinRoomEvent).pipe( switchMap(() => callbackToObservable(peerRoom.onLeaveRoom.bind(peerRoom))), map((peerId) => { console.log('onLeaveRoom', peerId) - return [UpdatePeerListCommand({ type: 'delete', peerId }), OnLeaveRoomEvent(peerId)] + const user = get(UserListQuery()).find((user) => user.peerId === peerId) + if (user) { + return [ + UpdateUserListCommand({ type: 'delete', user }), + messageListDomain.command.CreateItemCommand({ + ...user, + id: nanoid(), + body: `"${user.username}" left the chat`, + type: MessageType.Prompt, + date: Date.now() + }), + OnLeaveRoomEvent(peerId) + ] + } else { + return [OnLeaveRoomEvent(peerId)] + } }) ) return onLeaveRoom$ @@ -308,20 +420,9 @@ const RoomDomain = Remesh.domain({ return { query: { - PeerListQuery, + UserListQuery, MessageListQuery, - ...RoomStatusState.query - }, - event: { - SendTextMessageEvent, - SendLikeMessageEvent, - SendHateMessageEvent, - JoinRoomEvent, - LeaveRoomEvent, - OnMessageEvent, - OnJoinRoomEvent, - OnLeaveRoomEvent, - ...RoomStatusState.event + ...RoomStatusModule.query }, command: { JoinRoomCommand, @@ -329,7 +430,20 @@ const RoomDomain = Remesh.domain({ SendTextMessageCommand, SendLikeMessageCommand, SendHateMessageCommand, - ...RoomStatusState.command + SendUserSyncMessageCommand, + ...RoomStatusModule.command + }, + event: { + SendTextMessageEvent, + SendLikeMessageEvent, + SendHateMessageEvent, + SendUserSyncMessageEvent, + JoinRoomEvent, + LeaveRoomEvent, + OnMessageEvent, + OnJoinRoomEvent, + OnLeaveRoomEvent, + ...RoomStatusModule.event } } } diff --git a/src/domain/Toast.ts b/src/domain/Toast.ts new file mode 100644 index 0000000..183a6e2 --- /dev/null +++ b/src/domain/Toast.ts @@ -0,0 +1,11 @@ +import { Remesh } from 'remesh' +import ToastModule from './modules/Toast' + +const ToastDomain = Remesh.domain({ + name: 'ToastDomain', + impl: (domain) => { + return ToastModule(domain) + } +}) + +export default ToastDomain diff --git a/src/domain/externs/PeerRoom.ts b/src/domain/externs/PeerRoom.ts index baa5d63..8b28e70 100644 --- a/src/domain/externs/PeerRoom.ts +++ b/src/domain/externs/PeerRoom.ts @@ -6,12 +6,11 @@ export type PeerMessage = object | Blob | ArrayBuffer | ArrayBufferView export interface PeerRoom { readonly selfId: string joinRoom: (roomId: string) => Promise - sendMessage: (message: T) => Promise + sendMessage: (message: T, id?: string) => Promise onMessage: (callback: (message: T) => void) => Promisable leaveRoom: () => Promisable onJoinRoom: (callback: (id: string) => void) => Promisable onLeaveRoom: (callback: (id: string) => void) => Promisable - getRoomPeers: () => string[] } export const PeerRoomExtern = Remesh.extern({ @@ -34,9 +33,6 @@ export const PeerRoomExtern = Remesh.extern({ }, onLeaveRoom: () => { throw new Error('"onLeaveRoom" not implemented.') - }, - getRoomPeers: () => { - throw new Error('"getRoomPeers" not implemented.') } } }) diff --git a/src/domain/externs/Toast.ts b/src/domain/externs/Toast.ts new file mode 100644 index 0000000..1ea08f9 --- /dev/null +++ b/src/domain/externs/Toast.ts @@ -0,0 +1,25 @@ +import { Remesh } from 'remesh' + +export interface Toast { + success: (message: string) => void + error: (message: string) => void + info: (message: string) => void + warning: (message: string) => void +} + +export const ToastExtern = Remesh.extern({ + default: { + success: () => { + throw new Error('"success" not implemented.') + }, + error: () => { + throw new Error('"error" not implemented.') + }, + info: () => { + throw new Error('"info" not implemented.') + }, + warning: () => { + throw new Error('"warning" not implemented.') + } + } +}) diff --git a/src/domain/impls/PeerRoom.ts b/src/domain/impls/PeerRoom.ts index 150a517..972bd9e 100644 --- a/src/domain/impls/PeerRoom.ts +++ b/src/domain/impls/PeerRoom.ts @@ -24,12 +24,12 @@ class PeerRoom { return this.room } - async sendMessage(message: T) { + async sendMessage(message: T, id?: string) { if (!this.room) { throw new Error('Room not joined') } const [send] = this.room!.makeAction('MESSAGE') - return await send(message as DataPayload) + return await send(message as DataPayload, id) } onMessage(callback: (message: T) => void) { @@ -54,13 +54,6 @@ class PeerRoom { this.room.onPeerLeave((peerId) => callback(peerId)) } - getRoomPeers() { - if (!this.room) { - throw new Error('Room not joined') - } - return Object.keys(this.room.getPeers()).map((id) => id) - } - async leaveRoom() { return await this.room?.leave() } diff --git a/src/domain/impls/Toast.ts b/src/domain/impls/Toast.ts new file mode 100644 index 0000000..db6c60d --- /dev/null +++ b/src/domain/impls/Toast.ts @@ -0,0 +1,17 @@ +import { toast } from 'sonner' +import { ToastExtern } from '@/domain/externs/Toast' + +export const ToastImpl = ToastExtern.impl({ + success: (message: string) => { + toast.success(message) + }, + error: (message: string) => { + toast.error(message) + }, + info: (message: string) => { + toast.info(message) + }, + warning: (message: string) => { + toast.warning(message) + } +}) diff --git a/src/domain/modules/Status.ts b/src/domain/modules/Status.ts index f3129a5..f739c62 100644 --- a/src/domain/modules/Status.ts +++ b/src/domain/modules/Status.ts @@ -12,7 +12,7 @@ export interface StatusOptions { } const StatusModule = (domain: RemeshDomainContext, options: StatusOptions) => { - const StatusState = domain.state({ + const RoomStatusModule = domain.state({ name: `${options.name}.StatusState`, default: options.default ?? Status.Initial }) @@ -20,14 +20,14 @@ const StatusModule = (domain: RemeshDomainContext, options: StatusOptions) => { const StatusQuery = domain.query({ name: `${options.name}.StatusQuery`, impl: ({ get }) => { - return get(StatusState()) + return get(RoomStatusModule()) } }) const IsInitialQuery = domain.query({ name: `${options.name}.IsInitialQuery`, impl: ({ get }) => { - const state = get(StatusState()) + const state = get(RoomStatusModule()) return (state & Status.Initial) !== 0 } }) @@ -35,7 +35,7 @@ const StatusModule = (domain: RemeshDomainContext, options: StatusOptions) => { const IsLoadingQuery = domain.query({ name: `${options.name}.IsLoadingQuery`, impl: ({ get }) => { - const state = get(StatusState()) + const state = get(RoomStatusModule()) return (state & Status.Loading) !== 0 } }) @@ -43,7 +43,7 @@ const StatusModule = (domain: RemeshDomainContext, options: StatusOptions) => { const IsFinishedQuery = domain.query({ name: `${options.name}.IsFinishedQuery`, impl: ({ get }) => { - const state = get(StatusState()) + const state = get(RoomStatusModule()) return (state & Status.Finished) !== 0 } }) @@ -55,28 +55,28 @@ const StatusModule = (domain: RemeshDomainContext, options: StatusOptions) => { const SetInitialCommand = domain.command({ name: `${options.name}.SetInitialCommand`, impl: () => { - return [StatusState().new(Status.Initial), UpdateStatusEvent(Status.Initial)] + return [RoomStatusModule().new(Status.Initial), UpdateStatusEvent(Status.Initial)] } }) const SetLoadingCommand = domain.command({ name: `${options.name}.SetLoadingCommand`, impl: () => { - return [StatusState().new(Status.Loading), UpdateStatusEvent(Status.Loading)] + return [RoomStatusModule().new(Status.Loading), UpdateStatusEvent(Status.Loading)] } }) const SetFinishedCommand = domain.command({ name: `${options.name}.SetFinishedCommand`, impl: () => { - return [StatusState().new(Status.Finished), UpdateStatusEvent(Status.Finished)] + return [RoomStatusModule().new(Status.Finished), UpdateStatusEvent(Status.Finished)] } }) const UpdateStatusCommand = domain.command({ name: `${options.name}.UpdateStatusCommand`, impl: (_, status: Status) => { - return [StatusState().new(status), UpdateStatusEvent(status)] + return [RoomStatusModule().new(status), UpdateStatusEvent(status)] } }) diff --git a/src/domain/modules/Toast.ts b/src/domain/modules/Toast.ts index 9c3ce20..b117d84 100644 --- a/src/domain/modules/Toast.ts +++ b/src/domain/modules/Toast.ts @@ -1,11 +1,13 @@ import { type RemeshDomainContext, type DomainConceptName } from 'remesh' -import { toast } from 'sonner' +import { ToastExtern } from '../externs/Toast' export interface ToastOptions { name: DomainConceptName<'ToastModule'> } -export const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => { +const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => { + const toast = domain.getExtern(ToastExtern) + const SuccessEvent = domain.event({ name: `${options.name}.SuccessEvent` }) @@ -69,3 +71,5 @@ export const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = } } } + +export default ToastModule