chore(refactor): remove Notifications component

This commit is contained in:
Paul Makles 2022-06-29 17:31:59 +01:00
parent 05516c5823
commit a2a52e237d
5 changed files with 316 additions and 303 deletions

View file

@ -3,3 +3,14 @@ import { createBrowserHistory } from "history";
export const history = createBrowserHistory({
basename: import.meta.env.BASE_URL,
});
export const routeInformation = {
getServer: () =>
/server\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
history.location.pathname,
)?.[1],
getChannel: () =>
/channel\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
history.location.pathname,
)?.[1],
};

View file

@ -1,296 +0,0 @@
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, User } from "revolt.js";
import { decodeTime } from "ulid";
import { useCallback, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n";
import { useApplicationState } from "../../mobx/State";
import { useClient } from "../../controllers/client/ClientController";
const notifications: { [key: string]: Notification } = {};
async function createNotification(
title: string,
options: globalThis.NotificationOptions,
) {
try {
return new Notification(title, options);
} catch (err) {
const sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options);
}
}
function Notifier() {
const translate = useTranslation();
const state = useApplicationState();
const notifs = state.notifications;
const showNotification = state.settings.get("notifications:desktop");
const client = useClient();
const { guild: guild_id, channel: channel_id } = useParams<{
guild: string;
channel: string;
}>();
const history = useHistory();
const message = useCallback(
async (msg: Message) => {
if (msg.channel_id === channel_id && document.hasFocus()) return;
if (!notifs.shouldNotify(msg)) return;
state.settings.sounds.playSound("message");
if (!showNotification) return;
const effectiveName = msg.masquerade?.name ?? msg.author?.username;
let title;
switch (msg.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${effectiveName}`;
break;
case "Group":
if (msg.author?._id === "00000000000000000000000000") {
title = msg.channel.name;
} else {
title = `@${effectiveName} - ${msg.channel.name}`;
}
break;
case "TextChannel":
title = `@${effectiveName} (#${msg.channel.name}, ${msg.channel.server?.name})`;
break;
default:
title = msg.channel?._id;
break;
}
let image;
if (msg.attachments) {
const imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
}
}
let body, icon;
if (msg.content) {
body = client.markdownToText(msg.content);
if (msg.masquerade?.avatar) {
icon = client.proxyFile(msg.masquerade.avatar);
} else {
icon = msg.author?.generateAvatarURL({ max_side: 256 });
}
} else if (msg.system) {
const users = client.users;
switch (msg.system.type) {
case "user_added":
case "user_remove":
{
const user = users.get(msg.system.id);
body = translate(
`app.main.channel.system.${
msg.system.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.system.by)
?.username,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
const user = users.get(msg.system.id);
body = translate(
`app.main.channel.system.${msg.system.type}`,
{ user: user?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_renamed":
{
const user = users.get(msg.system.by);
body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.system.by)?.username,
name: msg.system.name,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_description_changed":
case "channel_icon_changed":
{
const user = users.get(msg.system.by);
body = translate(
`app.main.channel.system.${msg.system.type}`,
{ user: users.get(msg.system.by)?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
}
}
const notif = await createNotification(title!, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel?._id,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = msg.channel_id;
if (id !== channel_id) {
const channel = client.channels.get(id);
if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
notifications[msg.channel_id] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
state,
],
);
const relationship = useCallback(
async (user: User) => {
if (client.user?.status?.presence === "Busy") return;
if (!showNotification) return;
let event;
switch (user.relationship) {
case "Incoming":
event = translate("notifications.sent_request", {
person: user.username,
});
break;
case "Friend":
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
return;
}
const notif = await createNotification(event, {
icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
},
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => {
client.addListener("message", message);
client.addListener("user/relationship", relationship);
return () => {
client.removeListener("message", message);
client.removeListener("user/relationship", relationship);
};
}, [
client,
state,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => {
function visChange() {
if (document.visibilityState === "visible") {
if (notifications[channel_id]) {
notifications[channel_id].close();
}
}
}
visChange();
document.addEventListener("visibilitychange", visChange);
return () =>
document.removeEventListener("visibilitychange", visChange);
}, [guild_id, channel_id]);
return null;
}
export default function NotificationsComponent() {
return (
<Switch>
<Route path="/server/:server/channel/:channel">
<Notifier />
</Route>
<Route path="/channel/:channel">
<Notifier />
</Route>
<Route path="/">
<Notifier />
</Route>
</Switch>
);
}

View file

@ -39,6 +39,9 @@ export default class State {
locale: LocaleOptions;
experiments: Experiments;
layout: Layout;
/**
* DEPRECATED
*/
private config: ServerConfig;
notifications: NotificationOptions;
queue: MessageQueue;
@ -61,7 +64,7 @@ export default class State {
this.experiments = new Experiments();
this.layout = new Layout();
this.config = new ServerConfig();
this.notifications = new NotificationOptions();
this.notifications = new NotificationOptions(this);
this.queue = new MessageQueue();
this.settings = new Settings();
this.sync = new Sync(this);
@ -159,6 +162,17 @@ export default class State {
// Register listener for incoming packets.
client.addListener("packet", this.onPacket);
// Register events for notifications.
client.addListener("message", this.notifications.onMessage);
client.addListener(
"user/relationship",
this.notifications.onRelationship,
);
document.addEventListener(
"visibilitychange",
this.notifications.onVisibilityChange,
);
// Sync settings from remote server.
state.sync
.pull(client)
@ -253,6 +267,15 @@ export default class State {
if (client) {
client.removeListener("message", this.queue.onMessage);
client.removeListener("packet", this.onPacket);
client.removeListener("message", this.notifications.onMessage);
client.removeListener(
"user/relationship",
this.notifications.onRelationship,
);
document.removeEventListener(
"visibilitychange",
this.notifications.onVisibilityChange,
);
}
// Wipe all listeners.
@ -293,7 +316,7 @@ export default class State {
this.draft = new Draft();
this.experiments = new Experiments();
this.layout = new Layout();
this.notifications = new NotificationOptions();
this.notifications = new NotificationOptions(this);
this.queue = new MessageQueue();
this.settings = new Settings();
this.sync = new Sync(this);

View file

@ -1,8 +1,14 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
import { Channel, Message, Server } from "revolt.js";
import { Channel, Message, Server, User } from "revolt.js";
import { decodeTime } from "ulid";
import { translate } from "preact-i18n";
import { mapToRecord } from "../../lib/conversion";
import { history, routeInformation } from "../../context/history";
import State from "../State";
import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store";
import Syncable from "../interfaces/Syncable";
@ -37,22 +43,54 @@ export interface Data {
channel?: Record<string, NotificationState>;
}
/**
* Create a notification either directly or using service worker.
* @param title Notification Title
* @param options Notification Options
* @returns Notification
*/
async function createNotification(
title: string,
options: globalThis.NotificationOptions,
) {
try {
return new Notification(title, options);
} catch (err) {
const sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options);
}
}
/**
* Manages the user's notification preferences.
*/
export default class NotificationOptions
implements Store, Persistent<Data>, Syncable
{
private state: State;
private activeNotifications: Record<string, Notification>;
private server: ObservableMap<string, NotificationState>;
private channel: ObservableMap<string, NotificationState>;
/**
* Construct new Experiments store.
*/
constructor() {
constructor(state: State) {
this.server = new ObservableMap();
this.channel = new ObservableMap();
makeAutoObservable(this);
makeAutoObservable(this, {
onMessage: false,
onRelationship: false,
});
this.state = state;
this.activeNotifications = {};
this.onMessage = this.onMessage.bind(this);
this.onRelationship = this.onRelationship.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
}
get id() {
@ -209,6 +247,245 @@ export default class NotificationOptions
return false;
}
/**
* Handle incoming messages and create a notification.
* @param message Message
*/
async onMessage(message: Message) {
// Ignore if we are currently looking and focused on the channel.
if (
message.channel_id === routeInformation.getChannel() &&
document.hasFocus()
)
return;
// Ignore if muted.
if (!this.shouldNotify(message)) return;
// Play a sound and skip notif if disabled.
this.state.settings.sounds.playSound("message");
if (!this.state.settings.get("notifications:desktop")) return;
const effectiveName =
message.masquerade?.name ?? message.author?.username;
let title;
switch (message.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${effectiveName}`;
break;
case "Group":
if (message.author?._id === "00000000000000000000000000") {
title = message.channel.name;
} else {
title = `@${effectiveName} - ${message.channel.name}`;
}
break;
case "TextChannel":
title = `@${effectiveName} (#${message.channel.name}, ${message.channel.server?.name})`;
break;
default:
title = message.channel?._id;
break;
}
let image;
if (message.attachments) {
const imageAttachment = message.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = message.client.generateFileURL(imageAttachment, {
max_side: 720,
});
}
}
let body, icon;
if (message.content) {
body = message.client.markdownToText(message.content);
if (message.masquerade?.avatar) {
icon = message.client.proxyFile(message.masquerade.avatar);
} else {
icon = message.author?.generateAvatarURL({ max_side: 256 });
}
} else if (message.system) {
const users = message.client.users;
// ! FIXME: I've had to strip translations while
// ! I move stuff into the new project structure
switch (message.system.type) {
case "user_added":
case "user_remove":
{
const user = users.get(message.system.id);
body = `${user?.username} ${
message.system.type === "user_added"
? "added by"
: "removed by"
} ${users.get(message.system.by)?.username}`;
/*body = translate(
`app.main.channel.system.${
message.system.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(message.system.by)
?.username,
},
);*/
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
const user = users.get(message.system.id);
body = `${user?.username}`;
/*body = translate(
`app.main.channel.system.${message.system.type}`,
{ user: user?.username },
);*/
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_renamed":
{
const user = users.get(message.system.by);
body = `${user?.username} renamed channel to ${message.system.name}`;
/*body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(message.system.by)?.username,
name: message.system.name,
},
);*/
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_description_changed":
case "channel_icon_changed":
{
const user = users.get(message.system.by);
/*body = translate(
`app.main.channel.system.${message.system.type}`,
{ user: users.get(message.system.by)?.username },
);*/
body = `${users.get(message.system.by)?.username}`;
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
}
}
const notif = await createNotification(title!, {
icon,
image,
body,
timestamp: decodeTime(message._id),
tag: message.channel?._id,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = message.channel_id;
if (id !== routeInformation.getChannel()) {
const channel = message.client.channels.get(id);
if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
this.activeNotifications[message.channel_id] = notif;
notif.addEventListener(
"close",
() => delete this.activeNotifications[message.channel_id],
);
}
}
/**
* Handle user relationship changes.
* @param user User relationship changed with
*/
async onRelationship(user: User) {
// Ignore if disabled.
if (!this.state.settings.get("notifications:desktop")) return;
// Check whether we are busy.
// This is checked by `shouldNotify` in the case of messages.
if (user.status?.presence === "Busy") {
return false;
}
let event;
switch (user.relationship) {
case "Incoming":
/*event = translate("notifications.sent_request", {
person: user.username,
});*/
event = `${user.username} sent you a friend request`;
break;
case "Friend":
/*event = translate("notifications.now_friends", {
person: user.username,
});*/
event = `Now friends with ${user.username}`;
break;
default:
return;
}
const notif = await createNotification(event, {
icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
}
/**
* Called when document visibility changes.
*/
onVisibilityChange() {
if (document.visibilityState === "visible") {
const channel_id = routeInformation.getChannel()!;
if (this.activeNotifications[channel_id]) {
this.activeNotifications[channel_id].close();
}
}
}
@action apply(_key: "notifications", data: unknown, _revision: number) {
this.hydrate(data as Data);
}

View file

@ -8,7 +8,6 @@ import ContextMenus from "../lib/ContextMenus";
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
import Popovers from "../context/intermediate/Popovers";
import Notifications from "../context/revoltjs/Notifications";
import { Titlebar } from "../components/native/Titlebar";
import BottomNavigation from "../components/navigation/BottomNavigation";
@ -227,7 +226,6 @@ export default function App() {
</Routes>
<ContextMenus />
<Popovers />
<Notifications />
</OverlappingPanels>
</AppContainer>
</>