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 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}
|
||||||
|
|||||||
@@ -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
@@ -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': {
|
||||||
|
|||||||
Reference in New Issue
Block a user