diff --git a/package-lock.json b/package-lock.json index b9e468d..261683a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "fakebook-ainiro", "version": "0.0.0", "dependencies": { + "@microsoft/signalr": "^8.0.7", "@reduxjs/toolkit": "^1.8.3", "bootstrap": "^4.6.0", "preact": "^10.25.3", @@ -16,7 +17,8 @@ "react-icons": "^4.2.0", "react-player": "^2.12.0", "react-redux": "^8.0.2", - "react-router-dom": "^5.2.0" + "react-router-dom": "^5.2.0", + "signalr": "^2.4.3" }, "devDependencies": { "@preact/preset-vite": "^2.9.3", @@ -832,6 +834,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/signalr": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz", + "integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1306,6 +1321,18 @@ "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "license": "MIT" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/babel-plugin-transform-hook-names": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", @@ -1629,6 +1656,34 @@ "dev": true, "license": "MIT" }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1726,8 +1781,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", @@ -1841,6 +1895,26 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", @@ -1985,6 +2059,33 @@ "react": ">=0.14.0" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -2228,6 +2329,12 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", @@ -2299,6 +2406,21 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/signalr": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/signalr/-/signalr-2.4.3.tgz", + "integrity": "sha512-RbBKFVCZvDgyyxZDeu6Yck9T+diZO07GB0bDiKondUhBY1H8JRQSOq8R0pLkf47ddllQAssYlp7ckQAeom24mw==", + "license": "Apache-2.0", + "dependencies": { + "jquery": ">=1.6.4" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -2341,6 +2463,27 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/uncontrollable": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", @@ -2365,6 +2508,15 @@ "optional": true, "peer": true }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -2396,6 +2548,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", @@ -2492,6 +2654,43 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index ad66792..d715a86 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,17 @@ "preview": "vite preview" }, "dependencies": { - "preact": "^10.25.3", + "@microsoft/signalr": "^8.0.7", "@reduxjs/toolkit": "^1.8.3", "bootstrap": "^4.6.0", + "preact": "^10.25.3", "react-bootstrap": "^1.5.2", "react-dom": "^17.0.2", "react-icons": "^4.2.0", "react-player": "^2.12.0", "react-redux": "^8.0.2", - "react-router-dom": "^5.2.0" + "react-router-dom": "^5.2.0", + "signalr": "^2.4.3" }, "devDependencies": { "@preact/preset-vite": "^2.9.3", diff --git a/src/app copy/store.js b/src/app copy/store.js deleted file mode 100644 index 72f4154..0000000 --- a/src/app copy/store.js +++ /dev/null @@ -1,24 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import userReducer from "../features/user/userSlice"; -import currentUserReducer from "../features/currentUser/currentUserSlice"; -import usersReducer from "../features/users/usersSlice"; -import postsReducer from "../features/posts/postsSlice"; -import incomingMessagesReducer from "../features/incomingMessages/incomingMessagesSlice"; -import outgoingMessagesReducer from "../features/outgoingMessages/outgoingMessagesSlice"; -import imagesReducer from "../features/images/imagesSlice"; -import linkReducer from "../features/link/linkSlice"; -import accountPageReducer from "../features/accountPage/accountPageSlice"; - -export default configureStore({ - reducer: { - user: userReducer, - currentUser: currentUserReducer, - users: usersReducer, - posts: postsReducer, - incomingMessages: incomingMessagesReducer, - outgoingMessages: outgoingMessagesReducer, - images: imagesReducer, - link: linkReducer, - accountPage: accountPageReducer, - }, -}); diff --git a/src/backend/backend.js b/src/backend/backend.js index 38db8b9..54f0d17 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -1,11 +1,13 @@ /* ===================================================================== - Fakebook — back-end file (AUTH + mock social features) + Fakebook — back-end file (AUTH + mock social features) ===================================================================== */ +import * as signalR from "@microsoft/signalr"; + import store from "../app/store"; import { @@ -30,6 +32,8 @@ import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMe const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook"; +const SOCKETS_URL = "wss://alexerdei-team.us.ainiro.io/sockets"; + const REGISTER_URL = `${API_BASE}/register`; const LOGIN_URL = `${API_BASE}/login`; @@ -63,14 +67,8 @@ function setAuth(token, user_id) { localStorage.setItem(LS_TOKEN, token); localStorage.setItem(LS_USER_ID, user_id); -} -function clearAuth() { - authToken = authUser = null; - - localStorage.removeItem(LS_TOKEN); - - localStorage.removeItem(LS_USER_ID); + openSocket(); } /* --------------------------- Utilities -------------------------------- */ @@ -101,65 +99,13 @@ async function $fetch(url, opts = {}) { return data; } -/* ---------------------- REST → UI mapper ------------------------------- */ - -function addPath(u, fileName) { - if (typeof fileName !== "string" || !fileName.length) return fileName; - - if (fileName.includes("/")) return fileName; /* already has folder */ - - return `${u.user_id}/${fileName}`; /* prepend owner id */ -} - -function mapRestUser(u) { - const photosRaw = JSON.parse(u.photos || "[]"); - - const photos = photosRaw.map((item) => { - if (typeof item === "string") { - /* legacy: array of strings */ - - return { filename: addPath(u, item) }; - } - - if (item && typeof item.filename === "string") { - return { ...item, filename: addPath(u, item.filename) }; - } - - return item; - }); - - return { - userID: u.user_id, - - firstname: u.firstname, - - lastname: u.lastname, - - /* already stored as folder/filename → leave untouched */ - - profilePictureURL: u.profilePictureURL, - - backgroundPictureURL: u.backgroundPictureURL, - - photos, - - posts: JSON.parse(u.posts || "[]"), - - isOnline: !!u.isOnline, - - isEmailVerified: !!u.isEmailVerified, - - index: u.index ?? 0, - }; -} - /* ===================================================================== * - SECTION A — REAL AUTH WORKFLOW + SECTION A — REAL AUTH WORKFLOW - ===================================================================== */ + ===================================================================== */ /* ---------------------- subscribeAuth -------------------------------- */ @@ -310,6 +256,109 @@ export function sendPasswordReminder(email) { return Promise.resolve(); } +/* ------------------------------------------------------------------ */ + +/* SignalR hub – one connection shared across the app */ + +/* ------------------------------------------------------------------ */ + +let hub = null; + +function openSocket() { + if (hub) return; // already connected / connecting + + const token = localStorage.getItem(LS_TOKEN); + + if (!token) return; // not logged-in → no live updates + + hub = new signalR.HubConnectionBuilder() + + .withUrl(SOCKETS_URL, { + accessTokenFactory: () => token, + + skipNegotiation: true, // no CORS pre-flight + + transport: signalR.HttpTransportType.WebSockets, // WebSocket only + }) + + .withAutomaticReconnect() + + .configureLogging(signalR.LogLevel.Warning) + + .build(); + + /* ---------------------------------------------------- */ + + /* helper – SignalR sends a JSON-string → return POJO */ + + /* ---------------------------------------------------- */ + + const parseMsg = (raw) => (typeof raw === "string" ? JSON.parse(raw) : raw); + + /* 1️⃣ Start first … */ + + hub + .start() + + .then(() => { + console.info("[SignalR] connected, id:", hub.connectionId); + + /* 2️⃣ … then register listeners ----------------------------- */ + + /* inside openSocket() – unchanged except the helper */ + + hub.on("fakebook.users.post", (raw) => { + store.dispatch(usersUpdated([parseMsg(raw)])); + }); + + hub.on("fakebook.users.put", (raw) => { + store.dispatch(usersUpdated([parseMsg(raw)])); + }); + }) + + .catch((err) => { + console.warn("[SignalR] start failed:", err); + + hub = null; // let auto-reconnect retry + }); + + /* extra diagnostics ---------------------------------------------- */ + + hub.onreconnecting((err) => console.warn("[SignalR] reconnecting:", err)); + + hub.onreconnected((id) => console.info("[SignalR] reconnected, id:", id)); + + hub.onclose((err) => console.warn("[SignalR] closed:", err)); +} + +/* ------------------------------------------------------------------ */ + +/* Close SignalR + forget credentials */ + +/* ------------------------------------------------------------------ */ + +function clearAuth() { + /* forget in-memory copies */ + + authToken = null; + + authUser = null; + + /* forget persisted copies */ + + localStorage.removeItem(LS_TOKEN); // "fakebook.jwt" + + localStorage.removeItem(LS_USER_ID); // "fakebook.user_id" + + /* close the hub if it exists */ + + if (hub) { + hub.stop(); // graceful shutdown → returns a promise we don’t await + + hub = null; + } +} + /* ===================================================================== * SECTION B — IN-MEMORY MOCK (UNCHANGED) * @@ -372,7 +421,7 @@ export function subscribeCurrentUser() { const meRow = users.find((u) => u.user_id === user_id); if (meRow && !cancelled) { - store.dispatch(currentUserUpdated(mapRestUser(meRow))); + store.dispatch(currentUserUpdated(meRow)); } } catch (err) { console.warn("[subscribeCurrentUser] failed:", err.message); @@ -447,7 +496,7 @@ export function subscribeUsers() { }); if (!cancelled) { - store.dispatch(usersUpdated(users.map(mapRestUser))); + store.dispatch(usersUpdated(users)); } } catch (err) { console.warn("[subscribeUsers] failed:", err.message); diff --git a/src/features/currentUser/currentUserSlice.js b/src/features/currentUser/currentUserSlice.js index a05e3e3..ade2a0c 100644 --- a/src/features/currentUser/currentUserSlice.js +++ b/src/features/currentUser/currentUserSlice.js @@ -1,40 +1,56 @@ import { createSlice } from "@reduxjs/toolkit"; +import mapRestUser from "../../utils/mapRestUser"; // same helper the users slice uses + export const currentUserSlice = createSlice({ - name: "currentUser", - initialState: { - firstname: "", - lastname: "", - profilePictureURL: "fakebook-avatar.jpeg", - backgroundPictureURL: "background-server.jpg", - photos: [], - posts: [], - isOnline: false, - }, - reducers: { - currentUserUpdated: (state, action) => { - const { - firstname, - lastname, - profilePictureURL, - backgroundPictureURL, - photos, - posts, - isOnline, - index, - } = action.payload; - state.firstname = firstname; - state.lastname = lastname; - state.profilePictureURL = profilePictureURL; - state.backgroundPictureURL = backgroundPictureURL; - state.isOnline = isOnline; - if (index) state.index = index; - state.photos = []; - state.posts = []; - photos.forEach((photo) => state.photos.push(photo)); - posts.forEach((post) => state.posts.push(post)); - }, - }, + name: "currentUser", + + initialState: null, // stays a single object, not an array + + reducers: { + /* ------------------------------------------------------------------ + + currentUserUpdated + + – can receive either a *full* user object (first load / login) + + – or a *partial* patch coming from SignalR (online flag etc.) + + ------------------------------------------------------------------ */ + + currentUserUpdated: (state, action) => { + const raw = action.payload; + + /* first call or explicit full replacement ---------------------- */ + + if ( + state === null || + (raw.firstname !== undefined && raw.lastname !== undefined) + ) { + return mapRestUser(raw); // normalise every field once + } + + /* otherwise treat it as a PATCH -------------------------------- */ + + const next = { ...state }; // Immer lets us mutate but explicit copy is clear + + if (raw.isOnline !== undefined) next.isOnline = !!raw.isOnline; + + if (raw.firstname !== undefined) next.firstname = raw.firstname; + + if (raw.lastname !== undefined) next.lastname = raw.lastname; + + if (raw.profilePictureURL !== undefined) + next.profilePictureURL = raw.profilePictureURL; + + if (raw.backgroundPictureURL !== undefined) + next.backgroundPictureURL = raw.backgroundPictureURL; + + /* add similar guards if more fields can arrive partially … */ + + return next; // Immer will take care of immutability + }, + }, }); export const { currentUserUpdated } = currentUserSlice.actions; diff --git a/src/features/users/usersSlice.js b/src/features/users/usersSlice.js index 9ebc55e..5f8f4a1 100644 --- a/src/features/users/usersSlice.js +++ b/src/features/users/usersSlice.js @@ -1,13 +1,52 @@ import { createSlice } from "@reduxjs/toolkit"; +import mapRestUser from "../../utils/mapRestUser"; export const usersSlice = createSlice({ name: "users", initialState: [], reducers: { usersUpdated: (state, action) => { - const updatedState = []; - action.payload.forEach((user) => updatedState.push(user)); - return updatedState; + /* action.payload will always be an *array* */ + + const incoming = action.payload; + + incoming.forEach((raw) => { + const userID = raw.user_id ?? raw.userID; + + const idx = state.findIndex((u) => u.userID === userID); + + if (idx === -1) { + /* -------- NEW USER ----------------------------------------- */ + + state.push(mapRestUser(raw)); // map every field + + return; + } + + /* -------- EXISTING USER -------------------------------------- */ + + const cur = state[idx]; + + const patch = {}; + + /* Online flag comes as 0/1; convert to boolean if present */ + + if (raw.isOnline !== undefined) patch.isOnline = !!raw.isOnline; + + if (raw.firstname !== undefined) patch.firstname = raw.firstname; + + if (raw.lastname !== undefined) patch.lastname = raw.lastname; + + if (raw.profilePictureURL !== undefined) + patch.profilePictureURL = raw.profilePictureURL; + + if (raw.backgroundPictureURL !== undefined) + patch.backgroundPictureURL = raw.backgroundPictureURL; + + /* add other fields you expect to arrive partially … */ + + state[idx] = { ...cur, ...patch }; // keep old fields + }); }, }, }); diff --git a/src/utils/mapRestUser.js b/src/utils/mapRestUser.js new file mode 100644 index 0000000..de45a59 --- /dev/null +++ b/src/utils/mapRestUser.js @@ -0,0 +1,51 @@ +/* utils/mapRestUser.js + + Converts a “magic” REST / SignalR user row into the shape Fakebook + + components already consume. */ + +function addPath(row, fileName) { + if (typeof fileName !== "string" || !fileName.length) return fileName; + + if (fileName.includes("/")) return fileName; // already has folder + + return `${row.user_id}/${fileName}`; // prepend owner id +} + +export default function mapRestUser(row) { + const photosRaw = JSON.parse(row.photos || "[]"); + + const photos = photosRaw.map((item) => { + if (typeof item === "string") { + return { filename: addPath(row, item) }; // legacy array + } + + if (item && typeof item.filename === "string") { + return { ...item, filename: addPath(row, item.filename) }; + } + + return item; + }); + + return { + userID: row.user_id, + + firstname: row.firstname, + + lastname: row.lastname, + + profilePictureURL: row.profilePictureURL, + + backgroundPictureURL: row.backgroundPictureURL, + + photos, + + posts: JSON.parse(row.posts || "[]"), + + isOnline: !!row.isOnline, + + isEmailVerified: !!row.isEmailVerified, + + index: row.index ?? 0, + }; +}