Skip to content

Commit

Permalink
Notifications refactor
Browse files Browse the repository at this point in the history
- Introduces server rendered notification views for easier PWA push
  notifications handling. Without this change, the PWA had to be
  updated for notifications of new notification types to reach the
  end-user. This is no longer the case since rendering is now, by
  default, handled on the server.

- Moved favicon.png from the assets dir to the public dir.

- Set the default cache-control value for static files served to an
  hour (previously, it was a day).
  • Loading branch information
previnder committed Jan 12, 2025
1 parent 9b86f7c commit e45167f
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 357 deletions.
567 changes: 405 additions & 162 deletions core/notification.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/httputil/gzip_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func serveGzip(w http.ResponseWriter, r *http.Request, fs http.FileSystem, name
}

if w.Header().Get("Cache-Control") == "" {
w.Header().Add("Cache-Control", "public, max-age=86400, immutable")
w.Header().Add("Cache-Control", "public, max-age=3600, immutable")
}
http.ServeContent(w, r, name, info.ModTime(), f)
}
Expand Down
11 changes: 11 additions & 0 deletions internal/images/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,17 @@ func (m *Image) AppendCopy(name string, boxWidth, boxHeight int, fit ImageFit, f
return copy
}

// SelectCopy selects the first copy of this image that matches the name. If no
// copy is found, it returns nil.
func (m *Image) SelectCopy(name string) *ImageCopy {
for _, copy := range m.Copies {
if copy.Name == name {
return copy
}
}
return nil
}

// An ImageCopy is a transformed (size, format, and/or fit changed) copy of an
// Image.
type ImageCopy struct {
Expand Down
17 changes: 17 additions & 0 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"io"
"math/rand"
"strconv"
"strings"
)

Expand Down Expand Up @@ -195,3 +196,19 @@ func ExtractStringsFromMap(m map[string]any, trim bool) map[string]string {
}
return strMap
}

func StringCount(number int, thingName, thingNameMultiple string, excludeNumber bool) string {
var s string
if !excludeNumber {
s = strconv.Itoa(number) + " "
}
if thingNameMultiple == "" {
thingNameMultiple = thingName + "s"
}
if number == 1 {
s += thingName
} else {
s += thingNameMultiple
}
return s
}
11 changes: 7 additions & 4 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ func (s *Server) getNotifications(w *responseWriter, r *request) error {
res.NewCount = user.NumNewNotifications

query := r.urlQueryParams()
if res.Items, res.Next, err = core.GetNotifications(r.ctx, s.db, user.ID, 10, query.Get("next")); err != nil {
if res.Items, res.Next, err = core.GetNotifications(r.ctx, s.db, user.ID, 10, query.Get("next"), query.Get("render") == "true", core.TextFormat(query.Get("format"))); err != nil {
return err
}

Expand All @@ -394,8 +394,10 @@ func (s *Server) getNotification(w *responseWriter, r *request) error {
return errNotLoggedIn
}

query := r.urlQueryParams()

notifID := r.muxVar("notificationID")
notif, err := core.GetNotification(r.ctx, s.db, notifID)
notif, err := core.GetNotification(r.ctx, s.db, notifID, query.Get("render") == "true", core.TextFormat(query.Get("format")))
if err != nil {
if err == sql.ErrNoRows {
return httperr.NewNotFound("notif_not_found", "Notification not found.")
Expand All @@ -407,7 +409,6 @@ func (s *Server) getNotification(w *responseWriter, r *request) error {
return httperr.NewForbidden("not_owner", "")
}

query := r.urlQueryParams()
if r.req.Method == "PUT" {
action := query.Get("action")
switch action {
Expand All @@ -432,8 +433,10 @@ func (s *Server) deleteNotification(w *responseWriter, r *request) error {
return errNotLoggedIn
}

query := r.urlQueryParams()

notifID := r.muxVar("notificationID")
notif, err := core.GetNotification(r.ctx, s.db, notifID)
notif, err := core.GetNotification(r.ctx, s.db, notifID, query.Get("render") == "true", core.TextFormat(query.Get("format")))
if err != nil {
if err == sql.ErrNoRows {
return httperr.NewNotFound("notif_not_found", "Notification not found.")
Expand Down
4 changes: 2 additions & 2 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@
}
})();
</script>
<link rel="shortcut icon" href="./src/assets/imgs/favicon.png" />
<link rel="mask-icon" href="./src/assets/imgs/favicon.png" color="#6b02f2" />
<link rel="shortcut icon" href="/favicon.png" />
<link rel="mask-icon" href="/favicon.png" color="#6b02f2" />
<link rel="apple-touch-icon" href="/logo-manifest-512.png" />
</head>
<body>
Expand Down
File renamed without changes
File renamed without changes
19 changes: 10 additions & 9 deletions ui/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { getNotificationDisplayInformation } from './src/components/Navbar/notification';

const SW_BUILD_ID = import.meta.env.VITE_SW_BUILD_ID;

console.log(`Service worker version: ${SW_BUILD_ID}`);
Expand Down Expand Up @@ -147,17 +145,20 @@ self.addEventListener('fetch', (e) => {
});

const getNotificationInfo = (notification, csrfToken) => {
const { text: title, to, image } = getNotificationDisplayInformation(notification);
let icon = '';
if (notification.icons !== null) {
icon = notification.icons[0];
}
return {
title: title,
title: notification.title,
options: {
body: '',
icon: image.url,
body: notification.body,
icon,
badge: '/discuit-logo-pwa-badge.png',
tag: notification.id,
data: {
notificationId: notification.id,
toURL: `${to}?fromNotif=${notification.id}`,
toURL: notification.toURL,
csrfToken,
},
},
Expand Down Expand Up @@ -190,7 +191,7 @@ self.addEventListener('push', (e) => {
}

const pushNotif = e.data.json();
const res = await fetch(`/api/notifications/${pushNotif.id}`);
const res = await fetch(`/api/notifications/${pushNotif.id}?render=true`);
if (!res.ok) {
console.log('notification error');
return;
Expand Down Expand Up @@ -243,7 +244,7 @@ const closeSeenNotifications = async () => {
if (existingNotifs.length === 0) {
return;
}
const res = await (await fetch('/api/notifications')).json();
const res = await (await fetch('/api/notifications?render=true')).json();
const notifs = res.items || [];
existingNotifs.forEach((exNotif) => {
for (let i = 0; i < notifs.length; i++) {
Expand Down
3 changes: 1 addition & 2 deletions ui/src/components/CommunityProPic.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import favicon from '../assets/imgs/favicon.png';
import favicon from '../../public/favicon.png';
import { selectImageCopyURL } from '../helper';
import { useImageLoaded } from '../hooks';

Expand Down
13 changes: 9 additions & 4 deletions ui/src/components/Navbar/NotificationItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ButtonMore } from '../Button';
import Dropdown from '../Dropdown';
import Image from '../Image';
import TimeAgo from '../TimeAgo';
import { getNotificationDisplayInformation } from './notification';

const NotificationItem = ({ notification, ...rest }) => {
const { seen, createdAt, notif } = notification;
Expand All @@ -25,7 +24,9 @@ const NotificationItem = ({ notification, ...rest }) => {
const handleMarkAsSeen = () => dispatch(markNotificationAsSeen(notification, !seen));
const handleDelete = async () => {
try {
await mfetchjson(`/api/notifications/${notification.id}`, { method: 'DELETE' });
await mfetchjson(`/api/notifications/${notification.id}?render=true&format=html`, {
method: 'DELETE',
});
dispatch(notificationsDeleted(notification));
} catch (error) {
dispatch(snackAlertError(error));
Expand Down Expand Up @@ -56,8 +57,12 @@ const NotificationItem = ({ notification, ...rest }) => {
);
}

const { text: __html, to, image } = getNotificationDisplayInformation(notification, true);
const html = { __html };
let html = { __html: notification.title };
let to = notification.toURL;
let image = {
url: notification.icons[0],
backgroundColor: 'transparent',
};

return (
<a
Expand Down
14 changes: 8 additions & 6 deletions ui/src/components/Navbar/NotificationsView.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { useInView } from 'react-intersection-observer';
import { useDispatch, useSelector } from 'react-redux';
import { APIError, mfetch, mfetchjson } from '../../helper';
Expand All @@ -20,24 +20,24 @@ const NotificationsView = () => {
const { loaded, next, items, count, newCount } = notifications;

const dispatch = useDispatch();
const apiEndpoint = '/api/notifications?render=true&format=html';

// type is the type of notifications. Empty string means all notifications.
const markAsSeen = async (type = '') => {
const res = await mfetch(`/api/notifications?action=markAllAsSeen&type=${type}`, {
const res = await mfetch(`${apiEndpoint}&action=markAllAsSeen&type=${type}`, {
method: 'POST',
});
if (!res.ok) throw new APIError(res.status, await res.text());
return res;
};

const apiEndpoint = '/api/notifications';
useEffect(() => {
if (loaded && newCount === 0) return;
(async () => {
try {
dispatch(notificationsLoaded(await mfetchjson(apiEndpoint)));

const res = await mfetch(`/api/notifications?action=resetNewCount`, { method: 'POST' });
const res = await mfetch(`${apiEndpoint}&action=resetNewCount`, { method: 'POST' });
if (!res.ok) throw new APIError(res.status, await res.text());

dispatch(notificationsNewCountReset());
Expand Down Expand Up @@ -67,7 +67,7 @@ const NotificationsView = () => {
(async () => {
try {
nextItemsLoading.current = true;
dispatch(notificationsUpdated(await mfetchjson(`${apiEndpoint}?next=${next}`)));
dispatch(notificationsUpdated(await mfetchjson(`${apiEndpoint}&next=${next}`)));
} catch (error) {
dispatch(snackAlertError(error));
} finally {
Expand All @@ -93,7 +93,9 @@ const NotificationsView = () => {
};
const handleDeleteAll = async () => {
try {
const res = await mfetch(`/api/notifications?action=deleteAll`, { method: 'POST' });
const res = await mfetch(`${apiEndpoint}&action=deleteAll`, {
method: 'POST',
});
if (!res.ok) throw new APIError(res.status, await res.text());
dispatch(notificationsAllDeleted());
dispatch(snackAlert('All notifications are deleted.'));
Expand Down
Loading

0 comments on commit e45167f

Please sign in to comment.