feat(phase3): forum channels + tags + card UI
This commit is contained in:
@@ -9,6 +9,7 @@ import { MentionDropdown } from "./MentionDropdown";
|
||||
import { MessageSearch } from "./MessageSearch";
|
||||
import { ThreadListPanel } from "./ThreadListPanel.tsx";
|
||||
import { ThreadPanel } from "./ThreadPanel.tsx";
|
||||
import { ForumView } from "./ForumView.tsx";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
@@ -240,12 +241,14 @@ export function ChatArea() {
|
||||
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s flex items-center justify-between">
|
||||
<span>
|
||||
{activeChannel
|
||||
? `# ${activeChannel.name}`
|
||||
? activeChannel.type === 'forum'
|
||||
? `■ ${activeChannel.name}`
|
||||
: `# ${activeChannel.name}`
|
||||
: activeChannelId
|
||||
? `#${activeChannelId}`
|
||||
: "[NO CHANNEL SELECTED]"}
|
||||
</span>
|
||||
{activeChannelId && (
|
||||
{activeChannelId && activeChannel?.type !== 'forum' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowThreads((prev) => !prev)}
|
||||
@@ -278,14 +281,14 @@ export function ChatArea() {
|
||||
onClose={() => setShowSearch(false)}
|
||||
/>
|
||||
)}
|
||||
{showThreads && activeChannelId && (
|
||||
{showThreads && activeChannelId && activeChannel?.type !== 'forum' && (
|
||||
<ThreadListPanel
|
||||
channelId={activeChannelId}
|
||||
onSelect={(t) => setActiveThread(t)}
|
||||
onClose={() => setShowThreads(false)}
|
||||
/>
|
||||
)}
|
||||
{selectMode && activeChannelId && (
|
||||
{selectMode && activeChannelId && activeChannel?.type !== 'forum' && (
|
||||
<div className="px-3 py-1 text-xs font-mono text-gb-fg-f flex items-center justify-between bg-gb-bg-s border-b border-gb-bg-t">
|
||||
<span>{selectedIds.size} selected</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -314,75 +317,81 @@ export function ChatArea() {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-1 font-mono text-sm">
|
||||
{isLoading && <p className="text-gb-fg-f">[loading...]</p>}
|
||||
{!isLoading && messages.length === 0 && (
|
||||
<p className="text-gb-fg-f">[no messages in this channel]</p>
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
onClick={() => selectMode && activeChannelId && toggleSelectedMessage(activeChannelId, message.id)}
|
||||
className={`break-words ${selectMode ? 'cursor-pointer hover:bg-gb-bg-s' : ''} ${selectedIds.has(message.id) ? 'bg-gb-bg-s' : ''}`}
|
||||
>
|
||||
{selectMode && activeChannelId && (
|
||||
<span className="text-gb-fg-f mr-2">
|
||||
{selectedIds.has(message.id) ? '[x]' : '[ ]'}
|
||||
</span>
|
||||
{activeChannel && activeChannel.type === 'forum' && activeChannelId ? (
|
||||
<ForumView channelId={activeChannelId} channelName={activeChannel.name} onOpenThread={(t) => setActiveThread(t)} />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-1 font-mono text-sm">
|
||||
{isLoading && <p className="text-gb-fg-f">[loading...]</p>}
|
||||
{!isLoading && messages.length === 0 && (
|
||||
<p className="text-gb-fg-f">[no messages in this channel]</p>
|
||||
)}
|
||||
<span className="text-gb-fg-f">[{formatTime(message.created_at)}]</span>{" "}
|
||||
<span className="text-gb-aqua"><{message.author_username}></span>{" "}
|
||||
<span className="text-gb-fg">{renderContent(message.content, memberUsernames)}</span>
|
||||
{renderEmbeds(message.embeds)}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
onClick={() => selectMode && activeChannelId && toggleSelectedMessage(activeChannelId, message.id)}
|
||||
className={`break-words ${selectMode ? 'cursor-pointer hover:bg-gb-bg-s' : ''} ${selectedIds.has(message.id) ? 'bg-gb-bg-s' : ''}`}
|
||||
>
|
||||
{selectMode && activeChannelId && (
|
||||
<span className="text-gb-fg-f mr-2">
|
||||
{selectedIds.has(message.id) ? '[x]' : '[ ]'}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gb-fg-f">[{formatTime(message.created_at)}]</span>{" "}
|
||||
<span className="text-gb-aqua"><{message.author_username}></span>{" "}
|
||||
<span className="text-gb-fg">{renderContent(message.content, memberUsernames)}</span>
|
||||
{renderEmbeds(message.embeds)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
{showGifPicker && (
|
||||
<div className="px-3 pb-1">
|
||||
<GiphyPicker onSelect={handleGifSelect} onClose={() => setShowGifPicker(false)} />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="px-3 pb-1">
|
||||
<p className="text-gb-red text-xs font-mono">ERR: {error}</p>
|
||||
</div>
|
||||
)}
|
||||
{slowmodeRemaining > 0 && (
|
||||
<div className="px-3 pt-1 text-xs text-gb-fg-f font-mono">
|
||||
SLOWMODE: wait {slowmodeRemaining}s
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2 relative">
|
||||
<span className="text-gb-fg-f select-none shrink-0">{">"}</span>
|
||||
<div className="flex-1 min-w-0 relative">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="type a message..."
|
||||
className="terminal-input w-full"
|
||||
disabled={!activeChannelId || slowmodeRemaining > 0}
|
||||
/>
|
||||
{mentionQuery !== null && (
|
||||
<MentionDropdown query={mentionQuery} members={members} onSelect={handleMentionSelect} />
|
||||
{showGifPicker && (
|
||||
<div className="px-3 pb-1">
|
||||
<GiphyPicker onSelect={handleGifSelect} onClose={() => setShowGifPicker(false)} />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="px-3 pb-1">
|
||||
<p className="text-gb-red text-xs font-mono">ERR: {error}</p>
|
||||
</div>
|
||||
)}
|
||||
{slowmodeRemaining > 0 && (
|
||||
<div className="px-3 pt-1 text-xs text-gb-fg-f font-mono">
|
||||
SLOWMODE: wait {slowmodeRemaining}s
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2 relative">
|
||||
<span className="text-gb-fg-f select-none shrink-0">{">"}</span>
|
||||
<div className="flex-1 min-w-0 relative">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="type a message..."
|
||||
className="terminal-input w-full"
|
||||
disabled={!activeChannelId || slowmodeRemaining > 0}
|
||||
/>
|
||||
{mentionQuery !== null && (
|
||||
<MentionDropdown query={mentionQuery} members={members} onSelect={handleMentionSelect} />
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGifPicker((prev) => !prev)}
|
||||
disabled={!activeChannelId}
|
||||
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
||||
title="Toggle GIF picker"
|
||||
>
|
||||
[GIF]
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGifPicker((prev) => !prev)}
|
||||
disabled={!activeChannelId}
|
||||
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
||||
title="Toggle GIF picker"
|
||||
>
|
||||
[GIF]
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{activeThread && (
|
||||
<ThreadPanel threadId={activeThread.id} threadName={activeThread.name} onClose={() => setActiveThread(null)} />
|
||||
{activeThread && (
|
||||
<ThreadPanel threadId={activeThread.id} threadName={activeThread.name} onClose={() => setActiveThread(null)} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user