Skip to content

Commit 88b68aa

Browse files
committed
Merge branch 'working' of https://github.com/Magic-Fishes/Ecole-Directe-Plus into working
2 parents 6a625d6 + f8c8688 commit 88b68aa

File tree

14 files changed

+202
-32
lines changed

14 files changed

+202
-32
lines changed
Lines changed: 4 additions & 0 deletions
Loading

src/App.jsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function consoleLogEDPLogo() {
7171
consoleLogEDPLogo();
7272

7373
const currentEDPVersion = "0.3.1";
74-
const apiVersion = "4.60.4";
74+
const apiVersion = "4.60.5";
7575

7676
// secret webhooks
7777
const carpeConviviale = "CARPE_CONVIVIALE_WEBHOOK_URL";
@@ -1772,6 +1772,50 @@ export default function App() {
17721772
})
17731773
}
17741774

1775+
async function fetchMessageMarkAsUnread(ids=[], controller) {
1776+
if (ids.length < 1) {
1777+
return;
1778+
}
1779+
abortControllers.current.push(controller);
1780+
const userId = activeAccount;
1781+
const data = {
1782+
anneeMessages: getUserSettingValue("isSchoolYearEnabled") ? getUserSettingValue("schoolYear").join("-") : getCurrentSchoolYear().join("-"),
1783+
action: "marquerCommeNonLu",
1784+
ids: ids
1785+
}
1786+
fetch(
1787+
getProxiedURL(`https://api.ecoledirecte.com/v3/${accountsListState[userId].accountType === "E" ? "eleves/" + accountsListState[userId].id : "familles/" + accountsListState[userId].familyId}/messages.awp?verbe=put&v=${apiVersion}`, true),
1788+
{
1789+
method: "POST",
1790+
headers: {
1791+
"x-token": tokenState
1792+
},
1793+
body: `data=${JSON.stringify(data)}`,
1794+
signal: controller.signal,
1795+
referrerPolicy: "no-referrer",
1796+
},
1797+
)
1798+
.then((response) => response.json())
1799+
.then((response) => {
1800+
let code;
1801+
if (accountsListState[activeAccount].firstName === "Guest") {
1802+
code = 49969;
1803+
} else {
1804+
code = response.code;
1805+
}
1806+
if (code === 200) {
1807+
// message successfully marked as unread
1808+
} else if (code === 520 || code === 525) {
1809+
// token invalide
1810+
requireLogin();
1811+
}
1812+
setTokenState((old) => (response?.token || old));
1813+
})
1814+
.finally(() => {
1815+
abortControllers.current.splice(abortControllers.current.indexOf(controller), 1);
1816+
})
1817+
}
1818+
17751819

17761820
async function fetchSchoolLife(controller = (new AbortController())) {
17771821
abortControllers.current.push(controller);
@@ -2175,7 +2219,7 @@ export default function App() {
21752219
path: "messaging"
21762220
},
21772221
{
2178-
element: <Messaging isLoggedIn={isLoggedIn} activeAccount={activeAccount} fetchMessages={fetchMessages} fetchMessageContent={fetchMessageContent} />,
2222+
element: <Messaging isLoggedIn={isLoggedIn} activeAccount={activeAccount} fetchMessages={fetchMessages} fetchMessageContent={fetchMessageContent} fetchMessageMarkAsUnread={fetchMessageMarkAsUnread} />,
21792223
path: ":userId/messaging"
21802224
},
21812225
],

src/components/app/Header/Header.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export default function Header({ currentEDPVersion, token, accountsList, setActi
169169
link: `/app/${activeAccount}/homeworks`,
170170
icon: <HomeworksIconOfficial />,
171171
notifications: notifications?.homeworks || 0,
172-
isNew: true
172+
isNew: false
173173
},
174174
{
175175
enabled: accountsList[activeAccount]?.modules?.filter((item) => item.code === "EDT").map((item) => item.enable).includes(true) ?? true,
@@ -189,7 +189,7 @@ export default function Header({ currentEDPVersion, token, accountsList, setActi
189189
link: `/app/${activeAccount}/messaging`,
190190
icon: <MessagingIcon />,
191191
notifications: notifications?.messaging || 0,
192-
isNew: false
192+
isNew: true
193193
}
194194
]
195195
// Behavior

src/components/app/Header/HeaderNavigationButton.css

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,30 @@
6969
}
7070

7171
.header-button .notifications.new {
72-
background-color: rgb(var(--background-color-1));
72+
background-color: black;
73+
color: gold;
7374
width: 52px;
74-
color: rgb(var(--text-color-main));
75-
animation: new-feature-notification-animation .8s ease alternate forwards;
76-
animation-iteration-count: 20;
75+
/* animation: new-feature-notification-animation .8s ease alternate forwards;
76+
animation-iteration-count: 20; */
7777
left: 40px;
7878
}
79+
.light .header-button .notifications.new {
80+
background-color: gold;
81+
color: black;
82+
}
7983

8084
.light .header-button .notifications.new {
8185
box-shadow: 0 0 20px rgba(0, 0, 0, .2);
8286
}
8387

84-
@keyframes new-feature-notification-animation {
88+
/* @keyframes new-feature-notification-animation {
8589
from {
8690
scale: .9;
8791
}
8892
to {
8993
scale: 1;
9094
}
91-
}
95+
} */
9296

9397
@keyframes notification-pop-in {
9498
from {

src/components/app/Messaging/Inbox.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
#inbox {
1212
height: 100%;
13+
display: flex;
14+
flex-flow: column nowrap;
1315
}
1416

1517
#inbox .inbox-search-input {
@@ -73,12 +75,49 @@
7375
flex-flow: row nowrap;
7476
justify-content: space-between;
7577
width: 100%;
78+
gap: 5px;
79+
}
80+
81+
#inbox .message-container .message-subject .author-name {
82+
white-space: nowrap;
83+
overflow: hidden;
84+
text-overflow: ellipsis;
85+
flex-shrink: 1;
7686
}
87+
#inbox .message-container .message-subject .actions {
88+
display: flex;
89+
flex-flow: row nowrap;
90+
gap: 12px;
91+
}
92+
7793
#inbox .message-container .attachment-icon {
7894
height: 16px;
7995
transform: scale(1.3);
8096
}
8197

98+
#inbox .message-container .mark-as-unread {
99+
display: none;
100+
border: none;
101+
background-color: transparent;
102+
transition: .1s;
103+
cursor: pointer;
104+
outline: none;
105+
}
106+
#inbox .message-container .mark-as-unread:is(:hover, :focus-visible) .mark-as-unread-icon path {
107+
stroke: rgb(var(--text-color-alt));
108+
}
109+
#inbox .message-container .mark-as-unread .mark-as-unread-icon {
110+
height: 16px;
111+
transform: scale(1.35) translateY(-1px);
112+
}
113+
#inbox .message-container:is(:hover, :focus-visible, :focus-within, .selected) .mark-as-unread {
114+
display: block;
115+
}
116+
#inbox .message-container[data-read=false] .mark-as-unread {
117+
opacity: .2;
118+
pointer-events: none;
119+
}
120+
82121
:fullscreen #inbox ul {
83122
display: flex;
84123
flex-flow: row wrap;

src/components/app/Messaging/Inbox.jsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,44 @@ import ScrollShadedDiv from "../../generic/CustomDivs/ScrollShadedDiv";
77
import TextInput from "../../generic/UserInputs/TextInput";
88
import { removeAccents } from "../../../utils/utils";
99
import AttachmentIcon from "../../graphics/AttachmentIcon";
10+
import MarkAsUnread from "../../graphics/MarkAsUnread";
1011

1112

12-
export default function Inbox({ isLoggedIn, activeAccount, selectedMessage, setSelectedMessage }) {
13+
export default function Inbox({ selectedMessage, setSelectedMessage, fetchMessageMarkAsUnread }) {
1314
// States
1415
const { useUserData } = useContext(AppContext);
1516
const [search, setSearch] = useState("");
16-
const messages = useUserData("sortedMessages").get();
17-
console.log("Inbox ~ messages:", messages)
17+
const messages = useUserData("sortedMessages");
1818

1919
// behavior
20-
// TODO: handle keyboard navigation
2120
const handleClick = (message) => {
2221
setSelectedMessage(message.id);
2322
}
23+
24+
const handleKeyDown = (event, msg) => {
25+
if (event.key === "Enter" || event.key === " ") {
26+
handleClick(msg);
27+
}
28+
}
29+
30+
const handleMarkAsUnread = (event, msg) => {
31+
event.preventDefault();
32+
event.stopPropagation();
33+
const controller = new AbortController();
34+
fetchMessageMarkAsUnread([msg.id], controller);
35+
36+
if (msg.id === selectedMessage) {
37+
setSelectedMessage(null);
38+
}
39+
40+
// mark as unread locally and kick the content so as to trigger a refetch the next reading (as the "mark as read" feature is trigger when fetching the message)
41+
const oldMsg = messages.get();
42+
const msgIdx = oldMsg.findIndex((item) => item.id === msg.id);
43+
oldMsg[msgIdx].read = false;
44+
oldMsg[msgIdx].content = null;
45+
messages.set(oldMsg);
46+
47+
}
2448

2549
const handleChange = (event) => {
2650
setSearch(event.target.value)
@@ -32,7 +56,6 @@ export default function Inbox({ isLoggedIn, activeAccount, selectedMessage, setS
3256
regexp = new RegExp(removeAccents(search.toLowerCase()));
3357
} catch {return -1}
3458
const filterBy = [message.subject, message.from.name, message.content?.content, message.files?.map((file) => file.name)].flat();
35-
console.log("filterResearch ~ filterBy:", filterBy)
3659
for (let filter of filterBy) {
3760
if (filter) {
3861
filter = removeAccents(filter.toLowerCase());
@@ -47,13 +70,13 @@ export default function Inbox({ isLoggedIn, activeAccount, selectedMessage, setS
4770
// JSX
4871
return (
4972
<div id="inbox">
50-
{messages !== undefined
51-
? (messages.length > 0
73+
<TextInput onChange={handleChange} value={search} textType={"text"} placeholder={"Rechercher"} className="inbox-search-input" />
74+
{messages.get() !== undefined
75+
? (messages.get().length > 0
5276
? <ScrollShadedDiv className="messages-container">
53-
<TextInput onChange={handleChange} value={search} textType={"text"} placeholder={"Rechercher"} className="inbox-search-input" />
5477
<ul>
55-
{messages.filter(filterResearch).map((message) => <li className={"message-container" + (selectedMessage === message.id ? " selected" : "")} data-read={message.read} onClick={() => handleClick(message)} key={message.id} role="button" tabIndex={0}>
56-
<h4 className="message-subject">{message.from.name} {message.files?.length > 0 && <AttachmentIcon className="attachment-icon" />}</h4>
78+
{messages.get().filter(filterResearch).map((message) => <li className={"message-container" + (selectedMessage === message.id ? " selected" : "")} data-read={message.read} onClick={() => handleClick(message)} onKeyDown={(event) => handleKeyDown(event, message)} key={message.id} role="button" tabIndex={0}>
79+
<h4 className="message-subject"><span className="author-name">{message.from.name}</span> <span className="actions"><button disabled={!message.read} onClick={(event) => handleMarkAsUnread(event, message)} className="mark-as-unread" title="Marquer comme non lu"><MarkAsUnread className="mark-as-unread-icon"/></button> {message.files?.length > 0 && <AttachmentIcon className="attachment-icon" />}</span></h4>
5780
<p className="message-author">{message.subject}</p>
5881
<p className="message-date">{(new Date(message.date)).toLocaleDateString("fr-FR", {
5982
month: "long",

src/components/app/Messaging/MessageReader.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,18 @@
4747
padding: 20px;
4848
overflow: auto;
4949
}
50+
51+
/* We take some liberties to make the emails more readable overall */
5052
#message-reader .message-content > div {
5153
max-width: 800px;
5254
margin: 0 auto;
5355
}
5456

57+
#message-reader .message-content > div > p:has(> img) {
58+
text-align: center;
59+
}
60+
/* end */
61+
5562
#message-reader .email-footer {
5663
height: 100px;
5764
overflow: auto;

src/components/app/Messaging/MessageReader.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export default function MessageReader({ selectedMessage }) {
1515
const { useUserData, actualDisplayTheme } = useContext(AppContext);
1616
const messages = useUserData("sortedMessages").get();
1717
const message = messages ? messages.find((item) => item.id === selectedMessage) : null;
18-
console.log("MessageReader ~ message:", message)
1918

2019
// behavior
2120

@@ -45,7 +44,7 @@ export default function MessageReader({ selectedMessage }) {
4544
<hr />
4645
<div className="email-footer">
4746
<ul className="attachments-container">
48-
{message.files.map((file) => <li><button className="attachment" onClick={() => file.download()}><DownloadIcon className="download-icon" />{file.name + "." + file.extension}</button></li>)}
47+
{message.files.map((file) => <li key={file.id}><button className="attachment" onClick={() => file.download()}><DownloadIcon className="download-icon" />{file.name + "." + file.extension}</button></li>)}
4948
</ul>
5049
</div>
5150
</>

src/components/app/Messaging/Messaging.jsx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

2-
import { useState, useEffect, useContext } from "react";
2+
import { useState, useRef, useEffect, useContext } from "react";
3+
import { useNavigate, useLocation, Navigate, Link } from "react-router-dom";
4+
35
import {
46
WindowsContainer,
57
WindowsLayout,
@@ -15,11 +17,16 @@ import Inbox from "./Inbox";
1517
import MessageReader from "./MessageReader";
1618

1719

18-
export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fetchMessageContent }) {
20+
export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fetchMessageContent, fetchMessageMarkAsUnread }) {
1921
// States
22+
const navigate = useNavigate();
23+
const location = useLocation();
24+
2025
const { useUserData } = useContext(AppContext);
21-
const [selectedMessage, setSelectedMessage] = useState(null);
26+
const [selectedMessage, setSelectedMessage] = useState(isNaN(parseInt(location.hash.slice(1))) ? null : parseInt(location.hash.slice(1)));
27+
const oldSelectedMessage = useRef(selectedMessage);
2228
const messages = useUserData("sortedMessages");
29+
2330

2431
// behavior
2532
useEffect(() => {
@@ -30,7 +37,6 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe
3037
const controller = new AbortController();
3138
if (isLoggedIn) {
3239
if (messages.get() === undefined) {
33-
console.log("fetching messages");
3440
fetchMessages(controller);
3541
setSelectedMessage(null);
3642
}
@@ -43,14 +49,43 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe
4349

4450
useEffect(() => {
4551
const controller = new AbortController();
46-
console.log("useEffect ~ selectedMessage:", selectedMessage)
4752
if (selectedMessage !== null) {
4853
fetchMessageContent(selectedMessage, controller);
54+
const parsedHash = parseInt(location.hash.slice(1));
55+
if (parsedHash !== selectedMessage) {
56+
const newHash = "#" + selectedMessage;
57+
navigate(newHash);
58+
}
59+
} else {
60+
if (location.hash) {
61+
navigate("#");
62+
}
4963
}
5064

5165
return () => {
5266
controller.abort();
5367
}
68+
}, [location, selectedMessage]);
69+
70+
useEffect(() => {
71+
if (oldSelectedMessage.current !== selectedMessage) {
72+
return;
73+
}
74+
const parsedHash = parseInt(location.hash.slice(1));
75+
if (!isNaN(parsedHash) && parsedHash !== selectedMessage) {
76+
if (messages.get()) {
77+
const doesMessageExist = messages.get()?.findIndex((item) => item.id === parsedHash) !== -1;
78+
if (doesMessageExist) {
79+
setSelectedMessage(parsedHash);
80+
} else {
81+
navigate("#");
82+
}
83+
}
84+
}
85+
}, [location, messages.get(), oldSelectedMessage.current, selectedMessage]);
86+
87+
useEffect(() => {
88+
oldSelectedMessage.current = selectedMessage;
5489
}, [selectedMessage]);
5590

5691
// JSX
@@ -63,7 +98,7 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe
6398
<h2>Boîte de réception</h2>
6499
</WindowHeader>
65100
<WindowContent>
66-
<Inbox isLoggedIn={isLoggedIn} activeAccount={activeAccount} selectedMessage={selectedMessage} setSelectedMessage={setSelectedMessage} />
101+
<Inbox isLoggedIn={isLoggedIn} activeAccount={activeAccount} selectedMessage={selectedMessage} setSelectedMessage={setSelectedMessage} fetchMessageMarkAsUnread={fetchMessageMarkAsUnread} />
67102
</WindowContent>
68103
</Window>
69104
<Window growthFactor={3} className="message-content" allowFullscreen={true}>

0 commit comments

Comments
 (0)