fix: DM message ordering, consolidate input toolbar, add rich text/WYSIWYG, file upload with drag-drop
- Fix DM backend ListMessages to use DESC + reverse (match channel handler) - Remove spurious .reverse() from frontend message/conversation stores - Create shared MessageInput component with Slack-style single toolbar row - Add file upload via + button with progress bar and drag-and-drop - Add markdown/rich text toggle with full WYSIWYG block formatting (lists, blockquotes, links, headings, code blocks) - Add frontend+backend security for file uploads (extension + content-type guards)
This commit is contained in:
@@ -3,6 +3,9 @@ import { useThreadStore } from '../stores/thread.ts';
|
||||
import { useMessageStore } from '../stores/message.ts';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { ExpandableImage } from './ExpandableImage.tsx';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPaperPlane } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface ThreadPanelProps {
|
||||
threadId: string;
|
||||
@@ -22,7 +25,7 @@ export function ThreadPanel({ threadId, threadName, onClose }: ThreadPanelProps)
|
||||
const addMessage = useThreadStore((s) => s.addThreadMessage);
|
||||
const removeMessage = useMessageStore((s) => s.removeMessage);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages(threadId);
|
||||
@@ -71,7 +74,13 @@ export function ThreadPanel({ threadId, threadName, onClose }: ThreadPanelProps)
|
||||
<span className="text-gb-fg-f">[{formatTime(m.created_at)}]</span>{' '}
|
||||
<span className="text-gb-aqua"><{m.author_username}></span>{' '}
|
||||
<span className="text-gb-fg">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={{ p: ({ ...props }) => <span {...props} className="inline" /> }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ ...props }) => <span {...props} className="inline" />,
|
||||
img: ({ src, alt, ...props }) => <ExpandableImage src={src} alt={alt} {...props} />,
|
||||
}}
|
||||
>
|
||||
{m.content}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
@@ -79,9 +88,27 @@ export function ThreadPanel({ threadId, threadName, onClose }: ThreadPanelProps)
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2">
|
||||
<span className="text-gb-fg-f select-none shrink-0">{'>'}</span>
|
||||
<input ref={inputRef} type="text" placeholder="reply..." className="terminal-input w-full" />
|
||||
<form onSubmit={handleSubmit} className="p-3">
|
||||
<div className="flex items-end gap-1 bg-gb-bg-s terminal-border px-2 py-1.5">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
(e.currentTarget.form as HTMLFormElement | null)?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder="Reply..."
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent outline-none border-none resize-none overflow-y-auto max-h-20 text-sm py-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-gb-aqua hover:text-gb-orange p-1.5 shrink-0 disabled:opacity-40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPaperPlane} className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user