From 4ef4db87819abe560f46ae707f69da8967033197 Mon Sep 17 00:00:00 2001 From: Alex Erdei Date: Wed, 7 May 2025 17:22:06 +0100 Subject: [PATCH] Make messaging work --- src/backend/backend.js | 117 +++++++++++++----- .../incomingMessages/incomingMessagesSlice.js | 60 +++++++-- .../outgoingMessages/outgoingMessagesSlice.js | 53 ++++++-- 3 files changed, 181 insertions(+), 49 deletions(-) diff --git a/src/backend/backend.js b/src/backend/backend.js index 283b132..7a5270c 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -315,8 +315,6 @@ function openSocket() { /* ---------------------------------------------------- */ - const parseMsg = (raw) => (typeof raw === "string" ? JSON.parse(raw) : raw); - /* helper – does the row belong to me? */ const isMe = (row) => row && row.user_id === localStorage.getItem(LS_USER_ID); @@ -334,11 +332,11 @@ function openSocket() { /* inside openSocket() – unchanged except the helper */ hub.on("fakebook.users.post", (raw) => { - store.dispatch(usersUpdated([parseMsg(raw)])); + store.dispatch(usersUpdated([JSON.parse(raw)])); }); hub.on("fakebook.users.put", (raw) => { - const row = parseMsg(raw); + const row = JSON.parse(raw); store.dispatch(usersUpdated([row])); @@ -352,6 +350,22 @@ function openSocket() { hub.on("fakebook.posts.put", (raw) => { store.dispatch(postsUpdated([JSON.parse(raw)])); }); + + /* helper – my uid so we filter only my messages */ + + const myUID = () => localStorage.getItem(LS_USER_ID); + + /* ---------- new message created -------------------------------- */ + + hub.on("fakebook.message.post", (raw) => { + const msg = mapRestMessage(JSON.parse(raw)); // ← reuse the mapper + + if (msg.recipient === myUID()) + store.dispatch(incomingMessagesUpdated([msg])); + + if (msg.sender === myUID()) + store.dispatch(outgoingMessagesUpdated([msg])); + }); }) .catch((err) => { @@ -839,45 +853,86 @@ export function subscribeMessages(kind = "incoming") { cancelled = true; }; } -export function uploadMessage(msg) { - DB.messages.push({ ...msg, id: genId(), timestamp: nowISO(), isRead: false }); - persist(); - return Promise.resolve(); -} -export function updateToBeRead(id) { - const m = DB.messages.find((m) => m.id === id); - if (m) { - m.isRead = true; - persist(); - } - return Promise.resolve(); -} +/* -------------------------------------------------------------- -/* Ensure demo DB has at least one user */ + uploadMessage(msg) -if (!DB.users.length) { - DB.users.push({ - userID: genId(), - firstname: "Demo", + msg = { recipient, text, photoURL? } - lastname: "User", + RETURNS { id } // new message_id - profilePictureURL: "fakebook-avatar.jpeg", + -------------------------------------------------------------- */ - backgroundPictureURL: "background-server.jpg", +export async function uploadMessage(msg) { + const sender = localStorage.getItem(LS_USER_ID); - photos: [], + if (!sender) throw new Error("Not authenticated"); - posts: [], + const message_id = genId(); // Magic table PK - isOnline: 0, + const body = { + message_id, - isEmailVerified: true, + sender, // who sends - index: 0, + recipient: msg.recipient, // who receives + + text: msg.text ?? "", + + photoURL: msg.photoURL ?? "", + + isPhoto: msg.photoURL ? 1 : 0, + + isRead: 0, // unread on insert + + /* timestamp is generated by the server */ + }; + + await $fetch(`${API_BASE}/message`, { + method: "POST", + + body: JSON.stringify(body), }); - persist(); + /* No local dispatch necessary – the hub’s `message.post` event + + will arrive in a few ms and update Redux for both parties. */ + + return { id: message_id }; +} + +/* ------------------------------------------------------------------ + + updateToBeRead(message_id) + + – flips the flag in Redux immediately (optimistic) + + – then notifies the server; no need to wait for a hub echo + +------------------------------------------------------------------ */ + +export async function updateToBeRead(message_id) { + /* 1️⃣ optimistic local patch (affects whichever slice holds the row) */ + + const patch = [{ message_id, isRead: 1 }]; + + store.dispatch(incomingMessagesUpdated(patch)); + + store.dispatch(outgoingMessagesUpdated(patch)); + + /* 2️⃣ tell the server – ignore the hub echo, we no longer listen */ + + try { + await $fetch(`${API_BASE}/message`, { + method: "PUT", + + body: JSON.stringify({ message_id, isRead: 1 }), + }); + } catch (err) { + console.warn("[updateToBeRead] PUT failed:", err.message); + + // Optional: revert optimistic change on error + } } diff --git a/src/features/incomingMessages/incomingMessagesSlice.js b/src/features/incomingMessages/incomingMessagesSlice.js index 5cfdfda..216997b 100644 --- a/src/features/incomingMessages/incomingMessagesSlice.js +++ b/src/features/incomingMessages/incomingMessagesSlice.js @@ -1,15 +1,57 @@ import { createSlice } from "@reduxjs/toolkit"; +/* Helper to keep the array ordered (oldest → newest). + + If you don’t care about ordering you can remove the sort. */ + +const byTimestampAsc = (a, b) => + new Date(a.timestamp).valueOf() - new Date(b.timestamp).valueOf(); + export const incomingMessagesSlice = createSlice({ - name: "incomingMessages", - initialState: [], - reducers: { - incomingMessagesUpdated: (state, action) => { - const updatedState = []; - action.payload.forEach((message) => updatedState.push(message)); - return updatedState; - }, - }, + name: "incomingMessages", + + /* simple array of message objects */ + + initialState: [], + + reducers: { + /* ------------------------------------------------------------------ + + incomingMessagesUpdated + + – payload is ALWAYS an array (can be 1 or many rows) + + – for every row: if new → append + + if exist → merge/patch + + ------------------------------------------------------------------ */ + + incomingMessagesUpdated: (state, action) => { + action.payload.forEach((msg) => { + /* support both id and message_id just in case */ + + const msgId = msg.id ?? msg.message_id; + + const idx = state.findIndex((m) => (m.id ?? m.message_id) === msgId); + + if (idx === -1) { + /* ① brand-new message → push */ + + state.push(msg); + } else { + /* ② already stored → shallow merge keeps other fields */ + state[idx] = { ...state[idx], ...msg }; + } + }); + + /* keep messages sorted for deterministic rendering */ + + state.sort(byTimestampAsc); + + /* Immer lets us “mutate” state directly; no return needed */ + }, + }, }); export const { incomingMessagesUpdated } = incomingMessagesSlice.actions; diff --git a/src/features/outgoingMessages/outgoingMessagesSlice.js b/src/features/outgoingMessages/outgoingMessagesSlice.js index 8338ccb..c672dcc 100644 --- a/src/features/outgoingMessages/outgoingMessagesSlice.js +++ b/src/features/outgoingMessages/outgoingMessagesSlice.js @@ -1,15 +1,50 @@ import { createSlice } from "@reduxjs/toolkit"; +/* Optional: keep the list ordered (oldest → newest). */ + +const byTimestampAsc = (a, b) => + new Date(a.timestamp).valueOf() - new Date(b.timestamp).valueOf(); + export const outgoingMessagesSlice = createSlice({ - name: "outgoingMessages", - initialState: [], - reducers: { - outgoingMessagesUpdated: (state, action) => { - const updatedState = []; - action.payload.forEach((message) => updatedState.push(message)); - return updatedState; - }, - }, + name: "outgoingMessages", + + initialState: [], // simple array of message objects + + reducers: { + /* ------------------------------------------------------------------ + + outgoingMessagesUpdated + + – payload is ALWAYS an array (1 or many rows) + + – merge-in logic keeps existing rows and patches them if needed + + ------------------------------------------------------------------ */ + + outgoingMessagesUpdated: (state, action) => { + action.payload.forEach((msg) => { + const msgId = msg.id ?? msg.message_id; // tolerate either key + + const idx = state.findIndex((m) => (m.id ?? m.message_id) === msgId); + + if (idx === -1) { + /* ① brand-new row → append */ + + state.push(msg); + } else { + /* ② existing row → shallow merge (e.g. read flag flips) */ + + state[idx] = { ...state[idx], ...msg }; + } + }); + + /* Keep deterministic order for rendering (remove if unnecessary). */ + + state.sort(byTimestampAsc); + + /* Immer lets us mutate in place; no return statement required */ + }, + }, }); export const { outgoingMessagesUpdated } = outgoingMessagesSlice.actions;