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 */}
+
+
+ {/* 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 }) => ;