diff --git a/packages/web/src/components/layout/MobileDmsScreen.tsx b/packages/web/src/components/layout/MobileDmsScreen.tsx new file mode 100644 index 00000000..412b32a3 --- /dev/null +++ b/packages/web/src/components/layout/MobileDmsScreen.tsx @@ -0,0 +1,200 @@ +import React, { useMemo } from 'react'; +import { useUIStore } from '../../stores/uiStore'; +import { useSpaceStore } from '../../stores/spaceStore'; +import { useChatStore } from '../../stores/chatStore'; +import { useAuthStore } from '../../stores/authStore'; +import { useSocialStore } from '../../stores/socialStore'; +import { Avatar } from '../ui/Avatar'; +import { resolveAssetUrl } from '../../utils/assetUrls'; +import { useNavigate } from 'react-router-dom'; + +export function MobileDmsScreen() { + const pushMobileScreen = useUIStore((s) => s.pushMobileScreen); + const openModal = useUIStore((s) => s.openModal); + + const dmChannels = useSpaceStore((s) => s.dmChannels); + const readStates = useChatStore((s) => s.readStates); + const authUser = useAuthStore((s) => s.user); + const friends = useSocialStore((s) => s.friends); + const navigate = useNavigate(); + + // Online friends for the activity row + const onlineFriends = useMemo(() => + friends.filter(f => f.status === 'online' || f.status === 'idle' || f.status === 'dnd'), + [friends] + ); + + // Sort DMs by last message time (newest first) + const sortedDms = useMemo(() => + [...dmChannels].sort((a, b) => { + const aTime = a.lastMessage?.createdAt ?? a.createdAt; + const bTime = b.lastMessage?.createdAt ?? b.createdAt; + return bTime - aTime; + }), + [dmChannels] + ); + + const handleDmTap = (dmId: string) => { + navigate(`/channels/@me/${dmId}`); + pushMobileScreen('channel-chat', { channelId: dmId, spaceId: '@me' }); + }; + + const formatTimestamp = (ts: number): string => { + const now = Date.now(); + const diff = now - ts; + if (diff < 60_000) return 'now'; + if (diff < 3600_000) return `${Math.floor(diff / 60_000)}m`; + if (diff < 86400_000) return `${Math.floor(diff / 3600_000)}h`; + if (diff < 604800_000) return `${Math.floor(diff / 86400_000)}d`; + return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + }; + + return ( +
+ {/* Header */} +
+

Messages

+
+ +
+
+ + {/* Activity row — online friends */} + {onlineFriends.length > 0 && ( +
+
+ {onlineFriends.map(friend => { + const avatarUrl = friend.avatar + ? resolveAssetUrl(friend.avatar, friend._instanceOrigin) ?? `/api/uploads/${friend.avatar}` + : null; + return ( + + ); + })} +
+
+ )} + + {/* DM list */} +
+ {sortedDms.map(dm => { + const otherMembers = dm.members.filter(m => m.id !== authUser?.id); + const isGroup = dm.members.length > 2; + const name = isGroup + ? otherMembers.map(m => m.displayName ?? m.username).join(', ') + : otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? 'Unknown'; + + const lastMsgId = dm.lastMessage?.id; + const readState = readStates.get(dm.id); + const isUnread = lastMsgId && (!readState || readState < lastMsgId); + + const preview = dm.lastMessage?.content; + const previewTime = dm.lastMessage?.createdAt; + const previewSender = dm.lastMessage + ? dm.members.find(m => m.id === dm.lastMessage!.userId) + : null; + + const mainUser = otherMembers[0]; + const avatarUrl = mainUser?.avatar + ? `/api/uploads/${mainUser.avatar}` + : null; + + return ( + + ); + })} + + {sortedDms.length === 0 && ( +
+ No conversations yet +
+ )} +
+ + {/* FAB — New DM */} + +
+ ); +} diff --git a/packages/web/src/components/layout/MobileShell.tsx b/packages/web/src/components/layout/MobileShell.tsx index 919d4e05..9cfddd31 100644 --- a/packages/web/src/components/layout/MobileShell.tsx +++ b/packages/web/src/components/layout/MobileShell.tsx @@ -16,7 +16,7 @@ const PlaceholderScreen = ({ label }: { label: string }) => ( import { MobileSpacesScreen } from './MobileSpacesScreen'; // These will be replaced with real imports as each task is completed: -const MobileDmsScreen = () => ; +import { MobileDmsScreen } from './MobileDmsScreen'; const MobileYouScreen = () => ; import { MobileChatScreen } from './MobileChatScreen'; const MobileSettingsScreen = ({ initialPanel: _p }: { initialPanel?: string }) => ;