diff --git a/public/images/attachment-icon.svg b/public/images/attachment-icon.svg new file mode 100644 index 00000000..9f1284ee --- /dev/null +++ b/public/images/attachment-icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 111e2569..e34aba87 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,7 +22,6 @@ import { areOccurenciesEqual, createUserLists, encrypt, decrypt, getBrowser } fr import { getCurrentSchoolYear } from "./utils/date"; import { getProxiedURL } from "./utils/requests"; import EdpuLogo from "./components/graphics/EdpuLogo"; -import { add } from "date-fns"; // CODE-SPLITTING - DYNAMIC IMPORTS const Lab = lazy(() => import("./components/app/CoreApp").then((module) => { return { default: module.Lab } })); @@ -72,7 +71,7 @@ function consoleLogEDPLogo() { consoleLogEDPLogo(); const currentEDPVersion = "0.3.1"; -const apiVersion = "4.53.4"; +const apiVersion = "4.60.4"; // secret webhooks const carpeConviviale = "CARPE_CONVIVIALE_WEBHOOK_URL"; @@ -1140,6 +1139,40 @@ export default function App() { return sortedHomeworks } + + function sortMessages(messages) { + const sortedMessages = messages.messages.received.map((message) => { console.log("files:", message.files); return { + date: message.date, + files: structuredClone(message.files)?.map((file) => new File(file.id, file.type, file.libelle)), + from: message.from, + id: message.id, + read: message.read, + subject: message.subject, + content: null, + // ... + }}); + + return sortedMessages; + } + + function sortMessageContent(messageContent) { + if (!messageContent) { + return; + } + const oldSortedMessages = useUserData("sortedMessages").get(); + const targetMessageIdx = oldSortedMessages.findIndex((item) => item.id === messageContent.id); + oldSortedMessages[targetMessageIdx].read = true; + oldSortedMessages[targetMessageIdx].files = messageContent.files.map((file) => new File(file.id, file.type, file.libelle)); + oldSortedMessages[targetMessageIdx].content = { + id: messageContent.id, + subject: messageContent.subject, + date: messageContent.subject, + content: messageContent.content + // ... + }; + useUserData("sortedMessages").set(oldSortedMessages); + } + function sortSchoolLife(schoolLife, activeAccount) { const sortedSchoolLife = { delays: [], @@ -1283,6 +1316,7 @@ export default function App() { }) }) .then((response) => { + console.log(".then ~ response:", response) // GESTION DATA let statusCode = response.code; if (statusCode === 200) { @@ -1317,6 +1351,7 @@ export default function App() { accountType: "P", lastConnection: accounts.lastConnexion, id: account.id, + familyId: accounts.id, firstName: account.prenom, lastName: account.nom, email: email, @@ -1363,9 +1398,6 @@ export default function App() { if (error.name !== 'AbortError') { messages.submitButtonText = "Échec de la connexion"; messages.submitErrorMessage = "Error: " + error.message; - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); - } } }) .finally(() => { @@ -1419,11 +1451,6 @@ export default function App() { } setTokenState((old) => (response?.token || old)); }) - .catch((error) => { - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); - } - }) .finally(() => { abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); }) @@ -1472,11 +1499,6 @@ export default function App() { } setTokenState((old) => (response?.token || old)); }) - .catch((error) => { - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); - } - }) .finally(() => { abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); }) @@ -1536,11 +1558,6 @@ export default function App() { } setTokenState((old) => (response?.token || old)); }) - .catch((error) => { - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); - } - }) .finally(() => { abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); }) @@ -1602,10 +1619,6 @@ export default function App() { requireLogin(); } setTokenState(old => responseData?.token || old); - } catch (error) { - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); - } } finally { abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); } @@ -1650,16 +1663,109 @@ export default function App() { } setTokenState((old) => (response?.token || old)); }) - .catch((error) => { - if (error.message === "Unexpected token 'P', \"Proxy error\" is not valid JSON") { - setProxyError(true); + .finally(() => { + abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); + }) + } + + + async function fetchMessages(controller = (new AbortController())) { + abortControllers.current.push(controller); + const userId = activeAccount; + const data = { + anneeMessages: getUserSettingValue("isSchoolYearEnabled") ? getUserSettingValue("schoolYear").join("-") : getCurrentSchoolYear().join("-"), + } + fetch( + getProxiedURL(`https://api.ecoledirecte.com/v3/${accountsListState[userId].accountType === "E" ? "eleves/" + accountsListState[userId].id : "familles/" + accountsListState[userId].familyId}/messages.awp?force=false&typeRecuperation=received&idClasseur=0&orderBy=date&order=desc&query=&onlyRead=&page=0&itemsPerPage=100&getAll=0&verbe=get&v=${apiVersion}`, true), + { + method: "POST", + headers: { + "x-token": tokenState + }, + body: `data=${JSON.stringify(data)}`, + signal: controller.signal, + referrerPolicy: "no-referrer", + }, + ) + .then((response) => response.json()) + .then((response) => { + console.log(".then ~ response:", response) + let code; + if (accountsListState[activeAccount].firstName === "Guest") { + code = 49969; + } else { + code = response.code; + } + if (code === 200) { + changeUserData("sortedMessages", sortMessages(response.data)); + } else if (code === 520 || code === 525) { + // token invalide + requireLogin(); + } else if (code === 49969) { + // TODO: add data/messages.json for guest user + // import("./data/messages.json").then((module) => { + // changeUserData("sortedMessages", sortMessages(module.data));; + // }) + } + setTokenState((old) => (response?.token || old)); + }) + .finally(() => { + abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); + }) + } + + async function fetchMessageContent(id, controller) { + const oldSortedMessages = useUserData("sortedMessages").get(); + if (oldSortedMessages) { + const targetMessageIdx = oldSortedMessages.findIndex((item) => item.id === id); + if (oldSortedMessages[targetMessageIdx].content !== null) { + return; + } + } + abortControllers.current.push(controller); + const userId = activeAccount; + const data = { + anneeMessages: getUserSettingValue("isSchoolYearEnabled") ? getUserSettingValue("schoolYear").join("-") : getCurrentSchoolYear().join("-"), + } + fetch( + getProxiedURL(`https://api.ecoledirecte.com/v3/${accountsListState[userId].accountType === "E" ? "eleves/" + accountsListState[userId].id : "familles/" + accountsListState[userId].familyId}/messages/${id}.awp?verbe=get&mode=destinataire&v=${apiVersion}`, true), + { + method: "POST", + headers: { + "x-token": tokenState + }, + body: `data=${JSON.stringify(data)}`, + signal: controller.signal, + referrerPolicy: "no-referrer", + }, + ) + .then((response) => response.json()) + .then((response) => { + let code; + if (accountsListState[activeAccount].firstName === "Guest") { + code = 49969; + } else { + code = response.code; + } + if (code === 200) { + sortMessageContent(response.data) + } else if (code === 520 || code === 525) { + // token invalide + requireLogin(); + } else if (code === 49969) { + // TODO: add data/messages.json for guest user + // import("./data/messages.json").then((module) => { + // sortMessageContent(module.data) + // }) } + setTokenState((old) => (response?.token || old)); }) .finally(() => { abortControllers.current.splice(abortControllers.current.indexOf(controller), 1); }) } + async function fetchSchoolLife(controller = (new AbortController())) { abortControllers.current.push(controller); const data = { @@ -2062,7 +2168,7 @@ export default function App() { path: "messaging" }, { - element: , + element: , path: ":userId/messaging" }, ], diff --git a/src/components/app/Messaging/Inbox.css b/src/components/app/Messaging/Inbox.css new file mode 100644 index 00000000..98e4ca1e --- /dev/null +++ b/src/components/app/Messaging/Inbox.css @@ -0,0 +1,91 @@ + + +#messaging .inbox-window-header { + height: 50px; + border-radius: 15px 15px 0 0; + box-shadow: none; + border-bottom: 1px solid rgba(var(--text-color-alt), .5); + position: relative; +} + +#inbox { + height: 100%; +} + +#inbox .inbox-search-input { + background-color: rgb(var(--background-color-0), .5); + border: none; + background-color: none; + border-radius: 0; +} +#inbox .inbox-search-input .text-input-container { + border-radius: 0; + border: none; + background-color: none; +} +#inbox .inbox-search-input .text-input-container:is(:hover, :focus-within) { + background-color: rgb(var(--background-color-0), rgb(var(--text-color-alt), .5)); +} + +#inbox .messages-container { + height: 100%; +} + +#inbox .messages-container ul { + list-style-type: none; +} + +#inbox .message-container { + position: relative; + text-align: left; + padding: 10px; + border-left: 4px solid rgb(var(--text-color-alt), .5); + border-bottom: 1px solid rgb(var(--text-color-alt), .5); + opacity: .6; + outline: none; + cursor: pointer; +} + +#inbox .message-container:last-child { + border-bottom: none; +} + +#inbox .message-container[data-read=false] { + border-left: 4px solid rgb(var(--text-color-alt)); + padding-left: 6px; + opacity: 1; +} + +#inbox .message-container:is(:hover, :focus-visible) { + background-color: rgba(var(--background-color-0), .2); +} +#inbox .message-container.selected { + opacity: .8; + background-color: rgba(var(--background-color-focus)); +} + +#inbox .message-container :is(.message-author, .message-date) { + color: rgb(var(--text-color-alt)) +} + +#inbox .message-container .message-subject { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + width: 100%; +} +#inbox .message-container .attachment-icon { + height: 16px; + transform: scale(1.3); +} + +:fullscreen #inbox ul { + display: flex; + flex-flow: row wrap; +} +:fullscreen .message-container { + flex-basis: 25%; +} +:fullscreen #inbox .message-container:last-child { + border-bottom: none; +} diff --git a/src/components/app/Messaging/Inbox.jsx b/src/components/app/Messaging/Inbox.jsx new file mode 100644 index 00000000..b29e1c22 --- /dev/null +++ b/src/components/app/Messaging/Inbox.jsx @@ -0,0 +1,73 @@ +import { useState, useEffect, useContext } from "react"; + +import { AppContext } from "../../../App"; + +import "./Inbox.css"; +import ScrollShadedDiv from "../../generic/CustomDivs/ScrollShadedDiv"; +import TextInput from "../../generic/UserInputs/TextInput"; +import { removeAccents } from "../../../utils/utils"; +import AttachmentIcon from "../../graphics/AttachmentIcon"; + + +export default function Inbox({ isLoggedIn, activeAccount, selectedMessage, setSelectedMessage }) { + // States + const { useUserData } = useContext(AppContext); + const [search, setSearch] = useState(""); + const messages = useUserData("sortedMessages").get(); + console.log("Inbox ~ messages:", messages) + + // behavior + // TODO: handle keyboard navigation + const handleClick = (message) => { + setSelectedMessage(message.id); + } + + const handleChange = (event) => { + setSearch(event.target.value) + } + + const filterResearch = (message) => { + let regexp; + try { + regexp = new RegExp(removeAccents(search.toLowerCase())); + } catch {return -1} + const filterBy = [message.subject, message.from.name, message.content?.content, message.files?.map((file) => file.name)].flat(); + console.log("filterResearch ~ filterBy:", filterBy) + for (let filter of filterBy) { + if (filter) { + filter = removeAccents(filter.toLowerCase()); + if (regexp.test(filter)) { + return true; + } + } + } + return false; + } + + // JSX + return ( +
+ {messages !== undefined + ? (messages.length > 0 + ? + +
    + {messages.filter(filterResearch).map((message) =>
  • handleClick(message)} key={message.id} role="button" tabIndex={0}> +

    {message.from.name} {message.files?.length > 0 && }

    +

    {message.subject}

    +

    {(new Date(message.date)).toLocaleDateString("fr-FR", { + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + })}

    +
  • )} +
+
+ :

Vous n'avez reçu aucun message. Profitez bien de votre isolement social ^^

+ ) + :

content-loader

+ } +
+ ) +} \ No newline at end of file diff --git a/src/components/app/Messaging/MessageReader.css b/src/components/app/Messaging/MessageReader.css new file mode 100644 index 00000000..e8251768 --- /dev/null +++ b/src/components/app/Messaging/MessageReader.css @@ -0,0 +1,96 @@ + +.message-reader-window-header { + padding-inline: 15px; +} + +#message-reader { + height: 100%; +} + +#message-reader hr { + border: none; + height: 2px; + background-color: rgb(var(--text-color-alt), .5); +} + +#message-reader .email-header { + display: flex; + flex-flow: column nowrap; + padding: 10px; +} + +#message-reader .email-header .author { + color: rgb(var(--text-color-alt)); +} + +#message-reader .email-header h3 { + text-align: center; +} + +#message-reader .email-header .send-date { + text-align: right; + color: rgb(var(--text-color-alt)) +} + +#message-reader .message-container { + display: flex; + flex-flow: column nowrap; + background-color: rgba(var(--background-color-2), 1); + height: 100%; +} + +#message-reader .message-content-container { + height: 100%; +} + +#message-reader .message-content { + padding: 20px; + overflow: auto; +} +#message-reader .message-content > div { + max-width: 800px; + margin: 0 auto; +} + +#message-reader .email-footer { + height: 100px; + overflow: auto; +} + +#message-reader .attachments-container { + list-style-type: none; + height: 100%; + width: max-content; + padding: 15px; + display: flex; + flex-flow: row nowrap; + gap: 15px; +} + +#message-reader .attachments-container .attachment { + height: 100%; + padding-inline: 15px; + background-color: transparent; + font-size: var(--font-size-16); + border: 1px solid rgba(var(--text-color-alt), .5); + border-radius: 8px; + + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: 15px; + outline: none; + cursor: pointer; + transition: .1s; +} + +#message-reader .attachments-container .attachment:is(:hover, :focus-visible) { + background-color: rgba(var(--text-color-alt), .1); +} +#message-reader .attachments-container .attachment:active { + background-color: rgba(var(--text-color-alt), .2); +} + +#message-reader .attachments-container .attachment .download-icon { + height: 20px; +} diff --git a/src/components/app/Messaging/MessageReader.jsx b/src/components/app/Messaging/MessageReader.jsx new file mode 100644 index 00000000..526e3109 --- /dev/null +++ b/src/components/app/Messaging/MessageReader.jsx @@ -0,0 +1,60 @@ +import { useState, useEffect, useContext } from "react"; + +import { AppContext } from "../../../App"; + +import "./MessageReader.css"; +import EncodedHTMLDiv from "../../generic/CustomDivs/EncodedHTMLDiv"; +import FileComponent from "../../generic/FileComponent"; +import { capitalizeFirstLetter } from "../../../utils/utils"; +import ScrollShadedDiv from "../../generic/CustomDivs/ScrollShadedDiv"; +import DownloadIcon from "../../graphics/DownloadIcon"; + + +export default function MessageReader({ selectedMessage }) { + // States + const { useUserData, actualDisplayTheme } = useContext(AppContext); + const messages = useUserData("sortedMessages").get(); + const message = messages ? messages.find((item) => item.id === selectedMessage) : null; + console.log("MessageReader ~ message:", message) + + // behavior + + // JSX + return ( +
+ {selectedMessage !== null + ? message?.content + ?
+
+

{message && message?.from?.name}

+

{message && capitalizeFirstLetter(message?.subject)}

+

{message && message?.date && (new Date(message.date).toLocaleDateString("fr-FR", { + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + }))}

+
+
+ + + {message?.content && message?.content?.content} + + {message && (message?.files?.length > 0 + ? <> +
+
+
    + {message.files.map((file) =>
  • )} +
+
+ + : null)} + +
+ :

content-loader

+ :

Sélectionnez un message dans votre boîte de réception pour le visualiser ici !

+ } +
+ ) +} \ No newline at end of file diff --git a/src/components/app/Messaging/Messaging.jsx b/src/components/app/Messaging/Messaging.jsx index d6c2c437..7501f0a4 100644 --- a/src/components/app/Messaging/Messaging.jsx +++ b/src/components/app/Messaging/Messaging.jsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useContext } from "react"; import { WindowsContainer, WindowsLayout, @@ -8,45 +8,70 @@ import { WindowContent } from "../../generic/Window"; +import { AppContext } from "../../../App"; import "./Messaging.css"; +import Inbox from "./Inbox"; +import MessageReader from "./MessageReader"; -export default function Messaging({ }) { +export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fetchMessageContent }) { // States + const { useUserData } = useContext(AppContext); + const [selectedMessage, setSelectedMessage] = useState(null); + const messages = useUserData("sortedMessages"); // behavior useEffect(() => { document.title = "Messagerie • Ecole Directe Plus"; }, []); + useEffect(() => { + const controller = new AbortController(); + if (isLoggedIn) { + if (messages.get() === undefined) { + console.log("fetching messages"); + fetchMessages(controller); + setSelectedMessage(null); + } + } + + return () => { + controller.abort(); + } + }, [isLoggedIn, activeAccount, messages.get()]); + + useEffect(() => { + const controller = new AbortController(); + console.log("useEffect ~ selectedMessage:", selectedMessage) + if (selectedMessage !== null) { + fetchMessageContent(selectedMessage, controller); + } + + return () => { + controller.abort(); + } + }, [selectedMessage]); + // JSX return (
- - -

Dossiers

-
- - - -
- - + +

Boîte de réception

- +
- - + +

Message

- +
diff --git a/src/components/generic/CustomDivs/EncodedHTMLDiv.css b/src/components/generic/CustomDivs/EncodedHTMLDiv.css new file mode 100644 index 00000000..0e52eb7c --- /dev/null +++ b/src/components/generic/CustomDivs/EncodedHTMLDiv.css @@ -0,0 +1,3 @@ +.html-encoded-div > div > p { + margin-bottom: var(--font-size-16); +} \ No newline at end of file diff --git a/src/components/generic/CustomDivs/EncodedHTMLDiv.jsx b/src/components/generic/CustomDivs/EncodedHTMLDiv.jsx index 108baa3c..1cccef7c 100644 --- a/src/components/generic/CustomDivs/EncodedHTMLDiv.jsx +++ b/src/components/generic/CustomDivs/EncodedHTMLDiv.jsx @@ -2,16 +2,18 @@ import { useState, useRef, useEffect } from "react"; import { clearHTML } from "../../../utils/html"; -export default function EncodedHTMLDiv({ children, nonEncodedChildren=null, backgroundColor, ...props }) { +import "./EncodedHTMLDiv.css" + +export default function EncodedHTMLDiv({ children, nonEncodedChildren=null, backgroundColor, className="", ...props }) { const [backgroundColorState, setBackgroundColorState] = useState(null); const divRef = useRef(null); useEffect(() => { // dynamically change the background color parameter of `clearHTML` to ensure nice contrasts - let textColor = getComputedStyle(divRef.current).getPropertyValue("background-color"); - let condition = textColor.split(",").length > 3 && textColor.slice(-2, -1) === "0"; + let backgroundColor = getComputedStyle(divRef.current).getPropertyValue("background-color"); + let condition = backgroundColor.split(",").length > 3 && backgroundColor.slice(-2, -1) === "0"; if (!condition) { - let match = textColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+)?\)$/); + let match = backgroundColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+)?\)$/); if (match) { let red = parseInt(match[1]); @@ -25,7 +27,7 @@ export default function EncodedHTMLDiv({ children, nonEncodedChildren=null, back }, []); return ( -
+
{nonEncodedChildren &&
{nonEncodedChildren}
}
diff --git a/src/components/graphics/AttachmentIcon.jsx b/src/components/graphics/AttachmentIcon.jsx new file mode 100644 index 00000000..78c722f5 --- /dev/null +++ b/src/components/graphics/AttachmentIcon.jsx @@ -0,0 +1,10 @@ + + +import "./graphics.css" +export default function AttachmentIcon({ className = "", id = "", alt, ...props }) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/utils/html.js b/src/utils/html.js index e011b3ec..5265c646 100644 --- a/src/utils/html.js +++ b/src/utils/html.js @@ -144,6 +144,10 @@ export function clearHTML(html, backgroundColor, asString=true) { let red = parseInt(match[1]); let green = parseInt(match[2]); let blue = parseInt(match[3]); + if ((red + green + blue) === 0) { + el.style.color = ""; + return + } let hexColor = rgbToHex(red, green, blue); diff --git a/src/utils/utils.js b/src/utils/utils.js index d8871159..9b8dd86c 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -182,4 +182,8 @@ export function textToHSL(str, initialS = 42, initialL = 73, variationS = 10, va const h = Math.round((int % (10 ** 8)) / (10 ** 4)); const s = Math.round((int % (10 ** 12)) / (10 ** 8)); return [360 * (h / 9999), initialS + variationS * (s / 9999), initialL + variationL * (l / 9999)]; // [{0-360}, {70-100}, {40-70}] +} + +export function removeAccents(str) { + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } \ No newline at end of file