feat: message reactions + link/image previews
Reactions (full stack): - Wire: MsgReaction peer message (reaction_mid, reaction_emoji) - Store: reactions table (mid, emoji, from_peer), SaveReaction, ReactionsForRoom - Daemon: CmdSendReaction IPC command; EvtReaction IPC event; stored reactions replayed to newly-connected IPC clients alongside history - Web UI: reactions state (mid → emoji → [fromId]); hover a message to reveal a + button; click opens a 6-emoji picker (👍❤️😂😮😢🙏); reaction chips appear below the message with count and tooltip; own reactions highlighted Link/image previews: - URLs in message text rendered as clickable <a> links - Image URLs (.jpg/.jpeg/.png/.gif/.webp/blob:/data:image) rendered as inline thumbnails (max 320×200px) below the link Also scoped push notifications architecture in FUTURE.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,8 @@ interface WasteState {
|
||||
fileProgress: Record<string, { peerId: string; name: string; received: number; total: number }>
|
||||
// partial downloads found on daemon startup: sha256 → info
|
||||
resumableFiles: Record<string, { name: string; from: string; size: number; offset: number }>
|
||||
// reactions: mid → emoji → [fromId, ...]
|
||||
reactions: Record<string, Record<string, string[]>>
|
||||
|
||||
// actions
|
||||
connect: (url: string) => void
|
||||
@@ -74,6 +76,7 @@ interface WasteState {
|
||||
rejectOffer: (peerId: string, xid: string) => void
|
||||
cancelTransfer: (peerId: string, xid: string, direction: 'recv' | 'send') => void
|
||||
createRoom: (name: string) => void
|
||||
sendReaction: (networkId: string, mid: string, emoji: string) => void
|
||||
logout: (clearIdentity: boolean) => void
|
||||
handleEvent: (msg: IpcMessage) => void
|
||||
}
|
||||
@@ -101,6 +104,7 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
pendingOffers: {},
|
||||
fileProgress: {},
|
||||
resumableFiles: {},
|
||||
reactions: {},
|
||||
|
||||
connect(url: string) {
|
||||
const adapter = new DaemonAdapter(url)
|
||||
@@ -189,6 +193,10 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
window.location.reload()
|
||||
},
|
||||
|
||||
sendReaction(networkId, mid, emoji) {
|
||||
get().send({ type: 'send_reaction', network_id: networkId, reaction_mid: mid, reaction_emoji: emoji })
|
||||
},
|
||||
|
||||
createRoom(name) {
|
||||
const netId = get().activeNetworkId
|
||||
if (!netId || !name.trim()) return
|
||||
@@ -374,6 +382,19 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
set(s => ({ resumableFiles: { ...s.resumableFiles, ...byHash } }))
|
||||
break
|
||||
}
|
||||
case 'reaction': {
|
||||
const mid = msg.reaction_mid
|
||||
const emoji = msg.reaction_emoji
|
||||
const from = msg.peer_id
|
||||
if (!mid || !emoji || !from) break
|
||||
set(s => {
|
||||
const byEmoji = { ...(s.reactions[mid] ?? {}) }
|
||||
const existing = byEmoji[emoji] ?? []
|
||||
if (existing.includes(from)) return s
|
||||
return { reactions: { ...s.reactions, [mid]: { ...byEmoji, [emoji]: [...existing, from] } } }
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'history_loaded': {
|
||||
const room = msg.room
|
||||
const incoming = (msg.messages ?? []) as ChatMessage[]
|
||||
|
||||
Reference in New Issue
Block a user