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
Some checks failed
Build and Release / build-and-release (push) Failing after 0s
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user