Make images visible in the app
This commit is contained in:
parent
a8898d2ad9
commit
d92d7939dd
|
@ -1,13 +1,12 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import userReducer from '../features/user/userSlice'
|
import userReducer from "../features/user/userSlice";
|
||||||
import currentUserReducer from '../features/currentUser/currentUserSlice'
|
import currentUserReducer from "../features/currentUser/currentUserSlice";
|
||||||
import usersReducer from '../features/users/usersSlice'
|
import usersReducer from "../features/users/usersSlice";
|
||||||
import postsReducer from '../features/posts/postsSlice'
|
import postsReducer from "../features/posts/postsSlice";
|
||||||
import incomingMessagesReducer from '../features/incomingMessages/incomingMessagesSlice'
|
import incomingMessagesReducer from "../features/incomingMessages/incomingMessagesSlice";
|
||||||
import outgoingMessagesReducer from '../features/outgoingMessages/outgoingMessagesSlice'
|
import outgoingMessagesReducer from "../features/outgoingMessages/outgoingMessagesSlice";
|
||||||
import imagesReducer from '../features/images/imagesSlice'
|
import linkReducer from "../features/link/linkSlice";
|
||||||
import linkReducer from '../features/link/linkSlice'
|
import accountPageReducer from "../features/accountPage/accountPageSlice";
|
||||||
import accountPageReducer from '../features/accountPage/accountPageSlice'
|
|
||||||
|
|
||||||
export default configureStore({
|
export default configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
|
@ -17,7 +16,6 @@ export default configureStore({
|
||||||
posts: postsReducer,
|
posts: postsReducer,
|
||||||
incomingMessages: incomingMessagesReducer,
|
incomingMessages: incomingMessagesReducer,
|
||||||
outgoingMessages: outgoingMessagesReducer,
|
outgoingMessages: outgoingMessagesReducer,
|
||||||
images: imagesReducer,
|
|
||||||
link: linkReducer,
|
link: linkReducer,
|
||||||
accountPage: accountPageReducer,
|
accountPage: accountPageReducer,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
|
|
||||||
|
|
||||||
Fakebook — back-end file (AUTH + mock social features)
|
Fakebook — back-end file (AUTH + mock social features)
|
||||||
|
|
||||||
|
|
||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
|
||||||
import store from "../app/store";
|
import store from "../app/store";
|
||||||
|
@ -38,6 +40,39 @@ const LS_TOKEN = "fakebook.jwt";
|
||||||
|
|
||||||
const LS_USER_ID = "fakebook.user_id";
|
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 -------------------------------- */
|
/* --------------------------- Utilities -------------------------------- */
|
||||||
|
|
||||||
const genId = () => Math.random().toString(36).slice(2, 11);
|
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));
|
const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
async function $fetch(url, opts = {}) {
|
async function $fetch(url, opts = {}) {
|
||||||
|
/* inject bearer automatically */
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
|
||||||
|
...authHeader(),
|
||||||
|
|
||||||
|
...(opts.headers || {}),
|
||||||
|
},
|
||||||
|
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,9 +101,33 @@ async function $fetch(url, opts = {}) {
|
||||||
return data;
|
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) {
|
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 {
|
return {
|
||||||
userID: u.user_id,
|
userID: u.user_id,
|
||||||
|
|
||||||
|
@ -67,11 +135,13 @@ function mapRestUser(u) {
|
||||||
|
|
||||||
lastname: u.lastname,
|
lastname: u.lastname,
|
||||||
|
|
||||||
|
/* already stored as folder/filename → leave untouched */
|
||||||
|
|
||||||
profilePictureURL: u.profilePictureURL,
|
profilePictureURL: u.profilePictureURL,
|
||||||
|
|
||||||
backgroundPictureURL: u.backgroundPictureURL,
|
backgroundPictureURL: u.backgroundPictureURL,
|
||||||
|
|
||||||
photos: JSON.parse(u.photos || "[]"),
|
photos,
|
||||||
|
|
||||||
posts: JSON.parse(u.posts || "[]"),
|
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);
|
const user_id = localStorage.getItem(LS_USER_ID);
|
||||||
|
|
||||||
if (!token || !user_id) {
|
if (!token || !user_id) {
|
||||||
|
clearAuth(); /* make sure RAM copy is empty */
|
||||||
|
|
||||||
store.dispatch(signOut());
|
store.dispatch(signOut());
|
||||||
|
|
||||||
store.dispatch(loadingFinished());
|
store.dispatch(loadingFinished());
|
||||||
|
@ -107,16 +181,16 @@ export function subscribeAuth() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* restore session into RAM */
|
||||||
|
|
||||||
|
setAuth(token, user_id);
|
||||||
|
|
||||||
try {
|
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`, {
|
const u = users.find((x) => x.user_id === user_id);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
const u = users.find((u) => u.user_id === user_id);
|
|
||||||
|
|
||||||
if (!u) throw new Error("User not found");
|
if (!u) throw new Error("User not found");
|
||||||
|
|
||||||
|
@ -130,13 +204,9 @@ export function subscribeAuth() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
/* Token invalid / expired / user not found */
|
|
||||||
|
|
||||||
console.warn("[Auth] subscribeAuth failed:", err.message);
|
console.warn("[Auth] subscribeAuth failed:", err.message);
|
||||||
|
|
||||||
localStorage.removeItem(LS_TOKEN);
|
clearAuth();
|
||||||
|
|
||||||
localStorage.removeItem(LS_USER_ID);
|
|
||||||
|
|
||||||
store.dispatch(signOut());
|
store.dispatch(signOut());
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -167,7 +237,7 @@ export async function createUserAccount(user) {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
store.dispatch(errorOccured("")); // clear previous error
|
store.dispatch(errorOccured(""));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
store.dispatch(errorOccured(err.message));
|
store.dispatch(errorOccured(err.message));
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -175,41 +245,32 @@ export async function createUserAccount(user) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------- signInUser (FIXED) -------------------------- */
|
/* ---------------------- signInUser ---------------------------------- */
|
||||||
|
|
||||||
export async function signInUser(user) {
|
export async function signInUser(user) {
|
||||||
store.dispatch(loadingStarted());
|
store.dispatch(loadingStarted());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* 1. Login with email + password (as API expects) */
|
|
||||||
|
|
||||||
const url = `${LOGIN_URL}?email=${encodeURIComponent(
|
const url = `${LOGIN_URL}?email=${encodeURIComponent(
|
||||||
user.email
|
user.email
|
||||||
)}&password=${encodeURIComponent(user.password)}`;
|
)}&password=${encodeURIComponent(user.password)}`;
|
||||||
|
|
||||||
const { token, user_id } = await $fetch(url);
|
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`);
|
||||||
|
|
||||||
const users = await $fetch(`${USERS_URL}?limit=-1`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
const profile = users.find((u) => u.user_id === user_id);
|
const profile = users.find((u) => u.user_id === user_id);
|
||||||
|
|
||||||
if (!profile) throw new Error("User not found");
|
if (!profile) throw new Error("User not found");
|
||||||
|
|
||||||
if (!profile.isEmailVerified) {
|
if (!profile.isEmailVerified)
|
||||||
throw new Error("Please verify your email before to continue");
|
throw new Error("Please verify your email before to continue");
|
||||||
}
|
|
||||||
|
|
||||||
/* 4. Dispatch sign-in */
|
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
signIn({
|
signIn({
|
||||||
|
@ -225,9 +286,7 @@ export async function signInUser(user) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
store.dispatch(errorOccured(err.message));
|
store.dispatch(errorOccured(err.message));
|
||||||
|
|
||||||
localStorage.removeItem(LS_TOKEN);
|
clearAuth();
|
||||||
|
|
||||||
localStorage.removeItem(LS_USER_ID);
|
|
||||||
} finally {
|
} finally {
|
||||||
store.dispatch(loadingFinished());
|
store.dispatch(loadingFinished());
|
||||||
}
|
}
|
||||||
|
@ -236,13 +295,9 @@ export async function signInUser(user) {
|
||||||
/* ---------------------- signUserOut ---------------------------------- */
|
/* ---------------------- signUserOut ---------------------------------- */
|
||||||
|
|
||||||
export async function signUserOut() {
|
export async function signUserOut() {
|
||||||
/* mark offline in DB */
|
await patchOnline(false); /* mark offline */
|
||||||
await patchOnline(false);
|
|
||||||
|
|
||||||
/* clear session */
|
clearAuth();
|
||||||
localStorage.removeItem(LS_TOKEN);
|
|
||||||
|
|
||||||
localStorage.removeItem(LS_USER_ID);
|
|
||||||
|
|
||||||
store.dispatch(signOut());
|
store.dispatch(signOut());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,102 +1,68 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { getImageURL } from "../backend/backend";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
|
||||||
import placeholderImage from "../images/placeholder-image.jpg";
|
import placeholderImage from "../images/placeholder-image.jpg";
|
||||||
|
|
||||||
import fakebookAvatar from "../images/fakebook-avatar.jpeg";
|
import fakebookAvatar from "../images/fakebook-avatar.jpeg";
|
||||||
|
|
||||||
import backgroundServer from "../images/background-server.jpg";
|
import backgroundServer from "../images/background-server.jpg";
|
||||||
import { imageAdded, imageUrlFound } from "../features/images/imagesSlice";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
const StorageImage = (props) => {
|
const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook";
|
||||||
const { storagePath, alt, ...rest } = props;
|
|
||||||
|
|
||||||
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
|
function toWebp(name) {
|
||||||
//in the storage takes relatively long time and uses Firebase. We render the same
|
if (name.toLowerCase().endsWith(".webp")) return name;
|
||||||
//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();
|
|
||||||
|
|
||||||
const [url, setUrl] = useState(placeholderImage);
|
const dot = name.lastIndexOf(".");
|
||||||
|
|
||||||
function changeStoragePath(storagePath) {
|
return dot === -1 ? `${name}.webp` : `${name.slice(0, dot)}.webp`;
|
||||||
const words = storagePath.split(".");
|
}
|
||||||
words[words.length - 2] += "_400x400";
|
|
||||||
return words.join(".");
|
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(() => {
|
useEffect(() => {
|
||||||
let shouldUpdate = true;
|
let cancelled = false;
|
||||||
const cleanup = () => (shouldUpdate = false);
|
|
||||||
|
|
||||||
//We filter out placeholder pictures
|
fetchImageURL(storagePath)
|
||||||
if (storagePath === PLACEHOLDER_AVATAR_STORAGE_PATH) {
|
.then((url) => !cancelled && setSrc(url))
|
||||||
setUrl(fakebookAvatar);
|
|
||||||
return cleanup;
|
|
||||||
}
|
|
||||||
if (storagePath === PLACEHOLDER_BACKGROUND_STORAGE_PATH) {
|
|
||||||
setUrl(backgroundServer);
|
|
||||||
return cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
//We look for the url in images slice first
|
.catch(() => !cancelled && setSrc(placeholderImage));
|
||||||
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]);
|
|
||||||
|
|
||||||
return <img src={url} alt={alt} {...rest} />;
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [storagePath]);
|
||||||
|
|
||||||
|
return <img src={src} alt='' {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StorageImage;
|
export default StorageImage;
|
||||||
|
|
|
@ -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;
|
|
Loading…
Reference in New Issue