Merge branch 'develop'

This commit is contained in:
molvqingtai 2024-11-09 22:44:01 +08:00
commit 4f9c135fb9
18 changed files with 269 additions and 115 deletions

View file

@ -14,7 +14,7 @@ import { Toaster } from 'sonner'
import DanmakuContainer from './components/DanmakuContainer' import DanmakuContainer from './components/DanmakuContainer'
import DanmakuDomain from '@/domain/Danmaku' import DanmakuDomain from '@/domain/Danmaku'
import AppStatusDomain from '@/domain/AppStatus' import AppStatusDomain from '@/domain/AppStatus'
import { cn } from '@/utils' import { checkDarkMode, cn } from '@/utils'
/** /**
* Fix requestAnimationFrame error in jest * Fix requestAnimationFrame error in jest
@ -52,8 +52,6 @@ export default function App() {
} }
}, [userInfoSetFinished, messageListLoadFinished]) }, [userInfoSetFinished, messageListLoadFinished])
const danmakuContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
danmakuIsEnabled && send(danmakuDomain.command.MountCommand(danmakuContainerRef.current!)) danmakuIsEnabled && send(danmakuDomain.command.MountCommand(danmakuContainerRef.current!))
return () => { return () => {
@ -61,20 +59,41 @@ export default function App() {
} }
}, [danmakuIsEnabled]) }, [danmakuIsEnabled])
return ( const themeMode =
appStatusLoadIsFinished && ( userInfo?.themeMode === 'system'
<div id="app" className={cn('contents', userInfo?.themeMode)}> ? checkDarkMode()
<AppMain> ? 'dark'
<Header /> : 'light'
<Main /> : (userInfo?.themeMode ?? (checkDarkMode() ? 'dark' : 'light'))
<Footer />
{notUserInfo && <Setup></Setup>}
<Toaster richColors offset="70px" visibleToasts={1} position="top-center"></Toaster>
</AppMain>
<AppButton></AppButton>
<DanmakuContainer ref={danmakuContainerRef} /> const danmakuContainerRef = useRef<HTMLDivElement>(null)
</div>
) return (
<div id="app" className={cn('contents', themeMode)}>
{appStatusLoadIsFinished && (
<>
<AppMain>
<Header />
<Main />
<Footer />
{notUserInfo && <Setup></Setup>}
<Toaster
richColors
theme={themeMode}
offset="70px"
visibleToasts={1}
toastOptions={{
classNames: {
toast: 'dark:bg-slate-950 border dark:border-slate-600'
}
}}
position="top-center"
></Toaster>
</AppMain>
<AppButton></AppButton>
</>
)}
<DanmakuContainer ref={danmakuContainerRef} />
</div>
) )
} }

View file

@ -13,10 +13,10 @@ import { NotificationImpl } from '@/domain/impls/Notification'
import { ToastImpl } from '@/domain/impls/Toast' import { ToastImpl } from '@/domain/impls/Toast'
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom' // import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
import { PeerRoomImpl } from '@/domain/impls/PeerRoom2' import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
import '@/assets/styles/tailwind.css'
// Remove import after merging: https://github.com/emilkowalski/sonner/pull/508 // Remove import after merging: https://github.com/emilkowalski/sonner/pull/508
import '@/assets/styles/sonner.css' import '@/assets/styles/sonner.css'
import '@/assets/styles/overlay.css' import '@/assets/styles/overlay.css'
import '@/assets/styles/tailwind.css'
import NotificationDomain from '@/domain/Notification' import NotificationDomain from '@/domain/Notification'
import { createElement } from '@/utils' import { createElement } from '@/utils'

View file

@ -7,7 +7,7 @@ import { Button } from '@/components/ui/Button'
import { EVENT } from '@/constants/event' import { EVENT } from '@/constants/event'
import UserInfoDomain from '@/domain/UserInfo' import UserInfoDomain from '@/domain/UserInfo'
import useTriggerAway from '@/hooks/useTriggerAway' import useTriggerAway from '@/hooks/useTriggerAway'
import { checkSystemDarkMode, cn } from '@/utils' import { checkDarkMode, cn } from '@/utils'
import LogoIcon0 from '@/assets/images/logo-0.svg' import LogoIcon0 from '@/assets/images/logo-0.svg'
import LogoIcon1 from '@/assets/images/logo-1.svg' import LogoIcon1 from '@/assets/images/logo-1.svg'
import LogoIcon2 from '@/assets/images/logo-2.svg' import LogoIcon2 from '@/assets/images/logo-2.svg'
@ -36,8 +36,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())] const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())]
const isDarkMode = const isDarkMode = userInfo?.themeMode === 'dark' ? true : userInfo?.themeMode === 'light' ? false : checkDarkMode()
userInfo?.themeMode === 'dark' ? true : userInfo?.themeMode === 'light' ? false : checkSystemDarkMode()
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
@ -107,7 +106,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
<Button <Button
onClick={handleSwitchTheme} onClick={handleSwitchTheme}
variant="outline" variant="outline"
className="relative size-10 overflow-hidden rounded-full p-0 shadow" className="relative size-10 overflow-hidden rounded-full p-0 shadow dark:border-slate-600"
> >
<div <div
className={cn( className={cn(
@ -121,10 +120,18 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
</div> </div>
</Button> </Button>
<Button onClick={handleOpenOptionsPage} variant="outline" className="size-10 rounded-full p-0 shadow"> <Button
onClick={handleOpenOptionsPage}
variant="outline"
className="size-10 rounded-full p-0 shadow dark:border-slate-600"
>
<SettingsIcon size={20} /> <SettingsIcon size={20} />
</Button> </Button>
<Button ref={appButtonRef} variant="outline" className="size-10 cursor-grab rounded-full p-0 shadow"> <Button
ref={appButtonRef}
variant="outline"
className="size-10 cursor-grab rounded-full p-0 shadow dark:border-slate-600"
>
<HandIcon size={20} /> <HandIcon size={20} />
</Button> </Button>
</motion.div> </motion.div>

View file

@ -2,7 +2,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { MAX_AVATAR_SIZE } from '@/constants/config' import { MAX_AVATAR_SIZE } from '@/constants/config'
import MessageListDomain, { Message, MessageType } from '@/domain/MessageList' import MessageListDomain, { Message, MessageType } from '@/domain/MessageList'
import UserInfoDomain, { UserInfo } from '@/domain/UserInfo' import UserInfoDomain, { UserInfo } from '@/domain/UserInfo'
import { checkSystemDarkMode, generateRandomAvatar, generateRandomName } from '@/utils' import { generateRandomAvatar, generateRandomName } from '@/utils'
import { UserIcon } from 'lucide-react' import { UserIcon } from 'lucide-react'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
@ -39,7 +39,7 @@ const generateUserInfo = async (): Promise<UserInfo> => {
name: generateRandomName(), name: generateRandomName(),
avatar: await generateRandomAvatar(MAX_AVATAR_SIZE), avatar: await generateRandomAvatar(MAX_AVATAR_SIZE),
createTime: Date.now(), createTime: Date.now(),
themeMode: checkSystemDarkMode() ? 'dark' : 'system', themeMode: 'system',
danmakuEnabled: true, danmakuEnabled: true,
notificationEnabled: true, notificationEnabled: true,
notificationType: 'all' notificationType: 'all'

View file

@ -16,7 +16,15 @@ function App() {
<VersionLink></VersionLink> <VersionLink></VersionLink>
<Main> <Main>
<ProfileForm></ProfileForm> <ProfileForm></ProfileForm>
<Toaster richColors position="top-center" /> <Toaster
richColors
position="top-center"
toastOptions={{
classNames: {
toast: 'dark:bg-slate-950 border dark:border-slate-600'
}
}}
/>
</Main> </Main>
<BadgeList></BadgeList> <BadgeList></BadgeList>
</Layout> </Layout>

View file

@ -9,7 +9,7 @@ import { Button } from '@/components/ui/Button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
import { Input } from '@/components/ui/Input' import { Input } from '@/components/ui/Input'
import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo' import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo'
import { checkSystemDarkMode, cn, generateRandomAvatar } from '@/utils' import { cn, generateRandomAvatar } from '@/utils'
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup' import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
import { Label } from '@/components/ui/Label' import { Label } from '@/components/ui/Label'
import { RefreshCcwIcon } from 'lucide-react' import { RefreshCcwIcon } from 'lucide-react'
@ -24,7 +24,7 @@ const defaultUserInfo: UserInfo = {
name: '', name: '',
avatar: '', avatar: '',
createTime: Date.now(), createTime: Date.now(),
themeMode: checkSystemDarkMode() ? 'dark' : 'system', themeMode: 'system',
danmakuEnabled: true, danmakuEnabled: true,
notificationEnabled: true, notificationEnabled: true,
notificationType: 'all' notificationType: 'all'

View file

@ -40,7 +40,7 @@ const AppStatusDomain = Remesh.domain({
}) })
const StatusState = domain.state<AppStatus>({ const StatusState = domain.state<AppStatus>({
name: 'AppStatus.OpenState', name: 'AppStatus.StatusState',
default: defaultStatusState default: defaultStatusState
}) })

View file

@ -58,7 +58,7 @@ export interface TextMessage extends MessageUser {
export type RoomMessage = SyncUserMessage | SyncHistoryMessage | LikeMessage | HateMessage | TextMessage export type RoomMessage = SyncUserMessage | SyncHistoryMessage | LikeMessage | HateMessage | TextMessage
export type RoomUser = MessageUser & { peerId: string; joinTime: number } export type RoomUser = MessageUser & { peerIds: string[]; joinTime: number }
const MessageUserSchema = { const MessageUserSchema = {
userId: v.string(), userId: v.string(),
@ -165,7 +165,7 @@ const RoomDomain = Remesh.domain({
const SelfUserQuery = domain.query({ const SelfUserQuery = domain.query({
name: 'Room.SelfUserQuery', name: 'Room.SelfUserQuery',
impl: ({ get }) => { impl: ({ get }) => {
return get(UserListQuery()).find((user) => user.peerId === get(PeerIdQuery()))! return get(UserListQuery()).find((user) => user.peerIds.includes(peerRoom.peerId))!
} }
}) })
@ -185,9 +185,7 @@ const RoomDomain = Remesh.domain({
const JoinRoomCommand = domain.command({ const JoinRoomCommand = domain.command({
name: 'Room.JoinRoomCommand', name: 'Room.JoinRoomCommand',
impl: ({ get }) => { impl: ({ get }) => {
peerRoom.joinRoom()
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
return [ return [
UpdateUserListCommand({ UpdateUserListCommand({
type: 'create', type: 'create',
@ -204,15 +202,20 @@ const RoomDomain = Remesh.domain({
receiveTime: Date.now() receiveTime: Date.now()
}), }),
JoinStatusModule.command.SetFinishedCommand(), JoinStatusModule.command.SetFinishedCommand(),
JoinRoomEvent(peerRoom.roomId) JoinRoomEvent(peerRoom.roomId),
SelfJoinRoomEvent(peerRoom.roomId)
] ]
} }
}) })
JoinRoomCommand.after(() => {
peerRoom.joinRoom()
return null
})
const LeaveRoomCommand = domain.command({ const LeaveRoomCommand = domain.command({
name: 'Room.LeaveRoomCommand', name: 'Room.LeaveRoomCommand',
impl: ({ get }) => { impl: ({ get }) => {
peerRoom.leaveRoom()
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
return [ return [
messageListDomain.command.CreateItemCommand({ messageListDomain.command.CreateItemCommand({
@ -230,11 +233,17 @@ const RoomDomain = Remesh.domain({
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar } user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
}), }),
JoinStatusModule.command.SetInitialCommand(), JoinStatusModule.command.SetInitialCommand(),
LeaveRoomEvent(peerRoom.roomId) LeaveRoomEvent(peerRoom.roomId),
SelfLeaveRoomEvent(peerRoom.roomId)
] ]
} }
}) })
LeaveRoomCommand.after(() => {
peerRoom.leaveRoom()
return null
})
const SendTextMessageCommand = domain.command({ const SendTextMessageCommand = domain.command({
name: 'Room.SendTextMessageCommand', name: 'Room.SendTextMessageCommand',
impl: ({ get }, message: string | { body: string; atUsers: AtUser[] }) => { impl: ({ get }, message: string | { body: string; atUsers: AtUser[] }) => {
@ -314,6 +323,7 @@ const RoomDomain = Remesh.domain({
const syncUserMessage: SyncUserMessage = { const syncUserMessage: SyncUserMessage = {
...self, ...self,
id: nanoid(), id: nanoid(),
peerId: peerRoom.peerId,
sendTime: Date.now(), sendTime: Date.now(),
lastMessageTime, lastMessageTime,
type: SendType.SyncUser type: SendType.SyncUser
@ -393,12 +403,32 @@ const RoomDomain = Remesh.domain({
const UpdateUserListCommand = domain.command({ const UpdateUserListCommand = domain.command({
name: 'Room.UpdateUserListCommand', name: 'Room.UpdateUserListCommand',
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => { impl: ({ get }, action: { type: 'create' | 'delete'; user: Omit<RoomUser, 'peerIds'> & { peerId: string } }) => {
const userList = get(UserListState()) const userList = get(UserListState())
const existUser = userList.find((user) => user.userId === action.user.userId)
if (action.type === 'create') { if (action.type === 'create') {
return [UserListState().new(upsert(userList, action.user, 'userId'))] return [
UserListState().new(
upsert(
userList,
{ ...action.user, peerIds: [...(existUser?.peerIds || []), action.user.peerId] },
'userId'
)
)
]
} else { } else {
return [UserListState().new(userList.filter(({ userId }) => userId !== action.user.userId))] return [
UserListState().new(
upsert(
userList,
{
...action.user,
peerIds: existUser?.peerIds?.filter((peerId) => peerId !== action.user.peerId) || []
},
'userId'
).filter((user) => user.peerIds.length)
)
]
} }
} }
}) })
@ -443,10 +473,18 @@ const RoomDomain = Remesh.domain({
name: 'Room.OnJoinRoomEvent' name: 'Room.OnJoinRoomEvent'
}) })
const SelfJoinRoomEvent = domain.event<string>({
name: 'Room.SelfJoinRoomEvent'
})
const OnLeaveRoomEvent = domain.event<string>({ const OnLeaveRoomEvent = domain.event<string>({
name: 'Room.OnLeaveRoomEvent' name: 'Room.OnLeaveRoomEvent'
}) })
const SelfLeaveRoomEvent = domain.event<string>({
name: 'Room.SelfLeaveRoomEvent'
})
const OnErrorEvent = domain.event<Error>({ const OnErrorEvent = domain.event<Error>({
name: 'Room.OnErrorEvent' name: 'Room.OnErrorEvent'
}) })
@ -486,37 +524,33 @@ const RoomDomain = Remesh.domain({
const messageCommand$ = (() => { const messageCommand$ = (() => {
switch (message.type) { switch (message.type) {
case SendType.SyncUser: { case SendType.SyncUser: {
const userList = get(UserListQuery())
const selfUser = get(SelfUserQuery()) const selfUser = get(SelfUserQuery())
// If the browser has multiple tabs open, it can cause the same user to join multiple times with the same peerId but different userId
const isRepeatJoin = userList.some((user) => user.userId === message.userId) // If a new user joins after the current user has entered the room, a join log message needs to be created.
// When a new user joins, it triggers join events for all users, i.e., newUser join event and oldUser join event const existUser = get(UserListQuery()).find((user) => user.userId === message.userId)
// Use joinTime to determine if it's a new user const isNewJoinUser = !existUser && message.joinTime > selfUser.joinTime
const isNewJoinEvent = selfUser.joinTime < message.joinTime
const lastMessageTime = get(LastMessageTimeQuery()) const lastMessageTime = get(LastMessageTimeQuery())
const needSyncHistory = lastMessageTime > message.lastMessageTime const needSyncHistory = lastMessageTime > message.lastMessageTime
return isRepeatJoin return of(
? EMPTY UpdateUserListCommand({ type: 'create', user: message }),
: of( isNewJoinUser
UpdateUserListCommand({ type: 'create', user: message }), ? messageListDomain.command.CreateItemCommand({
isNewJoinEvent ...message,
? messageListDomain.command.CreateItemCommand({ id: nanoid(),
...message, body: `"${message.username}" joined the chat`,
id: nanoid(), type: MessageType.Prompt,
body: `"${message.username}" joined the chat`, receiveTime: Date.now()
type: MessageType.Prompt, })
receiveTime: Date.now() : null,
}) needSyncHistory
: null, ? SendSyncHistoryMessageCommand({
needSyncHistory peerId: message.peerId,
? SendSyncHistoryMessageCommand({ lastMessageTime: message.lastMessageTime
peerId: message.peerId, })
lastMessageTime: message.lastMessageTime : null
}) )
: null
)
} }
case SendType.SyncHistory: { case SendType.SyncHistory: {
@ -574,20 +608,26 @@ const RoomDomain = Remesh.domain({
impl: ({ get }) => { impl: ({ get }) => {
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe( const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
map((peerId) => { map((peerId) => {
if (get(JoinStatusModule.query.IsInitialQuery())) {
return null
}
// console.log('onLeaveRoom', peerId) // console.log('onLeaveRoom', peerId)
const user = get(UserListQuery()).find((user) => user.peerId === peerId)
if (user) { const existUser = get(UserListQuery()).find((user) => user.peerIds.includes(peerId))
if (existUser) {
return [ return [
UpdateUserListCommand({ type: 'delete', user }), UpdateUserListCommand({ type: 'delete', user: { ...existUser, peerId } }),
messageListDomain.command.CreateItemCommand({ existUser.peerIds.length === 1
...user, ? messageListDomain.command.CreateItemCommand({
id: nanoid(), ...existUser,
body: `"${user.username}" left the chat`, id: nanoid(),
type: MessageType.Prompt, body: `"${existUser.username}" left the chat`,
sendTime: Date.now(), type: MessageType.Prompt,
receiveTime: Date.now() sendTime: Date.now(),
}), receiveTime: Date.now()
})
: null,
OnLeaveRoomEvent(peerId) OnLeaveRoomEvent(peerId)
] ]
} else { } else {
@ -612,7 +652,6 @@ const RoomDomain = Remesh.domain({
} }
}) })
// TODO: Move this to a service worker in the future, so we don't need to send a leave room message every time the page refreshes
domain.effect({ domain.effect({
name: 'Room.OnUnloadEffect', name: 'Room.OnUnloadEffect',
impl: ({ get }) => { impl: ({ get }) => {
@ -647,7 +686,9 @@ const RoomDomain = Remesh.domain({
SendSyncUserMessageEvent, SendSyncUserMessageEvent,
SendSyncHistoryMessageEvent, SendSyncHistoryMessageEvent,
JoinRoomEvent, JoinRoomEvent,
SelfJoinRoomEvent,
LeaveRoomEvent, LeaveRoomEvent,
SelfLeaveRoomEvent,
OnMessageEvent, OnMessageEvent,
OnTextMessageEvent, OnTextMessageEvent,
OnJoinRoomEvent, OnJoinRoomEvent,

View file

@ -8,6 +8,18 @@ const ToastDomain = Remesh.domain({
impl: (domain) => { impl: (domain) => {
const roomDomain = domain.getDomain(RoomDomain()) const roomDomain = domain.getDomain(RoomDomain())
const toastModule = ToastModule(domain) const toastModule = ToastModule(domain)
domain.effect({
name: 'Toast.OnRoomSelfJoinRoomEffect',
impl: ({ fromEvent }) => {
const onRoomJoin$ = fromEvent(roomDomain.event.SelfJoinRoomEvent).pipe(
map(() => toastModule.command.LoadingCommand({ message: 'Connected to the chat.', duration: 3000 }))
)
return onRoomJoin$
}
})
domain.effect({ domain.effect({
name: 'Toast.OnRoomErrorEffect', name: 'Toast.OnRoomErrorEffect',
impl: ({ fromEvent }) => { impl: ({ fromEvent }) => {

View file

@ -5,7 +5,7 @@ export interface PeerRoom {
readonly peerId: string readonly peerId: string
readonly roomId: string readonly roomId: string
joinRoom: () => PeerRoom joinRoom: () => PeerRoom
sendMessage: (message: RoomMessage, id?: string) => PeerRoom sendMessage: (message: RoomMessage, id?: string | string[]) => PeerRoom
onMessage: (callback: (message: RoomMessage) => void) => PeerRoom onMessage: (callback: (message: RoomMessage) => void) => PeerRoom
leaveRoom: () => PeerRoom leaveRoom: () => PeerRoom
onJoinRoom: (callback: (id: string) => void) => PeerRoom onJoinRoom: (callback: (id: string) => void) => PeerRoom

View file

@ -1,10 +1,12 @@
import { Remesh } from 'remesh' import { Remesh } from 'remesh'
export interface Toast { export interface Toast {
success: (message: string) => void success: (message: string, duration?: number) => number | string
error: (message: string) => void error: (message: string, duration?: number) => number | string
info: (message: string) => void info: (message: string, duration?: number) => number | string
warning: (message: string) => void warning: (message: string, duration?: number) => number | string
loading: (message: string, duration?: number) => number | string
cancel: (id: number | string) => number | string
} }
export const ToastExtern = Remesh.extern<Toast>({ export const ToastExtern = Remesh.extern<Toast>({
@ -20,6 +22,12 @@ export const ToastExtern = Remesh.extern<Toast>({
}, },
warning: () => { warning: () => {
throw new Error('"warning" not implemented.') throw new Error('"warning" not implemented.')
},
loading: () => {
throw new Error('"loading" not implemented.')
},
cancel: () => {
throw new Error('"cancel" not implemented.')
} }
} }
}) })

View file

@ -45,7 +45,7 @@ class PeerRoom extends EventHub {
return this return this
} }
sendMessage(message: RoomMessage, id?: string) { sendMessage(message: RoomMessage, id?: string | string[]) {
if (!this.room) { if (!this.room) {
this.once('action', () => { this.once('action', () => {
if (!this.room) { if (!this.room) {

View file

@ -46,7 +46,7 @@ class PeerRoom extends EventHub {
return this return this
} }
sendMessage(message: RoomMessage, id?: string) { sendMessage(message: RoomMessage, id?: string | string[]) {
if (!this.room) { if (!this.room) {
this.once('action', () => { this.once('action', () => {
if (!this.room) { if (!this.room) {

View file

@ -2,16 +2,24 @@ import { toast } from 'sonner'
import { ToastExtern } from '@/domain/externs/Toast' import { ToastExtern } from '@/domain/externs/Toast'
export const ToastImpl = ToastExtern.impl({ export const ToastImpl = ToastExtern.impl({
success: (message: string) => { success: (message: string, duration: number = 4000) => {
toast.success(message) return toast.success(message, { duration })
}, },
error: (message: string) => { error: (message: string, duration: number = 4000) => {
toast.error(message) return toast.error(message, { duration })
}, },
info: (message: string) => { info: (message: string, duration: number = 4000) => {
toast.info(message) return toast.info(message, { duration })
}, },
warning: (message: string) => { warning: (message: string, duration: number = 4000) => {
toast.warning(message) return toast.warning(message, { duration })
},
loading: (message: string, duration: number = 4000) => {
const id = toast.loading(message, { duration })
setTimeout(() => toast.dismiss(id), duration)
return id
},
cancel: (id: number | string) => {
return toast.dismiss(id)
} }
}) })

View file

@ -8,51 +8,90 @@ export interface ToastOptions {
const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => { const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => {
const toast = domain.getExtern(ToastExtern) const toast = domain.getExtern(ToastExtern)
const SuccessEvent = domain.event({ const SuccessEvent = domain.event<number | string>({
name: `${options.name}.SuccessEvent` name: `${options.name}.SuccessEvent`
}) })
const SuccessCommand = domain.command({ const SuccessCommand = domain.command({
name: `${options.name}.SuccessCommand`, name: `${options.name}.SuccessCommand`,
impl: (_, message: string) => { impl: (_, message: string | { message: string; duration?: number }) => {
toast.success(message) const id = toast.success(
return [SuccessEvent()] typeof message === 'string' ? message : message.message,
typeof message === 'string' ? undefined : message.duration
)
return [SuccessEvent(id)]
} }
}) })
const ErrorEvent = domain.event({ const ErrorEvent = domain.event<number | string>({
name: `${options.name}.ErrorEvent` name: `${options.name}.ErrorEvent`
}) })
const ErrorCommand = domain.command({ const ErrorCommand = domain.command({
name: `${options.name}.ErrorCommand`, name: `${options.name}.ErrorCommand`,
impl: (_, message: string) => { impl: (_, message: string | { message: string; duration?: number }) => {
toast.error(message) const id = toast.error(
return [ErrorEvent()] typeof message === 'string' ? message : message.message,
typeof message === 'string' ? undefined : message.duration
)
return [ErrorEvent(id)]
} }
}) })
const InfoEvent = domain.event({ const InfoEvent = domain.event<number | string>({
name: `${options.name}.InfoEvent` name: `${options.name}.InfoEvent`
}) })
const InfoCommand = domain.command({ const InfoCommand = domain.command({
name: `${options.name}.InfoCommand`, name: `${options.name}.InfoCommand`,
impl: (_, message: string) => { impl: (_, message: string | { message: string; duration?: number }) => {
toast.info(message) const id = toast.info(
return [InfoEvent()] typeof message === 'string' ? message : message.message,
typeof message === 'string' ? undefined : message.duration
)
return [InfoEvent(id)]
} }
}) })
const WarningEvent = domain.event({ const WarningEvent = domain.event<number | string>({
name: `${options.name}.WarningEvent` name: `${options.name}.WarningEvent`
}) })
const WarningCommand = domain.command({ const WarningCommand = domain.command({
name: `${options.name}.WarningCommand`, name: `${options.name}.WarningCommand`,
impl: (_, message: string) => { impl: (_, message: string | { message: string; duration?: number }) => {
toast.warning(message) const id = toast.warning(
return [WarningEvent()] typeof message === 'string' ? message : message.message,
typeof message === 'string' ? undefined : message.duration
)
return [WarningEvent(id)]
}
})
const LoadingEvent = domain.event<number | string>({
name: `${options.name}.LoadingEvent`
})
const LoadingCommand = domain.command({
name: `${options.name}.LoadingCommand`,
impl: (_, message: string | { message: string; duration?: number }) => {
const id = toast.loading(
typeof message === 'string' ? message : message.message,
typeof message === 'string' ? undefined : message.duration
)
return [LoadingEvent(id)]
}
})
const CancelEvent = domain.event<number | string>({
name: `${options.name}.CancelEvent`
})
const CancelCommand = domain.command({
name: `${options.name}.CancelCommand`,
impl: (_, id: number | string) => {
toast.cancel(id)
return [CancelEvent(id)]
} }
}) })
@ -61,13 +100,17 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
SuccessEvent, SuccessEvent,
ErrorEvent, ErrorEvent,
InfoEvent, InfoEvent,
WarningEvent WarningEvent,
LoadingEvent,
CancelEvent
}, },
command: { command: {
SuccessCommand, SuccessCommand,
ErrorCommand, ErrorCommand,
InfoCommand, InfoCommand,
WarningCommand WarningCommand,
LoadingCommand,
CancelCommand
} }
} }
} }

View file

@ -0,0 +1,11 @@
const checkDarkMode = () => {
const colorScheme = document.documentElement.style.getPropertyValue('color-scheme').trim()
if (colorScheme === 'dark') {
return true // Prefer the website's color-scheme property value
}
return window.matchMedia('(prefers-color-scheme: dark)').matches // Otherwise, check the system theme
}
export default checkDarkMode

View file

@ -1,3 +0,0 @@
const checkSystemDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches
export default checkSystemDarkMode

View file

@ -4,7 +4,7 @@ export { default as createElement } from './createElement'
export { default as getSiteInfo } from './getSiteInfo' export { default as getSiteInfo } from './getSiteInfo'
export { default as compressImage } from './compressImage' export { default as compressImage } from './compressImage'
export { default as isNullish } from './isNullish' export { default as isNullish } from './isNullish'
export { default as checkSystemDarkMode } from './checkSystemDarkMode' export { default as checkDarkMode } from './checkDarkMode'
export { default as stringToHex } from './stringToHex' export { default as stringToHex } from './stringToHex'
export { default as debounce } from './debounce' export { default as debounce } from './debounce'
export { default as throttle } from './throttle' export { default as throttle } from './throttle'