import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Message, SYSTEM_USER_ID, User } from "revolt.js"; import { Users } from "revolt.js/dist/api/objects"; import { decodeTime } from "ulid"; import { useContext, useEffect } from "preact/hooks"; import { useTranslation } from "../../lib/i18n"; import { connectState } from "../../redux/connector"; import { getNotificationState, Notifications, shouldNotify, } from "../../redux/reducers/notifications"; import { NotificationOptions } from "../../redux/reducers/settings"; import { SoundContext } from "../Settings"; import { AppContext } from "./RevoltClient"; interface Props { options?: NotificationOptions; notifs: Notifications; } 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({ options, notifs }: Props) { const translate = useTranslation(); const showNotification = options?.desktopEnabled ?? false; const client = useContext(AppContext); const { guild: guild_id, channel: channel_id } = useParams<{ guild: string; channel: string; }>(); const history = useHistory(); const playSound = useContext(SoundContext); async function message(msg: Message) { if (msg.author === client.user!._id) return; if (msg.channel === channel_id && document.hasFocus()) return; if (client.user!.status?.presence === Users.Presence.Busy) return; const channel = client.channels.get(msg.channel); const author = client.users.get(msg.author); if (!channel) return; if (author?.relationship === Users.Relationship.Blocked) return; const notifState = getNotificationState(notifs, channel); if (!shouldNotify(notifState, msg, client.user!._id)) return; playSound("message"); if (!showNotification) return; let title; switch (channel.channel_type) { case "SavedMessages": return; case "DirectMessage": title = `@${author?.username}`; break; case "Group": if (author?._id === SYSTEM_USER_ID) { title = channel.name; } else { title = `@${author?.username} - ${channel.name}`; } break; case "TextChannel": const server = client.servers.get(channel.server); title = `@${author?.username} (#${channel.name}, ${server?.name})`; break; default: title = msg.channel; 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 (typeof msg.content === "string") { body = client.markdownToText(msg.content); icon = client.users.getAvatarURL(msg.author, { max_side: 256 }); } else { const users = client.users; switch (msg.content.type) { case "user_added": case "user_remove": body = translate( `app.main.channel.system.${ msg.content.type === "user_added" ? "added_by" : "removed_by" }`, { user: users.get(msg.content.id)?.username, other_user: users.get(msg.content.by)?.username, }, ); icon = client.users.getAvatarURL(msg.content.id, { max_side: 256, }); break; case "user_joined": case "user_left": case "user_kicked": case "user_banned": body = translate( `app.main.channel.system.${msg.content.type}`, { user: users.get(msg.content.id)?.username }, ); icon = client.users.getAvatarURL(msg.content.id, { max_side: 256, }); break; case "channel_renamed": body = translate( `app.main.channel.system.channel_renamed`, { user: users.get(msg.content.by)?.username, name: msg.content.name, }, ); icon = client.users.getAvatarURL(msg.content.by, { max_side: 256, }); break; case "channel_description_changed": case "channel_icon_changed": body = translate( `app.main.channel.system.${msg.content.type}`, { user: users.get(msg.content.by)?.username }, ); icon = client.users.getAvatarURL(msg.content.by, { max_side: 256, }); break; } } const notif = await createNotification(title, { icon, image, body, timestamp: decodeTime(msg._id), tag: msg.channel, badge: "/assets/icons/android-chrome-512x512.png", silent: true, }); if (notif) { notif.addEventListener("click", () => { window.focus(); const id = msg.channel; if (id !== channel_id) { const channel = client.channels.get(id); if (channel) { if (channel.channel_type === "TextChannel") { history.push( `/server/${channel.server}/channel/${id}`, ); } else { history.push(`/channel/${id}`); } } } }); notifications[msg.channel] = notif; notif.addEventListener( "close", () => delete notifications[msg.channel], ); } } async function relationship(user: User, property: string) { if (client.user?.status?.presence === Users.Presence.Busy) return; if (property !== "relationship") return; if (!showNotification) return; let event; switch (user.relationship) { case Users.Relationship.Incoming: event = translate("notifications.sent_request", { person: user.username, }); break; case Users.Relationship.Friend: event = translate("notifications.now_friends", { person: user.username, }); break; default: return; } const notif = await createNotification(event, { icon: client.users.getAvatarURL(user._id, { max_side: 256 }), badge: "/assets/icons/android-chrome-512x512.png", timestamp: +new Date(), }); notif?.addEventListener("click", () => { history.push(`/friends`); }); } useEffect(() => { client.addListener("message", message); client.users.addListener("mutation", relationship); return () => { client.removeListener("message", message); client.users.removeListener("mutation", relationship); }; }, [client, playSound, guild_id, channel_id, showNotification, notifs]); 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; } const NotifierComponent = connectState( Notifier, (state) => { return { options: state.settings.notification, notifs: state.notifications, }; }, true, ); export default function NotificationsComponent() { return ( ); }