fix(dm): add image paste support to DM chat and fix realtime DM updates

This commit is contained in:
2026-07-06 13:44:41 +00:00
parent 0c26e2402b
commit c2fb3165f4
3 changed files with 127 additions and 31 deletions
+53
View File
@@ -149,6 +149,58 @@ export function DMChat() {
} }
}; };
const handlePaste = async (e: React.ClipboardEvent<HTMLInputElement>) => {
const items = e.clipboardData.items;
let imageFile: File | null = null;
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
imageFile = items[i].getAsFile();
break;
}
}
if (imageFile) {
e.preventDefault();
const formData = new FormData();
formData.append('file', imageFile);
try {
const res = await fetch('/api/v1/upload', {
method: 'POST',
body: formData,
credentials: 'include'
});
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
const imageUrl = data.url;
// Insert into input
const imgMarkdown = `![image](${imageUrl})`;
const inputEl = inputRef.current;
if (inputEl) {
const start = inputEl.selectionStart || 0;
const end = inputEl.selectionEnd || 0;
const newValue = input.slice(0, start) + imgMarkdown + input.slice(end);
setInput(newValue);
setTimeout(() => {
inputEl.focus();
inputEl.setSelectionRange(start + imgMarkdown.length, start + imgMarkdown.length);
}, 0);
} else {
setInput(prev => prev + (prev.length > 0 && !prev.endsWith(' ') ? ' ' : '') + imgMarkdown);
}
} catch (err) {
console.error('Failed to upload image:', err);
}
}
};
const selfDM = conversation && conversation.members.length === 1 && conversation.members[0].id === currentUser?.id; const selfDM = conversation && conversation.members.length === 1 && conversation.members[0].id === currentUser?.id;
const title = selfDM const title = selfDM
? "Notes" ? "Notes"
@@ -215,6 +267,7 @@ export function DMChat() {
} }
} }
}} }}
onPaste={handlePaste}
placeholder="type a message..." placeholder="type a message..."
className="terminal-input w-full" className="terminal-input w-full"
disabled={!id} disabled={!id}
+25 -1
View File
@@ -42,6 +42,8 @@ interface ConversationState {
fetchOlderMessages: (conversationId: string) => Promise<void>; fetchOlderMessages: (conversationId: string) => Promise<void>;
sendMessage: (conversationId: string, content: string) => Promise<ConversationMessage>; sendMessage: (conversationId: string, content: string) => Promise<ConversationMessage>;
addMessage: (message: ConversationMessage) => void; addMessage: (message: ConversationMessage) => void;
updateMessage: (message: ConversationMessage) => void;
deleteMessage: (conversationId: string, messageId: string) => void;
} }
export const useConversationStore = create<ConversationState>((set, get) => ({ export const useConversationStore = create<ConversationState>((set, get) => ({
@@ -151,8 +153,30 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
[message.conversation_id]: [ [message.conversation_id]: [
...(state.messagesByConversation[message.conversation_id] || []), ...(state.messagesByConversation[message.conversation_id] || []),
message, message,
], ].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()),
}, },
})); }));
}, },
updateMessage: (message) => {
set((state) => {
const convMsgs = state.messagesByConversation[message.conversation_id] || [];
return {
messagesByConversation: {
...state.messagesByConversation,
[message.conversation_id]: convMsgs.map((m) => (m.id === message.id ? message : m)),
},
};
});
},
deleteMessage: (conversationId, messageId) => {
set((state) => {
const convMsgs = state.messagesByConversation[conversationId] || [];
return {
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: convMsgs.filter((m) => m.id !== messageId),
},
};
});
},
})); }));
+25 -6
View File
@@ -6,7 +6,9 @@ import { usePresenceStore } from './presence.ts';
import { useTypingStore } from './typing.ts'; import { useTypingStore } from './typing.ts';
import { useVoiceStore } from './voice.ts'; import { useVoiceStore } from './voice.ts';
import { useAuthStore } from './auth.ts'; import { useAuthStore } from './auth.ts';
import { useConversationStore } from './conversation.ts';
import type { Message } from './message.ts'; import type { Message } from './message.ts';
import type { ConversationMessage } from './conversation.ts';
import type { Channel } from './channel.ts'; import type { Channel } from './channel.ts';
import type { Server } from './server.ts'; import type { Server } from './server.ts';
import type { UserStatus } from './auth.ts'; import type { UserStatus } from './auth.ts';
@@ -47,12 +49,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null; return typeof value === 'object' && value !== null;
} }
function extractIds(payload: UnknownPayload | undefined): { channel_id: string; message_id: string } | null { function extractIds(payload: UnknownPayload | undefined): { channel_id?: string; conversation_id?: string; message_id: string } | null {
if (!payload || typeof payload.channel_id !== 'string' || typeof payload.message_id !== 'string') { if (!payload || typeof payload.message_id !== 'string') return null;
if (typeof payload.channel_id === 'string') return { channel_id: payload.channel_id, message_id: payload.message_id };
if (typeof payload.conversation_id === 'string') return { conversation_id: payload.conversation_id, message_id: payload.message_id };
return null; return null;
} }
return { channel_id: payload.channel_id, message_id: payload.message_id };
}
export const useWebSocketStore = create<WebSocketState>((set, get) => ({ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
socket: null, socket: null,
@@ -127,6 +129,10 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
switch (data.type) { switch (data.type) {
case 'MESSAGE_CREATE': { case 'MESSAGE_CREATE': {
if (isRecord(payload)) { if (isRecord(payload)) {
if (payload.conversation_id) {
const msg = payload as unknown as ConversationMessage;
useConversationStore.getState().addMessage(msg);
} else {
const msg = payload as unknown as Message; const msg = payload as unknown as Message;
addMessage(msg); addMessage(msg);
@@ -154,15 +160,28 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
} }
} }
} }
}
break; break;
} }
case 'MESSAGE_UPDATE': { case 'MESSAGE_UPDATE': {
if (isRecord(payload)) updateMessage(payload as unknown as Message); if (isRecord(payload)) {
if (payload.conversation_id) {
useConversationStore.getState().updateMessage(payload as unknown as ConversationMessage);
} else {
updateMessage(payload as unknown as Message);
}
}
break; break;
} }
case 'MESSAGE_DELETE': { case 'MESSAGE_DELETE': {
const ids = extractIds(payload); const ids = extractIds(payload);
if (ids) removeMessage(ids.channel_id, ids.message_id); if (ids) {
if (ids.conversation_id) {
useConversationStore.getState().deleteMessage(ids.conversation_id, ids.message_id);
} else if (ids.channel_id) {
removeMessage(ids.channel_id, ids.message_id);
}
}
break; break;
} }
case 'REACTION_ADD': { case 'REACTION_ADD': {