Make SignalR connection work on users' online status

This commit is contained in:
Alex Erdei 2025-05-01 22:23:55 +01:00
parent 1ae6adb349
commit 2bf2c4d9dd
7 changed files with 462 additions and 130 deletions

205
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "fakebook-ainiro", "name": "fakebook-ainiro",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@microsoft/signalr": "^8.0.7",
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"preact": "^10.25.3", "preact": "^10.25.3",
@ -16,7 +17,8 @@
"react-icons": "^4.2.0", "react-icons": "^4.2.0",
"react-player": "^2.12.0", "react-player": "^2.12.0",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-router-dom": "^5.2.0" "react-router-dom": "^5.2.0",
"signalr": "^2.4.3"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.9.3", "@preact/preset-vite": "^2.9.3",
@ -832,6 +834,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@ -1306,6 +1321,18 @@
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
"license": "MIT" "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": { "node_modules/babel-plugin-transform-hook-names": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", "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, "dev": true,
"license": "MIT" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1726,8 +1781,7 @@
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
@ -1841,6 +1895,26 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "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": { "node_modules/node-html-parser": {
"version": "6.1.13", "version": "6.1.13",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
@ -1985,6 +2059,33 @@
"react": ">=0.14.0" "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": { "node_modules/react": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
@ -2228,6 +2329,12 @@
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT" "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": { "node_modules/reselect": {
"version": "4.1.8", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
@ -2299,6 +2406,21 @@
"semver": "bin/semver.js" "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": { "node_modules/source-map": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@ -2341,6 +2463,27 @@
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT" "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": { "node_modules/uncontrollable": {
"version": "7.2.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
@ -2365,6 +2508,15 @@
"optional": true, "optional": true,
"peer": 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": { "node_modules/update-browserslist-db": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
@ -2396,6 +2548,16 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/use-sync-external-store": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", "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" "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": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -9,15 +9,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"preact": "^10.25.3", "@microsoft/signalr": "^8.0.7",
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"preact": "^10.25.3",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-icons": "^4.2.0", "react-icons": "^4.2.0",
"react-player": "^2.12.0", "react-player": "^2.12.0",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-router-dom": "^5.2.0" "react-router-dom": "^5.2.0",
"signalr": "^2.4.3"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.9.3", "@preact/preset-vite": "^2.9.3",

View File

@ -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,
},
});

View File

@ -6,6 +6,8 @@
===================================================================== */ ===================================================================== */
import * as signalR from "@microsoft/signalr";
import store from "../app/store"; import store from "../app/store";
import { import {
@ -30,6 +32,8 @@ import { outgoingMessagesUpdated } from "../features/outgoingMessages/outgoingMe
const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook"; 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 REGISTER_URL = `${API_BASE}/register`;
const LOGIN_URL = `${API_BASE}/login`; const LOGIN_URL = `${API_BASE}/login`;
@ -63,14 +67,8 @@ function setAuth(token, user_id) {
localStorage.setItem(LS_TOKEN, token); localStorage.setItem(LS_TOKEN, token);
localStorage.setItem(LS_USER_ID, user_id); localStorage.setItem(LS_USER_ID, user_id);
}
function clearAuth() { openSocket();
authToken = authUser = null;
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
} }
/* --------------------------- Utilities -------------------------------- */ /* --------------------------- Utilities -------------------------------- */
@ -101,58 +99,6 @@ async function $fetch(url, opts = {}) {
return data; 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,
};
}
/* ===================================================================== * /* ===================================================================== *
@ -310,6 +256,109 @@ export function sendPasswordReminder(email) {
return Promise.resolve(); 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 dont await
hub = null;
}
}
/* ===================================================================== * /* ===================================================================== *
SECTION B IN-MEMORY MOCK (UNCHANGED) * SECTION B IN-MEMORY MOCK (UNCHANGED) *
@ -372,7 +421,7 @@ export function subscribeCurrentUser() {
const meRow = users.find((u) => u.user_id === user_id); const meRow = users.find((u) => u.user_id === user_id);
if (meRow && !cancelled) { if (meRow && !cancelled) {
store.dispatch(currentUserUpdated(mapRestUser(meRow))); store.dispatch(currentUserUpdated(meRow));
} }
} catch (err) { } catch (err) {
console.warn("[subscribeCurrentUser] failed:", err.message); console.warn("[subscribeCurrentUser] failed:", err.message);
@ -447,7 +496,7 @@ export function subscribeUsers() {
}); });
if (!cancelled) { if (!cancelled) {
store.dispatch(usersUpdated(users.map(mapRestUser))); store.dispatch(usersUpdated(users));
} }
} catch (err) { } catch (err) {
console.warn("[subscribeUsers] failed:", err.message); console.warn("[subscribeUsers] failed:", err.message);

View File

@ -1,38 +1,54 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import mapRestUser from "../../utils/mapRestUser"; // same helper the users slice uses
export const currentUserSlice = createSlice({ export const currentUserSlice = createSlice({
name: "currentUser", name: "currentUser",
initialState: {
firstname: "", initialState: null, // stays a single object, not an array
lastname: "",
profilePictureURL: "fakebook-avatar.jpeg",
backgroundPictureURL: "background-server.jpg",
photos: [],
posts: [],
isOnline: false,
},
reducers: { 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) => { currentUserUpdated: (state, action) => {
const { const raw = action.payload;
firstname,
lastname, /* first call or explicit full replacement ---------------------- */
profilePictureURL,
backgroundPictureURL, if (
photos, state === null ||
posts, (raw.firstname !== undefined && raw.lastname !== undefined)
isOnline, ) {
index, return mapRestUser(raw); // normalise every field once
} = action.payload; }
state.firstname = firstname;
state.lastname = lastname; /* otherwise treat it as a PATCH -------------------------------- */
state.profilePictureURL = profilePictureURL;
state.backgroundPictureURL = backgroundPictureURL; const next = { ...state }; // Immer lets us mutate but explicit copy is clear
state.isOnline = isOnline;
if (index) state.index = index; if (raw.isOnline !== undefined) next.isOnline = !!raw.isOnline;
state.photos = [];
state.posts = []; if (raw.firstname !== undefined) next.firstname = raw.firstname;
photos.forEach((photo) => state.photos.push(photo));
posts.forEach((post) => state.posts.push(post)); 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
}, },
}, },
}); });

View File

@ -1,13 +1,52 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import mapRestUser from "../../utils/mapRestUser";
export const usersSlice = createSlice({ export const usersSlice = createSlice({
name: "users", name: "users",
initialState: [], initialState: [],
reducers: { reducers: {
usersUpdated: (state, action) => { usersUpdated: (state, action) => {
const updatedState = []; /* action.payload will always be an *array* */
action.payload.forEach((user) => updatedState.push(user));
return updatedState; 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
});
}, },
}, },
}); });

51
src/utils/mapRestUser.js Normal file
View File

@ -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,
};
}