From af7142d3d04997d8de0ae5eddf8d99bd210d90d7 Mon Sep 17 00:00:00 2001 From: Nixyan Date: Mon, 12 Jan 2026 14:48:44 -0300 Subject: [PATCH] feat: added mobile style and date handling - Integrated `moment` library for improved date formatting in direct messages. - Refactored timestamp display logic to show relative time for today's messages and full date for older messages. - Made various UI adjustments for better responsiveness and consistency across components for the mobile version. - Updated dependencies in `package.json` and `bun.lock` to include `moment`. --- bun.lock | 3 + package.json | 1 + src/components/home/index.tsx | 7 +- src/components/ui/dm/DmChannelContent.tsx | 48 +-- .../ui/friends/friend-list-item.tsx | 15 +- src/components/ui/friends/friends-page.tsx | 14 +- src/components/ui/layout/channel-list.tsx | 273 +++++++++++------- .../ui/layout/main-content-layout.tsx | 156 +++++++++- src/components/ui/layout/page-header.tsx | 71 +++-- src/components/ui/layout/settings-page.tsx | 2 +- 10 files changed, 420 insertions(+), 170 deletions(-) diff --git a/bun.lock b/bun.lock index 790e5a0..fd532e8 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,7 @@ "dexie-react-hooks": "^4.2.0", "framer-motion": "^12.23.27", "lucide-react": "^0.562.0", + "moment": "^2.30.1", "nanostores": "^1.1.0", "next": "16.1.1", "next-themes": "^0.4.6", @@ -520,6 +521,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], diff --git a/package.json b/package.json index b2a0ff1..484376b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dexie-react-hooks": "^4.2.0", "framer-motion": "^12.23.27", "lucide-react": "^0.562.0", + "moment": "^2.30.1", "nanostores": "^1.1.0", "next": "16.1.1", "next-themes": "^0.4.6", diff --git a/src/components/home/index.tsx b/src/components/home/index.tsx index 69e557d..a59b1dd 100644 --- a/src/components/home/index.tsx +++ b/src/components/home/index.tsx @@ -8,8 +8,7 @@ import { SidebarInset, SidebarMenu, SidebarMenuItem, - SidebarProvider, - SidebarTrigger + SidebarProvider } from "@/components/ui/sidebar"; import { CompassIcon, HouseIcon } from "@phosphor-icons/react"; import { Plus } from "lucide-react"; @@ -85,9 +84,6 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current
-
- -
{ @@ -122,7 +118,6 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current {/* Socket connection status */}
-
{/* Spacer for centering on mobile */}
diff --git a/src/components/ui/dm/DmChannelContent.tsx b/src/components/ui/dm/DmChannelContent.tsx index c8b7125..a99e08f 100644 --- a/src/components/ui/dm/DmChannelContent.tsx +++ b/src/components/ui/dm/DmChannelContent.tsx @@ -3,6 +3,7 @@ import { useSocketContext } from "@/contexts/socket-context"; import { clearUnread, db, sendMessage } from "@/lib/db"; import { useLiveQuery } from "dexie-react-hooks"; import { KeyRound } from "lucide-react"; +import moment from "moment"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Avatar, AvatarFallback, AvatarImage } from "../avatar"; @@ -222,10 +223,13 @@ export default function DMChannelContent(
-
+ {/* Spacer to push messages to the bottom when there are few messages */} +
+ +
{/* Load more indicator */} {hasMoreMessages && (
@@ -244,7 +248,7 @@ export default function DMChannelContent( setIsLoadingMore(false); }, 100); }} - className="text-xs text-muted-foreground hover:text-foreground transition-colors px-3 py-1 rounded-md hover:bg-muted/50" + className="text-xs text-muted-foreground hover:text-foreground transition-colors px-3 py-1 rounded-md hover:bg-muted/50 active:bg-muted/70" > Load more messages @@ -256,7 +260,8 @@ export default function DMChannelContent( const selfDetail = participantDetails.find((p) => p.id === userId); const isSelf = msg.fromUserId === userId; const displayName = isSelf ? selfDetail?.displayUsername ?? selfDetail?.username ?? selfDetail?.name ?? "You" : (sender?.displayUsername ?? sender?.username ?? sender?.name ?? "Unknown"); - const timeLabel = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""; + const timestamp = moment(msg.timestamp); + const timeLabel = timestamp.isSame(moment(), "day") ? timestamp.format("h:mm A") : timestamp.format("MMM D, YYYY h:mm A"); // Check if this message is from the same user as the previous one within 5 minutes const prevMsg = index > 0 ? messages[index - 1] : null; @@ -268,12 +273,12 @@ export default function DMChannelContent( return (
{!isGrouped ? ( // Full message with avatar and header -
- +
+ {displayName.slice(0, 2).toUpperCase()} @@ -281,28 +286,30 @@ export default function DMChannelContent(
-
- +
+ {displayName} - + {timeLabel}
-
+
{msg.content}
) : ( // Compact message without avatar (grouped) -
-
+
+
- {new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} + { + timeLabel + }
-
+
{msg.content}
@@ -317,12 +324,17 @@ export default function DMChannelContent(
{/* Message input */} -
+
setMessageInput(e.target.value)} + disabled={otherUser.status === "offline"} onKeyDown={async (e) => { if (e.key === 'Enter' && !e.shiftKey && messageInput.trim() && password) { e.preventDefault(); diff --git a/src/components/ui/friends/friend-list-item.tsx b/src/components/ui/friends/friend-list-item.tsx index 1474c29..03b4dd4 100644 --- a/src/components/ui/friends/friend-list-item.tsx +++ b/src/components/ui/friends/friend-list-item.tsx @@ -48,7 +48,7 @@ export function FriendListItem({ return (
{ // Call the db to create or get the dm channel getOrCreateDmChannel(userId, { @@ -62,7 +62,7 @@ export function FriendListItem({ }} > {/* Left side: Avatar + Info */} -
+
- {/* Right side: Actions Menu */} -
+ {/* Right side: Actions Menu - always visible on mobile via opacity, hover on desktop */} +
+ +
+ + {/* Divider with label */} +
+
+
+ + )} + + {/* Mobile Navigation Buttons */} + {isMobile && ( +
-
- -
+ )} {/* Channel List */} -
+
{page === "friends" || !currentChannel ? (
-
- + {/* DM Header */} +
+ Direct Messages
+ {openDmChannels.length > 0 ? ( - openDmChannels.map((channel) => { - const isActive = dmChannel?.id === channel.id - const lastMessage = channel.times?.lastMessage - const lastMessageTime = channel.times?.lastMessageAt - const channelUnreadCount = unreadCount?.find((unread) => unread.channelId === channel.id)?.count ?? 0 - if (!channel.isOpen) return null; +
+ {openDmChannels.map((channel) => { + const participantDetails = dmChannel?.participantDetails.find((p) => p.id === channel.participants[0]) + const isActive = dmChannel?.id === channel.id + const lastMessage = channel.times?.lastMessage + const lastMessageTime = channel.times?.lastMessageAt + const channelUnreadCount = unreadCount?.find((unread) => unread.channelId === channel.id)?.count ?? 0 + if (!channel.isOpen) return null; - return ( -
{ - clearUnread(channel.id) - console.log("Cleared unread count for channel", channel.id) - router.push(`/channels/me/${channel.id}`) - }} - > -
- - {channelUnreadCount > 0 && ( - - {channelUnreadCount > 99 ? '99+' : channelUnreadCount} - - )} -
- - {/* Channel Info */} -
-
- - {channel.name} - - {lastMessageTime && ( - - {formatDistanceToNow(lastMessageTime, { addSuffix: false })} + return ( +
{ + clearUnread(channel.id) + console.log("Cleared unread count for channel", channel.id) + handleNavigation(`/channels/me/${channel.id}`) + }} + > +
+ + {channelUnreadCount > 0 && ( + + {channelUnreadCount > 99 ? '99+' : channelUnreadCount} )}
- {lastMessage && ( - - {lastMessage.content} - - )} + + {/* Channel Info */} +
+
+ + {channel.name} + + {lastMessageTime && ( + + {formatDistanceToNow(lastMessageTime, { addSuffix: false })} + + )} +
+ {lastMessage && ( + + {lastMessage.content} + + )} +
+ + {/* Close button - always visible on mobile, hover-visible on desktop */} +
- - {/* Close button */} - -
- ) - }) + ) + })} +
) : ( -
- +
+
+ +
+ {emptyMessage}
)}
) : ( -
- No channels +
+ No channels
)}
diff --git a/src/components/ui/layout/main-content-layout.tsx b/src/components/ui/layout/main-content-layout.tsx index f61577d..5a0eb92 100644 --- a/src/components/ui/layout/main-content-layout.tsx +++ b/src/components/ui/layout/main-content-layout.tsx @@ -1,9 +1,16 @@ "use client" import FriendRequestModal from "@/components/home/modals/friendRequest" +import { Button } from "@/components/ui/button" +import LogoIcon from "@/components/ui/logo-icon" +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet" +import { useIsMobile } from "@/hooks/use-mobile" import { db } from "@/lib/db" +import { cn } from "@/lib/utils" +import { CompassIcon } from "@phosphor-icons/react" import { useQuery } from "convex/react" import { useLiveQuery } from "dexie-react-hooks" +import { Plus } from "lucide-react" import * as React from "react" import { useEffect, useMemo } from "react" import { api } from "../../../../convex/_generated/api" @@ -39,6 +46,8 @@ export function MainContentLayout({ const [friendsPage, setFriendsPage] = React.useState<"all" | "available">("all") const [friendModal, setFriendModal] = React.useState(false) const [currentChannel] = React.useState(null) + const [mobileChannelListOpen, setMobileChannelListOpen] = React.useState(false) + const isMobile = useIsMobile() // Use useLiveQuery to reactively fetch channels - automatically updates when DB changes const openDmChannels = useLiveQuery( @@ -70,6 +79,7 @@ export function MainContentLayout({ displayUsername: participant.displayUsername ?? "", image: participant.image ?? "", status: participant.status, + isCurrentUser: participant.id === userId, })) } }, [openDmChannels, dmChannelId, getParticipantDetails]) @@ -85,6 +95,34 @@ export function MainContentLayout({ } }, [dmChannelId, serverChannelId]); + // Close mobile channel list when navigating to a channel + const handlePageChange = React.useCallback((newPage: "friends" | "support" | "dm" | "server") => { + setPage(newPage); + if (isMobile) { + setMobileChannelListOpen(false); + } + }, [isMobile]); + + // Close mobile sheet when a DM channel is selected + const handleMobileChannelSelect = React.useCallback(() => { + if (isMobile) { + setMobileChannelListOpen(false); + } + }, [isMobile]); + + const channelListContent = ( + + ); + return ( <>
@@ -98,19 +136,70 @@ export function MainContentLayout({ dmChannel={dmChannel} serverId={serverId} serverChannelId={serverChannelId} + onToggleMobileChannelList={() => setMobileChannelListOpen(true)} + isMobile={isMobile} /> {/* Content Area - Channel List + Main Content */}
+ {/* Desktop: Always visible channel list */} +
+ {channelListContent} +
- + {/* Mobile: Sheet-based channel list - Discord-style two-panel layout */} + {isMobile && ( + + + + Channels + Navigate between channels and DMs + +
+ {/* Left Rail - Server/Home Icons (Discord-style) */} +
+ {/* Home/DMs Button */} + + + + + {/* Divider */} +
+ + {/* Discover */} + + + + + {/* Future: Server icons will go here */} + {/* Placeholder for servers */} + + {/* Add Server Button */} + + + +
+ + {/* Right Panel - Channel List */} +
+ {/* Panel Header */} +
+ Direct Messages +
+ + {/* Channel List Content */} +
+ {channelListContent} +
+
+
+ + + )} {/* Main Content */}
@@ -151,4 +240,55 @@ export function MainContentLayout({ /> ) +} + +// Discord-style mobile server icon component +function MobileServerIcon({ + children, + isActive, + isHome, + isAddButton, + label, + onClick +}: { + children: React.ReactNode + isActive?: boolean + isHome?: boolean + isAddButton?: boolean + label?: string + onClick?: () => void +}) { + return ( +
+ {/* Left pill indicator */} +
+ + {/* Icon button */} + +
+ ) } \ No newline at end of file diff --git a/src/components/ui/layout/page-header.tsx b/src/components/ui/layout/page-header.tsx index db7073f..26ebca1 100644 --- a/src/components/ui/layout/page-header.tsx +++ b/src/components/ui/layout/page-header.tsx @@ -1,7 +1,7 @@ "use client" import { Button } from "@/components/ui/button" -import { PhoneIcon, SearchIcon, UserIcon, UsersIcon, VideoIcon } from "lucide-react" +import { MenuIcon, PhoneIcon, SearchIcon, UserIcon, UserPlusIcon, UsersIcon, VideoIcon } from "lucide-react" import UserCard from "../user/user-card" export interface PageHeaderProps { @@ -19,10 +19,13 @@ export interface PageHeaderProps { displayUsername: string image: string status: "online" | "busy" | "offline" | "away" + isCurrentUser: boolean }[] } serverId?: string serverChannelId?: string + onToggleMobileChannelList?: () => void + isMobile?: boolean } export function PageHeader({ @@ -34,11 +37,28 @@ export function PageHeader({ dmChannel, serverId, serverChannelId, + onToggleMobileChannelList, + isMobile, }: PageHeaderProps) { + + const otherParticipant = dmChannel && dmChannel.participantDetails.find((p) => !p.isCurrentUser) + return ( -
- {/* SCS or DM Selector */} -
+
+ {/* Mobile: Menu toggle button */} + {isMobile && ( + + )} + + {/* Desktop: SCS or DM Selector */} +
{!currentChannel || currentChannel.type === "DM" ? (
) : serverChannelId ? ( -
+
#{serverChannelId}
) : page === "friends" ? ( -
-
+
+
- Friends + Friends
- -
+ +
diff --git a/src/components/ui/layout/settings-page.tsx b/src/components/ui/layout/settings-page.tsx index 2a0b578..f4dc10c 100644 --- a/src/components/ui/layout/settings-page.tsx +++ b/src/components/ui/layout/settings-page.tsx @@ -8,7 +8,7 @@ export interface SettingsPageProps { export function SettingsPage({}: SettingsPageProps) { return ( -
+
Servers