feat: Implement core chat application UI, including chat, voice, members, DMs, and shared components.
Some checks failed
Build and Release / build-and-release (push) Failing after 0s

This commit is contained in:
Bryan1029384756
2026-02-14 01:57:15 -06:00
parent 6f12f98d30
commit 958cf56b23
51 changed files with 4761 additions and 1858 deletions

View File

@@ -13,9 +13,11 @@ import { useToasts } from '../components/Toast';
import { PresenceProvider } from '../contexts/PresenceContext';
import { getUserPref, setUserPref } from '../utils/userPreferences';
import { usePlatform } from '../platform';
import { useIsMobile } from '../hooks/useIsMobile';
const Chat = () => {
const { crypto, settings } = usePlatform();
const isMobile = useIsMobile();
const [userId, setUserId] = useState(() => localStorage.getItem('userId'));
const [username, setUsername] = useState(() => localStorage.getItem('username') || '');
const [view, setView] = useState(() => {
@@ -27,6 +29,7 @@ const Chat = () => {
const [activeDMChannel, setActiveDMChannel] = useState(null);
const [showMembers, setShowMembers] = useState(true);
const [showPinned, setShowPinned] = useState(false);
const [mobileView, setMobileView] = useState('sidebar');
const convex = useConvex();
const { toasts, addToast, removeToast, ToastContainer } = useToasts();
@@ -156,15 +159,21 @@ const Chat = () => {
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
setView('me');
if (isMobile) setMobileView('chat');
} catch (err) {
console.error('Error opening DM:', err);
}
}, [convex]);
}, [convex, isMobile]);
const handleSelectChannel = useCallback((channelId) => {
setActiveChannel(channelId);
setShowPinned(false);
if (isMobile) setMobileView('chat');
}, [isMobile]);
const handleMobileBack = useCallback(() => {
setMobileView('sidebar');
}, []);
const activeChannelObj = channels.find(c => c._id === activeChannel);
@@ -173,6 +182,7 @@ const Chat = () => {
const isDMView = view === 'me' && activeDMChannel;
const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice';
const currentChannelId = isDMView ? activeDMChannel.channel_id : activeChannel;
const effectiveShowMembers = isMobile ? false : showMembers;
// PiP: show when watching a stream and NOT currently viewing the voice channel in VoiceStage
const isViewingVoiceStage = view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId;
@@ -196,6 +206,8 @@ const Chat = () => {
onToggleMembers={() => {}}
showMembers={false}
onTogglePinned={() => setShowPinned(p => !p)}
isMobile={isMobile}
onMobileBack={handleMobileBack}
/>
<div className="chat-content">
<ChatArea
@@ -215,12 +227,43 @@ const Chat = () => {
</div>
);
}
return <FriendsView onOpenDM={openDM} />;
return (
<>
{isMobile && (
<div className="chat-header" style={{ position: 'sticky', top: 0, zIndex: 10 }}>
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<span className="chat-header-name">Friends</span>
</div>
</div>
)}
<FriendsView onOpenDM={openDM} />
</>
);
}
if (activeChannel) {
if (activeChannelObj?.type === 'voice') {
return <VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />;
return (
<div className="chat-container">
{isMobile && (
<div className="chat-header">
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<svg width="20" height="20" viewBox="0 0 24 24" style={{ color: 'var(--text-muted)', marginRight: 4 }}>
<path fill="currentColor" d="M11.383 3.07904C11.009 2.92504 10.579 3.01004 10.293 3.29604L6.586 7.00304H3C2.45 7.00304 2 7.45304 2 8.00304V16.003C2 16.553 2.45 17.003 3 17.003H6.586L10.293 20.71C10.579 20.996 11.009 21.082 11.383 20.927C11.757 20.772 12 20.407 12 20.003V4.00304C12 3.59904 11.757 3.23404 11.383 3.07904Z" />
</svg>
<span className="chat-header-name">{activeChannelObj?.name}</span>
</div>
</div>
)}
<VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />
</div>
);
}
return (
<div className="chat-container">
@@ -229,9 +272,11 @@ const Chat = () => {
channelType="text"
channelTopic={activeChannelObj?.topic}
onToggleMembers={() => setShowMembers(!showMembers)}
showMembers={showMembers}
showMembers={effectiveShowMembers}
onTogglePinned={() => setShowPinned(p => !p)}
serverName={serverName}
isMobile={isMobile}
onMobileBack={handleMobileBack}
/>
<div className="chat-content">
<ChatArea
@@ -241,7 +286,7 @@ const Chat = () => {
channelKey={channelKeys[activeChannel]}
username={username}
userId={userId}
showMembers={showMembers}
showMembers={effectiveShowMembers}
onToggleMembers={() => setShowMembers(!showMembers)}
onOpenDM={openDM}
showPinned={showPinned}
@@ -249,7 +294,7 @@ const Chat = () => {
/>
<MembersList
channelId={activeChannel}
visible={showMembers}
visible={effectiveShowMembers}
onMemberClick={(member) => {}}
/>
</div>
@@ -266,6 +311,16 @@ const Chat = () => {
);
}
const handleSetActiveDMChannel = useCallback((dm) => {
setActiveDMChannel(dm);
if (isMobile && dm) setMobileView('chat');
}, [isMobile]);
const handleViewChange = useCallback((newView) => {
setView(newView);
if (isMobile) setMobileView('sidebar');
}, [isMobile]);
if (!userId) {
return (
<div className="app-container">
@@ -276,27 +331,33 @@ const Chat = () => {
);
}
const showSidebar = !isMobile || mobileView === 'sidebar';
const showMainContent = !isMobile || mobileView === 'chat';
return (
<PresenceProvider userId={userId}>
<div className="app-container">
<Sidebar
channels={channels}
categories={categories}
activeChannel={activeChannel}
onSelectChannel={handleSelectChannel}
username={username}
channelKeys={channelKeys}
view={view}
onViewChange={setView}
onOpenDM={openDM}
activeDMChannel={activeDMChannel}
setActiveDMChannel={setActiveDMChannel}
dmChannels={dmChannels}
userId={userId}
serverName={serverName}
serverIconUrl={serverIconUrl}
/>
{renderMainContent()}
<div className={`app-container${isMobile ? ' is-mobile' : ''}`}>
{showSidebar && (
<Sidebar
channels={channels}
categories={categories}
activeChannel={activeChannel}
onSelectChannel={handleSelectChannel}
username={username}
channelKeys={channelKeys}
view={view}
onViewChange={handleViewChange}
onOpenDM={openDM}
activeDMChannel={activeDMChannel}
setActiveDMChannel={handleSetActiveDMChannel}
dmChannels={dmChannels}
userId={userId}
serverName={serverName}
serverIconUrl={serverIconUrl}
isMobile={isMobile}
/>
)}
{showMainContent && renderMainContent()}
{showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />}
<ToastContainer />
</div>