Add sending notifications for new announcements

This commit is contained in:
pikiou 2022-02-17 17:29:27 +01:00
parent 140891c4c0
commit 1e10f773fb
8 changed files with 213 additions and 63 deletions

View File

@ -1,23 +1,9 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.mainMenu {
margin: 0;
padding: 0;
list-style: none;
@include nav-menu;
}
.mainMenuItem {
display: inline-block;
margin: 0 4px;
a {
font-size: 0.9em;
font-weight: bold;
color: $color-black;
text-decoration: none;
}
a:hover {
color: $color-grey-dark;
}
@include nav-menu-item;
}

View File

@ -2,11 +2,11 @@ import _ from "lodash"
import React, { memo, useCallback, useEffect, useRef, useState } from "react"
import isNode from "detect-node"
import { useDispatch, useSelector } from "react-redux"
import classnames from "classnames"
import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet"
import styles from "./styles.module.scss"
import { selectUserJwtToken } from "../../store/auth"
import { VolunteerNotifs } from "../../services/volunteers"
import LogoutButton from "../LogoutButton/LogoutButton"
// import { TeamWishesForm } from ".."
import { fetchFor as fetchForTeamWishesForm } from "../VolunteerBoard/TeamWishesForm/TeamWishesForm"
@ -144,6 +144,43 @@ const Notifications = ({ volunteerNotifs }: Props): JSX.Element | null => {
)
}
const onSubmit3 = useCallback((): void => {
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 3],
})
)
}, [dispatch, jwtToken, volunteerNotifs])
if (!_.includes(hidden, 3)) {
notifs.push(
<div key="3">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<form onSubmit={onSubmit3}>
<div
className={classnames(styles.notifIntro, styles.notifCentered)}
key="login-intro"
>
La{" "}
<a
href="https://mailchi.mp/3c75c3b3a20f/gazette_2020_02-8978118"
onClick={onSubmit3}
>
gazette de février
</a>{" "}
est disponible !<br />
<div className={styles.formButtons}>
<button type="submit">Ok, masquer</button>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
// const onSubmit3 = useCallback((): void => {
// dispatch(
// fetchVolunteerNotifsSet(jwtToken, 0, {
@ -154,7 +191,7 @@ const Notifications = ({ volunteerNotifs }: Props): JSX.Element | null => {
// if (!_.includes(hidden, 3)) {
// notifs.push(
// <div key="1">
// <div key="4">
// <div className={styles.notificationsPage}>
// <div className={styles.notificationsContent}>
// <TeamWishesForm afterSubmit={onSubmit3} />
@ -395,12 +432,7 @@ Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique.
return null
}
return (
<div>
{notifs.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}
<LogoutButton />
</div>
)
return <div>{notifs.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
export default memo(Notifications)

View File

@ -22,6 +22,9 @@
.notifIntro {
margin-bottom: 10px;
}
.notifCentered {
text-align: center;
}
.formLine {
padding: 5px 0;

View File

@ -32,13 +32,15 @@ import {
} from "./gsheets/volunteers"
import { wishListGet, wishAdd } from "./gsheets/wishes"
import config from "../config"
import notificationsSubscribe from "./notificationsSubscribe"
import { notificationsSubscribe, notificationMain } from "./notifications"
import checkAccess from "./checkAccess"
import { hasGSheetsAccess } from "./gsheets/accessors"
import { addStatus, showStatusAt } from "./status"
checkAccess()
notificationMain()
const app = express()
// Use helmet to secure Express with various HTTP headers

131
src/server/notifications.ts Normal file
View File

@ -0,0 +1,131 @@
import { Request, Response, NextFunction } from "express"
import webpush from "web-push"
import {
Announcement,
AnnouncementWithoutId,
translationAnnouncement,
} from "../services/announcement"
import { translationVolunteer, Volunteer, VolunteerWithoutId } from "../services/volunteers"
import { getSheet } from "./gsheets/accessors"
const publicKey = process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY
const privateKey = process.env.FORCE_ORANGE_PRIVATE_VAPID_KEY
const hasPushAccess = publicKey && privateKey && !__DEV__ && !__LOCAL__
if (hasPushAccess) {
webpush.setVapidDetails("mailto: contact@parisestludique.fr", publicKey, privateKey)
}
type Payload = {
message: string
}
export function sendNotifToVolunteer(volunteer: Volunteer, payload: Payload): void {
if (volunteer.acceptsNotifs !== "oui") {
console.error(`Volunteer refuses notifs`)
} else {
const subscription = JSON.parse(volunteer.pushNotifSubscription)
if (!subscription) {
console.error(`Volunteer has no notif subscription`)
} else if (hasPushAccess) {
const stringifiedPayload = JSON.stringify(payload)
webpush
.sendNotification(subscription, stringifiedPayload)
.then((result) => console.error(result))
.catch((e) => console.error(e.stack))
} else {
console.error(
`Fake sending push notif to ${JSON.stringify(subscription)} of ${JSON.stringify(
payload
)})}`
)
}
}
}
export function sendNotifToSubscription(
subscription: webpush.PushSubscription,
payload: Payload
): void {
if (hasPushAccess) {
const stringifiedPayload = JSON.stringify(payload)
webpush
.sendNotification(subscription, stringifiedPayload)
.then((result) => console.error(result))
.catch((e) => console.error(e.stack))
} else {
console.error(
`Fake sending push notif to ${JSON.stringify(subscription)} of ${JSON.stringify(
payload
)})}`
)
}
}
export function notificationsSubscribe(
request: Request,
response: Response,
_next: NextFunction
): void {
sendNotifToSubscription(request.body, {
message:
"Clique-moi pour voir la gazette de février dans la page Annonces !\nFini les emails, cette notification sera notre seul moyen de te prévenir :)",
})
response.status(200).json({ success: true })
}
export function notificationMain(): void {
setInterval(notifyAboutAnnouncement, 5 * 60 * 1000)
setTimeout(notifyAboutAnnouncement, 60 * 1000)
}
async function notifyAboutAnnouncement(): Promise<void> {
const announcementSheet = getSheet<AnnouncementWithoutId, Announcement>(
"Announcements",
new Announcement(),
translationAnnouncement
)
const announcementList = await announcementSheet.getList()
if (!announcementList) {
return
}
const toSend = announcementList.find((a) => !a.informedWithNotif)
if (!toSend) {
return
}
const volunteerSheet = getSheet<VolunteerWithoutId, Volunteer>(
"Volunteers",
new Volunteer(),
translationVolunteer
)
const volunteerList = await volunteerSheet.getList()
if (!volunteerList) {
return
}
const audience = volunteerList.filter(
(v) =>
v.acceptsNotifs === "oui" &&
!v.discordId &&
(v.active === "oui" || v.active === "peut-etre")
)
console.error(
`Sending announcement ${toSend.title} to ${audience
.map((v) => `${v.firstname} #${v.id}`)
.join(", ")}`
)
audience.forEach((v) => {
sendNotifToVolunteer(v, {
message:
"Clique-moi pour voir la gazette de février dans la page Annonces !\n\nFini les emails/spam, cette notification est notre nouveau moyen de te prévenir :)",
})
})
toSend.informedWithNotif = true
await announcementSheet.set(toSend)
}

View File

@ -1,37 +0,0 @@
import { Request, Response, NextFunction } from "express"
import webpush from "web-push"
const publicKey = process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY
const privateKey = process.env.FORCE_ORANGE_PRIVATE_VAPID_KEY
const hasPushAccess = publicKey && privateKey
if (hasPushAccess) {
webpush.setVapidDetails("mailto: contact@parisestludique.fr", publicKey, privateKey)
}
export default function notificationsSubscribe(
request: Request,
response: Response,
_next: NextFunction
): void {
const subscription = request.body
const payload = JSON.stringify({
title: "Hello!",
body: "It works.",
})
if (hasPushAccess) {
webpush
.sendNotification(subscription, payload)
.then((result) => console.log(result))
.catch((e) => console.log(e.stack))
} else {
console.error(
`Fake sending push notif to ${JSON.stringify(subscription)} of ${JSON.stringify(
payload
)})}`
)
}
response.status(200).json({ success: true })
}

View File

@ -8,6 +8,8 @@ export class Announcement {
title = ""
url = ""
informedWithNotif = false
}
export const translationAnnouncement: { [k in keyof Announcement]: string } = {
@ -16,6 +18,7 @@ export const translationAnnouncement: { [k in keyof Announcement]: string } = {
type: "type",
title: "titre",
url: "url",
informedWithNotif: "informéAvecUneNotif",
}
export const elementName = "Announcement"

View File

@ -36,6 +36,36 @@
background-color: $color-grey-lighter;
}
@mixin nav-menu($desktopWidth: 520px) {
margin: 0;
padding: 0;
list-style: none;
@include desktop {
padding: 5px;
}
}
@mixin nav-menu-item($desktopWidth: 520px) {
display: inline-block;
margin: 0 4px;
@include desktop {
padding: 2px;
}
a {
font-size: 0.9em;
font-weight: bold;
color: $color-black;
text-decoration: none;
}
a:hover {
color: $color-grey-dark;
}
}
@mixin clear-ul-style {
margin: 0;
padding: 0;