From 21859e4c5505435cedc1f74aecd10f1d6189947c Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 24 Jun 2021 23:59:46 +0100 Subject: [PATCH] Feature: Basic notification options implementation --- external/lang | 2 +- src/context/intermediate/Intermediate.tsx | 4 +- src/context/revoltjs/Notifications.tsx | 33 +++++++--- src/context/revoltjs/SyncManager.tsx | 9 ++- src/lib/ContextMenus.tsx | 75 +++++++++++++++++++++-- src/pages/settings/panes/Sync.tsx | 1 + src/redux/index.ts | 8 ++- src/redux/reducers/index.ts | 5 +- src/redux/reducers/notifications.ts | 56 +++++++++++++++++ src/redux/reducers/sync.ts | 5 +- 10 files changed, 173 insertions(+), 25 deletions(-) create mode 100644 src/redux/reducers/notifications.ts diff --git a/external/lang b/external/lang index 099fb741..9cc46c3a 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit 099fb74131c60955e8226ce0a290cb22e959d7d6 +Subproject commit 9cc46c3a4abab74e17e56597db10e2c16ac0f9b5 diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 12f0e442..e96a72a3 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -116,7 +116,7 @@ export default function Intermediate(props: Props) { screen.id } /** By specifying a key, we reset state whenever switching screen. */ /> - {/* { if (action === 'POP') { @@ -128,7 +128,7 @@ export default function Intermediate(props: Props) { return true; }} - />*/} + /> ); diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx index 51f610b8..d9f4f1f9 100644 --- a/src/context/revoltjs/Notifications.tsx +++ b/src/context/revoltjs/Notifications.tsx @@ -8,9 +8,11 @@ import { connectState } from "../../redux/connector"; import { Message, SYSTEM_USER_ID, User } from "revolt.js"; import { NotificationOptions } from "../../redux/reducers/settings"; import { Route, Switch, useHistory, useParams } from "react-router-dom"; +import { getNotificationState, Notifications } from "../../redux/reducers/notifications"; interface Props { options?: NotificationOptions; + notifs: Notifications; } const notifications: { [key: string]: Notification } = {}; @@ -24,9 +26,9 @@ async function createNotification(title: string, options: globalThis.Notificatio } } -function Notifier(props: Props) { +function Notifier({ options, notifs }: Props) { const translate = useTranslation(); - const showNotification = props.options?.desktopEnabled ?? false; + const showNotification = options?.desktopEnabled ?? false; const client = useContext(AppContext); const { guild: guild_id, channel: channel_id } = useParams<{ @@ -39,17 +41,27 @@ function Notifier(props: Props) { 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; + 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); + switch (notifState) { + case 'muted': + case 'none': return; + case 'mention': { + if (!msg.mentions?.includes(client.user!._id)) return; + } + } playSound('message'); if (!showNotification) return; - const channel = client.channels.get(msg.channel); - const author = client.users.get(msg.author); - if (author?.relationship === Users.Relationship.Blocked) return; - let title; - switch (channel?.channel_type) { + switch (channel.channel_type) { case "SavedMessages": return; case "DirectMessage": @@ -192,7 +204,7 @@ function Notifier(props: Props) { client.removeListener("message", message); client.users.removeListener("mutation", relationship); }; - }, [client, playSound, guild_id, channel_id, showNotification]); + }, [client, playSound, guild_id, channel_id, showNotification, notifs]); useEffect(() => { function visChange() { @@ -217,7 +229,8 @@ const NotifierComponent = connectState( Notifier, state => { return { - options: state.settings.notification + options: state.settings.notification, + notifs: state.notifications }; }, true diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx index 95361cc5..d3fd7ca7 100644 --- a/src/context/revoltjs/SyncManager.tsx +++ b/src/context/revoltjs/SyncManager.tsx @@ -9,6 +9,7 @@ import { useContext, useEffect } from "preact/hooks"; import { connectState } from "../../redux/connector"; import { WithDispatcher } from "../../redux/reducers"; import { Settings } from "../../redux/reducers/settings"; +import { Notifications } from "../../redux/reducers/notifications"; import { AppContext, ClientStatus, StatusContext } from "./RevoltClient"; import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../redux/reducers/sync"; @@ -16,7 +17,8 @@ import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../red type Props = WithDispatcher & { settings: Settings, locale: Language, - sync: SyncOptions + sync: SyncOptions, + notifications: Notifications }; var lastValues: { [key in SyncKeys]?: any } = { }; @@ -78,7 +80,7 @@ function SyncManager(props: Props) { } let disabled = props.sync.disabled ?? []; - for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale] ] as [SyncKeys, any][]) { + for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale], ['notifications', props.notifications] ] as [SyncKeys, any][]) { useEffect(() => { if (disabled.indexOf(key) === -1) { if (typeof lastValues[key] !== 'undefined') { @@ -117,7 +119,8 @@ export default connectState( return { settings: state.settings, locale: state.locale, - sync: state.sync + sync: state.sync, + notifications: state.notifications }; }, true diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 89bdf8f4..875e9a44 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -5,7 +5,8 @@ import { Attachment, Channels, Message, Servers, Users } from "revolt.js/dist/ap import { ContextMenu, ContextMenuWithData, - MenuItem + MenuItem, + openContextMenu } from "preact-context-menu"; import { ChannelPermission, ServerPermission, UserPermission } from "revolt.js/dist/api/permissions"; import { QueuedMessage } from "../redux/reducers/queue"; @@ -18,6 +19,9 @@ import { Children } from "../types/Preact"; import LineDivider from "../components/ui/LineDivider"; import { connectState } from "../redux/connector"; import { internalEmit } from "./eventEmitter"; +import { AtSign, Bell, BellOff, Check, CheckSquare, ChevronRight, Slash, Square } from "@styled-icons/feather"; +import { getNotificationState, Notifications, NotificationState } from "../redux/reducers/notifications"; +import { ArrowLeft } from "@styled-icons/bootstrap"; interface ContextMenuData { user?: string; @@ -68,11 +72,17 @@ type Action = | { action: "close_dm"; target: Channels.DirectMessageChannel } | { action: "leave_server"; target: Servers.Server } | { action: "delete_server"; target: Servers.Server } + | { action: "open_notification_options", channel: Channels.Channel } | { action: "open_channel_settings", id: string } | { action: "open_server_settings", id: string } - | { action: "open_server_channel_settings", server: string, id: string }; + | { action: "open_server_channel_settings", server: string, id: string } + | { action: "set_notification_state", key: string, state?: NotificationState }; -function ContextMenus(props: WithDispatcher) { +type Props = WithDispatcher & { + notifications: Notifications +}; + +function ContextMenus(props: Props) { const { openScreen, writeClipboard } = useIntermediate(); const client = useContext(AppContext); const userId = client.user!._id; @@ -301,9 +311,24 @@ function ContextMenus(props: WithDispatcher) { case "ban_member": case "kick_member": openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user }); break; + case "open_notification_options": { + openContextMenu("NotificationOptions", { channel: data.channel }); + break; + } + case "open_channel_settings": history.push(`/channel/${data.id}/settings`); break; case "open_server_channel_settings": history.push(`/server/${data.server}/channel/${data.id}/settings`); break; case "open_server_settings": history.push(`/server/${data.id}/settings`); break; + + case "set_notification_state": { + const { key, state } = data; + if (state) { + props.dispatcher({ type: "NOTIFICATIONS_SET", key, state }); + } else { + props.dispatcher({ type: "NOTIFICATIONS_REMOVE", key }); + } + break; + } } })().catch(err => { openScreen({ id: "error", error: takeError(err) }); @@ -567,6 +592,10 @@ function ContextMenus(props: WithDispatcher) { pushDivider(); if (channel) { + if (channel.channel_type !== 'VoiceChannel') { + generateAction({ action: "open_notification_options", channel }, undefined, undefined, ); + } + switch (channel.channel_type) { case 'Group': // ! generateAction({ action: "create_invite", target: channel }); FIXME: add support for group invites @@ -669,14 +698,50 @@ function ContextMenus(props: WithDispatcher) { )} + + {({ channel }: { channel: Channels.Channel }) => { + const state = props.notifications[channel._id]; + const actual = getNotificationState(props.notifications, channel); + + let elements: Children[] = [ + + +
+ { (state !== undefined) && } + { (state === undefined) && } +
+
+ ]; + + function generate(key: string, icon: Children) { + elements.push( + + { icon } + + { (state === undefined && actual === key) &&
} + { (state === key) &&
} +
+ ); + } + + generate('all', ); + generate('mention', ); + generate('muted', ); + generate('none', ); + + return elements; + }} +
); } export default connectState( ContextMenus, - () => { - return {}; + state => { + return { + notifications: state.notifications + }; }, true ); diff --git a/src/pages/settings/panes/Sync.tsx b/src/pages/settings/panes/Sync.tsx index e5631286..ad7c84f8 100644 --- a/src/pages/settings/panes/Sync.tsx +++ b/src/pages/settings/panes/Sync.tsx @@ -20,6 +20,7 @@ export function Component(props: Props & WithDispatcher) { ['appearance', 'appearance.title'], ['theme', 'appearance.theme'], ['locale', 'language.title'] + // notifications sync is always-on ] as [ SyncKeys, string ][]).map( ([ key, title ]) => { drafts, sync, experiments, - lastOpened + lastOpened, + notifications } = store.getState() as State; localForage.setItem("state", { @@ -66,6 +69,7 @@ store.subscribe(() => { drafts, sync, experiments, - lastOpened + lastOpened, + notifications }); }); diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index 6c84f87f..fe47ccbd 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -12,6 +12,7 @@ import { drafts, DraftAction } from "./drafts"; import { sync, SyncAction } from "./sync"; import { experiments, ExperimentsAction } from "./experiments"; import { lastOpened, LastOpenedAction } from "./last_opened"; +import { notifications, NotificationsAction } from "./notifications"; export default combineReducers({ config, @@ -24,7 +25,8 @@ export default combineReducers({ drafts, sync, experiments, - lastOpened + lastOpened, + notifications }); export type Action = @@ -39,6 +41,7 @@ export type Action = | SyncAction | ExperimentsAction | LastOpenedAction + | NotificationsAction | { type: "__INIT"; state: State }; export type WithDispatcher = { dispatcher: (action: Action) => void }; diff --git a/src/redux/reducers/notifications.ts b/src/redux/reducers/notifications.ts new file mode 100644 index 00000000..2ca5b030 --- /dev/null +++ b/src/redux/reducers/notifications.ts @@ -0,0 +1,56 @@ +import { Channel } from "revolt.js"; + +export type NotificationState = 'all' | 'mention' | 'none' | 'muted'; + +export type Notifications = { + [key: string]: NotificationState +} + +export const DEFAULT_STATES: { [key in Channel['channel_type']]: NotificationState } = { + 'SavedMessages': 'all', + 'DirectMessage': 'all', + 'Group': 'all', + 'TextChannel': 'mention', + 'VoiceChannel': 'mention' +}; + +export function getNotificationState(notifications: Notifications, channel: Channel) { + return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type]; +} + +export type NotificationsAction = + | { type: undefined } + | { + type: "NOTIFICATIONS_SET"; + key: string; + state: NotificationState; + } + | { + type: "NOTIFICATIONS_REMOVE"; + key: string; + } + | { + type: "RESET"; + }; + +export function notifications( + state = {} as Notifications, + action: NotificationsAction +): Notifications { + switch (action.type) { + case "NOTIFICATIONS_SET": + return { + ...state, + [action.key]: action.state + }; + case "NOTIFICATIONS_REMOVE": + { + const { [action.key]: _, ...newState } = state; + return newState; + } + case "RESET": + return {}; + default: + return state; + } +} diff --git a/src/redux/reducers/sync.ts b/src/redux/reducers/sync.ts index 1e2d4a31..0e2c8111 100644 --- a/src/redux/reducers/sync.ts +++ b/src/redux/reducers/sync.ts @@ -1,19 +1,22 @@ import { AppearanceOptions } from "./settings"; import { Language } from "../../context/Locale"; import { ThemeOptions } from "../../context/Theme"; +import { Notifications } from "./notifications"; -export type SyncKeys = "theme" | "appearance" | "locale"; +export type SyncKeys = "theme" | "appearance" | "locale" | "notifications"; export interface SyncData { locale?: Language; theme?: ThemeOptions; appearance?: AppearanceOptions; + notifications?: Notifications; } export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [ "theme", "appearance", "locale", + "notifications" ]; export interface SyncOptions { disabled?: SyncKeys[];