fix(dm): add image paste support to DM chat and fix realtime DM updates
This commit is contained in:
@@ -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 = ``;
|
||||
|
||||
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 title = selfDM
|
||||
? "Notes"
|
||||
@@ -215,6 +267,7 @@ export function DMChat() {
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
placeholder="type a message..."
|
||||
className="terminal-input w-full"
|
||||
disabled={!id}
|
||||
|
||||
@@ -42,6 +42,8 @@ interface ConversationState {
|
||||
fetchOlderMessages: (conversationId: string) => Promise<void>;
|
||||
sendMessage: (conversationId: string, content: string) => Promise<ConversationMessage>;
|
||||
addMessage: (message: ConversationMessage) => void;
|
||||
updateMessage: (message: ConversationMessage) => void;
|
||||
deleteMessage: (conversationId: string, messageId: string) => void;
|
||||
}
|
||||
|
||||
export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
@@ -151,8 +153,30 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
[message.conversation_id]: [
|
||||
...(state.messagesByConversation[message.conversation_id] || []),
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
+49
-30
@@ -6,7 +6,9 @@ import { usePresenceStore } from './presence.ts';
|
||||
import { useTypingStore } from './typing.ts';
|
||||
import { useVoiceStore } from './voice.ts';
|
||||
import { useAuthStore } from './auth.ts';
|
||||
import { useConversationStore } from './conversation.ts';
|
||||
import type { Message } from './message.ts';
|
||||
import type { ConversationMessage } from './conversation.ts';
|
||||
import type { Channel } from './channel.ts';
|
||||
import type { Server } from './server.ts';
|
||||
import type { UserStatus } from './auth.ts';
|
||||
@@ -47,11 +49,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function extractIds(payload: UnknownPayload | undefined): { channel_id: string; message_id: string } | null {
|
||||
if (!payload || typeof payload.channel_id !== 'string' || typeof payload.message_id !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return { channel_id: payload.channel_id, message_id: payload.message_id };
|
||||
function extractIds(payload: UnknownPayload | undefined): { channel_id?: string; conversation_id?: string; message_id: string } | null {
|
||||
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;
|
||||
}
|
||||
|
||||
export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
@@ -127,42 +129,59 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
switch (data.type) {
|
||||
case 'MESSAGE_CREATE': {
|
||||
if (isRecord(payload)) {
|
||||
const msg = payload as unknown as Message;
|
||||
addMessage(msg);
|
||||
|
||||
// Desktop notification
|
||||
const currentUserId = useAuthStore.getState().user?.id;
|
||||
if (msg.author_id !== currentUserId && !document.hasFocus()) {
|
||||
// Update browser tab title
|
||||
unreadCount++;
|
||||
if (document.title.match(/^\(\d+\)\s/)) {
|
||||
document.title = document.title.replace(/^\(\d+\)\s/, `(${unreadCount}) `);
|
||||
} else {
|
||||
document.title = `(${unreadCount}) ` + document.title;
|
||||
}
|
||||
if (payload.conversation_id) {
|
||||
const msg = payload as unknown as ConversationMessage;
|
||||
useConversationStore.getState().addMessage(msg);
|
||||
} else {
|
||||
const msg = payload as unknown as Message;
|
||||
addMessage(msg);
|
||||
|
||||
// Desktop notification
|
||||
const currentUserId = useAuthStore.getState().user?.id;
|
||||
if (msg.author_id !== currentUserId && !document.hasFocus()) {
|
||||
// Update browser tab title
|
||||
unreadCount++;
|
||||
if (document.title.match(/^\(\d+\)\s/)) {
|
||||
document.title = document.title.replace(/^\(\d+\)\s/, `(${unreadCount}) `);
|
||||
} else {
|
||||
document.title = `(${unreadCount}) ` + document.title;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
const title = msg.author_username ? `New message from ${msg.author_display_name || msg.author_username}` : 'New message';
|
||||
const notification = new Notification(title, {
|
||||
body: msg.content,
|
||||
icon: '/icons/icon-192.png',
|
||||
});
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
};
|
||||
if (Notification.permission === 'granted') {
|
||||
const title = msg.author_username ? `New message from ${msg.author_display_name || msg.author_username}` : 'New message';
|
||||
const notification = new Notification(title, {
|
||||
body: msg.content,
|
||||
icon: '/icons/icon-192.png',
|
||||
});
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
case 'MESSAGE_DELETE': {
|
||||
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;
|
||||
}
|
||||
case 'REACTION_ADD': {
|
||||
|
||||
Reference in New Issue
Block a user