Make images visible in the app

This commit is contained in:
Alex Erdei 2025-04-29 23:43:54 +01:00
parent a8898d2ad9
commit d92d7939dd
4 changed files with 167 additions and 172 deletions

View File

@ -1,13 +1,12 @@
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'
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 linkReducer from "../features/link/linkSlice";
import accountPageReducer from "../features/accountPage/accountPageSlice";
export default configureStore({
reducer: {
@ -17,7 +16,6 @@ export default configureStore({
posts: postsReducer,
incomingMessages: incomingMessagesReducer,
outgoingMessages: outgoingMessagesReducer,
images: imagesReducer,
link: linkReducer,
accountPage: accountPageReducer,
},

View File

@ -1,7 +1,9 @@
/* =====================================================================
Fakebook back-end file (AUTH + mock social features)
===================================================================== */
import store from "../app/store";
@ -38,6 +40,39 @@ const LS_TOKEN = "fakebook.jwt";
const LS_USER_ID = "fakebook.user_id";
/* --------------------------- SESSION (NEW) ----------------------------- */
/* In-memory copy null while logged-out */
let authToken = null;
let authUser = null;
/* Header helper import this anywhere you need the bearer token */
const authHeader = () =>
authToken ? { Authorization: `Bearer ${authToken}` } : {};
/* Initialise / clear session */
function setAuth(token, user_id) {
authToken = token;
authUser = 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);
}
/* --------------------------- Utilities -------------------------------- */
const genId = () => Math.random().toString(36).slice(2, 11);
@ -45,8 +80,17 @@ const genId = () => Math.random().toString(36).slice(2, 11);
const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms));
async function $fetch(url, opts = {}) {
/* inject bearer automatically */
const res = await fetch(url, {
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
headers: {
"Content-Type": "application/json",
...authHeader(),
...(opts.headers || {}),
},
...opts,
});
@ -57,9 +101,33 @@ async function $fetch(url, opts = {}) {
return data;
}
/* ---------------------- REST → UI mapper (moved up) ------------------ */
/* ---------------------- 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,
@ -67,11 +135,13 @@ function mapRestUser(u) {
lastname: u.lastname,
/* already stored as folder/filename → leave untouched */
profilePictureURL: u.profilePictureURL,
backgroundPictureURL: u.backgroundPictureURL,
photos: JSON.parse(u.photos || "[]"),
photos,
posts: JSON.parse(u.posts || "[]"),
@ -85,7 +155,9 @@ function mapRestUser(u) {
/* ===================================================================== *
SECTION A REAL AUTH WORKFLOW (FIXED) *
SECTION A REAL AUTH WORKFLOW
===================================================================== */
@ -100,6 +172,8 @@ export function subscribeAuth() {
const user_id = localStorage.getItem(LS_USER_ID);
if (!token || !user_id) {
clearAuth(); /* make sure RAM copy is empty */
store.dispatch(signOut());
store.dispatch(loadingFinished());
@ -107,16 +181,16 @@ export function subscribeAuth() {
return;
}
/* restore session into RAM */
setAuth(token, user_id);
try {
/* The users endpoint does NOT understand ?user_id=.
/* users endpoint lacks ?user_id, so fetch all */
Workaround: fetch all users (limit=-1) then find ours. */
const users = await $fetch(`${USERS_URL}?limit=-1`);
const users = await $fetch(`${USERS_URL}?limit=-1`, {
headers: { Authorization: `Bearer ${token}` },
});
const u = users.find((u) => u.user_id === user_id);
const u = users.find((x) => x.user_id === user_id);
if (!u) throw new Error("User not found");
@ -130,13 +204,9 @@ export function subscribeAuth() {
})
);
} catch (err) {
/* Token invalid / expired / user not found */
console.warn("[Auth] subscribeAuth failed:", err.message);
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
clearAuth();
store.dispatch(signOut());
} finally {
@ -167,7 +237,7 @@ export async function createUserAccount(user) {
}),
});
store.dispatch(errorOccured("")); // clear previous error
store.dispatch(errorOccured(""));
} catch (err) {
store.dispatch(errorOccured(err.message));
} finally {
@ -175,41 +245,32 @@ export async function createUserAccount(user) {
}
}
/* ---------------------- signInUser (FIXED) -------------------------- */
/* ---------------------- signInUser ---------------------------------- */
export async function signInUser(user) {
store.dispatch(loadingStarted());
try {
/* 1. Login with email + password (as API expects) */
const url = `${LOGIN_URL}?email=${encodeURIComponent(
user.email
)}&password=${encodeURIComponent(user.password)}`;
const { token, user_id } = await $fetch(url);
/* 2. Persist session */
/* persist + put into RAM */
localStorage.setItem(LS_TOKEN, token);
setAuth(token, user_id);
localStorage.setItem(LS_USER_ID, user_id);
/* get full profile */
/* 3. Fetch FULL user list, find our profile */
const users = await $fetch(`${USERS_URL}?limit=-1`, {
headers: { Authorization: `Bearer ${token}` },
});
const users = await $fetch(`${USERS_URL}?limit=-1`);
const profile = users.find((u) => u.user_id === user_id);
if (!profile) throw new Error("User not found");
if (!profile.isEmailVerified) {
if (!profile.isEmailVerified)
throw new Error("Please verify your email before to continue");
}
/* 4. Dispatch sign-in */
store.dispatch(
signIn({
@ -225,9 +286,7 @@ export async function signInUser(user) {
} catch (err) {
store.dispatch(errorOccured(err.message));
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
clearAuth();
} finally {
store.dispatch(loadingFinished());
}
@ -236,13 +295,9 @@ export async function signInUser(user) {
/* ---------------------- signUserOut ---------------------------------- */
export async function signUserOut() {
/* mark offline in DB */
await patchOnline(false);
await patchOnline(false); /* mark offline */
/* clear session */
localStorage.removeItem(LS_TOKEN);
localStorage.removeItem(LS_USER_ID);
clearAuth();
store.dispatch(signOut());
}

View File

@ -1,102 +1,68 @@
import React, { useState } from "react";
import { getImageURL } from "../backend/backend";
import { useSelector, useDispatch } from "react-redux";
import React, { useEffect, useState } from "react";
import placeholderImage from "../images/placeholder-image.jpg";
import fakebookAvatar from "../images/fakebook-avatar.jpeg";
import backgroundServer from "../images/background-server.jpg";
import { imageAdded, imageUrlFound } from "../features/images/imagesSlice";
import { useEffect } from "react";
const StorageImage = (props) => {
const { storagePath, alt, ...rest } = props;
const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook";
const PLACEHOLDER_AVATAR_STORAGE_PATH = "fakebook-avatar.jpeg";
const PLACEHOLDER_BACKGROUND_STORAGE_PATH = "background-server.jpg";
/* ------------------------------------------------------------------ */
//We use the images slice as a buffer. Fetching the actual url of the image
//in the storage takes relatively long time and uses Firebase. We render the same
//image in the app several times. Our goal to fetch the url only once for each image
//to save resources.
const images = useSelector((state) => state.images);
const dispatch = useDispatch();
function toWebp(name) {
if (name.toLowerCase().endsWith(".webp")) return name;
const [url, setUrl] = useState(placeholderImage);
const dot = name.lastIndexOf(".");
function changeStoragePath(storagePath) {
const words = storagePath.split(".");
words[words.length - 2] += "_400x400";
return words.join(".");
return dot === -1 ? `${name}.webp` : `${name.slice(0, dot)}.webp`;
}
async function fetchImageURL(storagePath) {
if (!storagePath) return placeholderImage;
if (storagePath === "fakebook-avatar.jpeg") return fakebookAvatar;
if (storagePath === "background-server.jpg") return backgroundServer;
const [folder, ...rest] = storagePath.split("/");
const rawFilename = rest.join("/");
const filenameWebp = toWebp(rawFilename); // only change
const url =
`${API_BASE}/image?folder=fakebook/${encodeURIComponent(folder)}` +
`&filename=${encodeURIComponent(filenameWebp)}`;
const res = await fetch(url);
if (!res.ok) throw new Error("Image request failed");
const blob = await res.blob();
return URL.createObjectURL(blob);
}
/* ================================================================== */
const StorageImage = ({ storagePath, ...rest }) => {
const [src, setSrc] = useState(placeholderImage);
useEffect(() => {
let shouldUpdate = true;
const cleanup = () => (shouldUpdate = false);
let cancelled = false;
//We filter out placeholder pictures
if (storagePath === PLACEHOLDER_AVATAR_STORAGE_PATH) {
setUrl(fakebookAvatar);
return cleanup;
}
if (storagePath === PLACEHOLDER_BACKGROUND_STORAGE_PATH) {
setUrl(backgroundServer);
return cleanup;
}
fetchImageURL(storagePath)
.then((url) => !cancelled && setSrc(url))
//We look for the url in images slice first
let imageIndex = images
.map((image) => image.storagePath)
.indexOf(storagePath);
if (imageIndex === -1) {
imageIndex = images
.map((image) => image.storagePath)
.indexOf(changeStoragePath(storagePath));
//If we are unable to find it anyway we add the image to the slice
if (imageIndex === -1) {
dispatch(
imageAdded({
storagePath,
url,
})
);
}
//We add the url for the image to the images slice when we have actually got it
//We also update the local state to show the right image
getImageURL(storagePath)
.then((url) => {
setUrl(url);
dispatch(
imageUrlFound({
storagePath,
url,
})
);
})
.catch((_error) => {
getImageURL(changeStoragePath(storagePath)).then((url) => {
setUrl(url);
dispatch(
imageUrlFound({
storagePath,
url,
})
);
});
});
} else {
//If we are able to find the url in the images slice we just use it instead of fetching
const newUrl = images[imageIndex].url;
//We only update the state if it contains different value and we should update because
//the component has not been unmounted by the time the promise resolves
if (newUrl !== url && shouldUpdate) setUrl(newUrl);
}
//We return a cleanup function which runs when the component unmounts. We set
//the shouldUpdate to false, so after this the state cannot be updated. If
//we don't do this React gives us error messages about state update on our
//unmounted component
return cleanup;
}, [images, storagePath, url, dispatch]);
.catch(() => !cancelled && setSrc(placeholderImage));
return <img src={url} alt={alt} {...rest} />;
return () => {
cancelled = true;
};
}, [storagePath]);
return <img src={src} alt='' {...rest} />;
};
export default StorageImage;

View File

@ -1,24 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
export const imagesSlice = createSlice({
name: "images",
initialState: [],
reducers: {
imageAdded: (state, action) => {
const index = state
.map((image) => image.storagePath)
.indexOf(action.payload.storagePath);
if (index === -1) state.push(action.payload);
},
imageUrlFound: (state, action) => {
const index = state
.map((image) => image.storagePath)
.indexOf(action.payload.storagePath);
if (index > -1) state[index].url = action.payload.url;
},
},
});
export const { imageAdded, imageUrlFound } = imagesSlice.actions;
export default imagesSlice.reducer;