Add a mute setting for rooms (#391)
Co-authored-by: Coracle-Bot <npub1klq6260@nostr.local>
This commit is contained in:
parent
f83019a401
commit
368e984d17
5 changed files with 84 additions and 21 deletions
|
|
@ -10,6 +10,7 @@
|
|||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
|
|
@ -25,7 +26,12 @@
|
|||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import {roomLists, rooms} from "@app/core"
|
||||
import {deriveRoomMembers, deriveUserIsRoomAdmin, deriveUserRooms} from "@app/rooms"
|
||||
import {deriveShouldNotify, toggleRoomNotifications} from "@app/settings"
|
||||
import {
|
||||
deriveIsMuted,
|
||||
deriveShouldNotify,
|
||||
toggleRoomMuted,
|
||||
toggleRoomNotifications,
|
||||
} from "@app/settings"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -43,6 +49,7 @@
|
|||
const meta = $derived($room?.meta)
|
||||
const isFavorite = $derived($userRooms.includes(h))
|
||||
const shouldNotify = deriveShouldNotify(url, h)
|
||||
const isMuted = deriveIsMuted(url, h)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
@ -61,6 +68,10 @@
|
|||
const toggleShouldNotify = () => {
|
||||
toggleRoomNotifications(url, h)
|
||||
}
|
||||
|
||||
const toggleMuted = () => {
|
||||
toggleRoomMuted(url, h)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
|
|
@ -143,6 +154,23 @@
|
|||
<div class="card card-sm flex flex-col gap-4">
|
||||
<strong class="text-lg">Room Settings</strong>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Bookmark} />
|
||||
<span>Favorite</span>
|
||||
</div>
|
||||
<input type="checkbox" class="toggle" checked={isFavorite} onchange={toggleFavorite} />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<Icon icon={BellOff} />
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span>Mute</span>
|
||||
<span class="text-sm opacity-75">Hide alerts and unread badges for this room</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" class="toggle" checked={$isMuted} onchange={toggleMuted} />
|
||||
</div>
|
||||
<div class="flex items-center justify-between" class:opacity-50={$isMuted}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Bell} />
|
||||
<span>Notifications</span>
|
||||
|
|
@ -151,15 +179,9 @@
|
|||
type="checkbox"
|
||||
class="toggle"
|
||||
checked={$shouldNotify}
|
||||
disabled={$isMuted}
|
||||
onchange={toggleShouldNotify} />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Bookmark} />
|
||||
<span>Favorite</span>
|
||||
</div>
|
||||
<input type="checkbox" class="toggle" checked={isFavorite} onchange={toggleFavorite} />
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
const shouldNotifyForSpace = deriveShouldNotify(url)
|
||||
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
||||
const notification = $derived($shouldNotifyForRoom ? $notifications.has(path) : false)
|
||||
const notification = $derived($notifications.has(path))
|
||||
const roomName = $derived($room?.meta?.name() || h)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {deriveEventsByIdByUrl} from "@app/repository"
|
|||
import {app, fromApp} from "@app/core"
|
||||
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
||||
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {getIsMuted, notificationSettings, userSettingsValues} from "@app/settings"
|
||||
import {chatsById} from "@app/chats"
|
||||
import {dufflepud, DUFFLEPUD_URL, PLATFORM_RELAYS} from "@app/env"
|
||||
import {kv} from "@app/storage"
|
||||
|
|
@ -235,11 +235,12 @@ export const latestActivityByPath = derived(
|
|||
{kinds: [MESSAGE, ...CONTENT_KINDS]},
|
||||
makeCommentFilter(CONTENT_KINDS),
|
||||
]),
|
||||
userSettingsValues,
|
||||
],
|
||||
identity,
|
||||
),
|
||||
),
|
||||
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl]) => {
|
||||
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl, $settings]) => {
|
||||
const activity = new Map<string, TrustedEvent>()
|
||||
|
||||
for (const {pubkeys, messages} of $chatsById.values()) {
|
||||
|
|
@ -256,7 +257,8 @@ export const latestActivityByPath = derived(
|
|||
|
||||
if ($relays.get(url)?.hasNip(29)) {
|
||||
for (const [h, [latestEvent]] of groupBy(e => tagValue(tagSpec("h"), e.tags), events)) {
|
||||
if (h) {
|
||||
// A muted room is left out entirely, so it can't light up its own badge or the space's
|
||||
if (h && !getIsMuted($settings, url, h)) {
|
||||
activity.set(makeRoomPath(url, h), latestEvent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "@capacitor/push-notifications"
|
||||
import type {PluginListenerHandle} from "@capacitor/core"
|
||||
import {goto} from "$app/navigation"
|
||||
import {assoc, call, now, on, poll, spec, throttle} from "@welshman/lib"
|
||||
import {assoc, call, now, on, poll, spec, throttle, uniq} from "@welshman/lib"
|
||||
import {LOCAL_RELAY_URL} from "@welshman/net"
|
||||
import type {RepositoryUpdate} from "@welshman/net"
|
||||
import {
|
||||
|
|
@ -24,7 +24,7 @@ import {merged, withGetter} from "@welshman/store"
|
|||
import {User} from "@welshman/app"
|
||||
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
||||
import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||
import {notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
|
||||
import {getMutedRooms, notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
|
||||
import {makeEventPath, goToSpace} from "@app/routes"
|
||||
|
||||
export type PushSubscription = {
|
||||
|
|
@ -165,23 +165,28 @@ export const syncRelaySubscriptions = (
|
|||
notificationSettings,
|
||||
userSettingsValues,
|
||||
]).subscribe(
|
||||
throttle(3000, ([$spaceUrls, {spaces, mentions}, {alerts}]) => {
|
||||
throttle(3000, ([$spaceUrls, {spaces, mentions}, $settings]) => {
|
||||
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)]
|
||||
|
||||
for (const url of $spaceUrls) {
|
||||
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
|
||||
const {notify = true, exceptions = []} = $settings.alerts.find(spec({url})) || {}
|
||||
const muted = getMutedRooms($settings, url)
|
||||
const filters: Filter[] = []
|
||||
const ignore: Filter[] = []
|
||||
|
||||
if (spaces) {
|
||||
if (notify) {
|
||||
if (exceptions.length > 0) {
|
||||
ignore.push({"#h": exceptions})
|
||||
const skipped = uniq([...exceptions, ...muted])
|
||||
|
||||
if (skipped.length > 0) {
|
||||
ignore.push({"#h": skipped})
|
||||
}
|
||||
filters.push(...baseFilters)
|
||||
} else {
|
||||
if (exceptions.length > 0) {
|
||||
filters.push(...baseFilters.map(f => ({...f, "#h": exceptions})))
|
||||
const included = exceptions.filter(h => !muted.includes(h))
|
||||
|
||||
if (included.length > 0) {
|
||||
filters.push(...baseFilters.map(f => ({...f, "#h": included})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export type SpaceNotificationSettings = {
|
|||
url: string
|
||||
notify: boolean
|
||||
exceptions: string[]
|
||||
// Absent in settings published before muting existed
|
||||
muted?: string[]
|
||||
}
|
||||
|
||||
export type SettingsValues = {
|
||||
|
|
@ -113,7 +115,18 @@ export const zapAmounts = derived(userSettingsValues, $settings => $settings.zap
|
|||
|
||||
export const getSetting = <K extends keyof SettingsValues>(key: K) => userSettingsValues.get()[key]
|
||||
|
||||
export const getShouldNotify = ({alerts}: SettingsValues, url: string, h?: string) => {
|
||||
export const getMutedRooms = ({alerts}: SettingsValues, url: string) =>
|
||||
alerts.find(spec({url}))?.muted ?? []
|
||||
|
||||
export const getIsMuted = (settings: SettingsValues, url: string, h: string) =>
|
||||
getMutedRooms(settings, url).includes(h)
|
||||
|
||||
export const deriveIsMuted = (url: string, h: string) =>
|
||||
derived(userSettingsValues, $settings => getIsMuted($settings, url, h))
|
||||
|
||||
// The stored notification preference, ignoring mute. Toggling notifications writes this, so it is
|
||||
// preserved while a room is muted and comes back as it was when the room is unmuted.
|
||||
export const getNotifyPreference = ({alerts}: SettingsValues, url: string, h?: string) => {
|
||||
const pref = alerts.find(spec({url}))
|
||||
|
||||
if (!pref) return true
|
||||
|
|
@ -122,6 +135,13 @@ export const getShouldNotify = ({alerts}: SettingsValues, url: string, h?: strin
|
|||
return pref.notify ? !pref.exceptions.includes(h) : pref.exceptions.includes(h)
|
||||
}
|
||||
|
||||
// Muting a room is stronger than turning its notifications off: it forces notifications off and
|
||||
// also hides unread badges, which `notifications` handles by dropping muted rooms from the
|
||||
// activity it tracks. This is what the room settings toggle displays; it is only editable when the
|
||||
// room is not muted, so what it shows and what clicking it writes cannot disagree.
|
||||
export const getShouldNotify = (settings: SettingsValues, url: string, h?: string) =>
|
||||
h && getIsMuted(settings, url, h) ? false : getNotifyPreference(settings, url, h)
|
||||
|
||||
export const shouldNotify = (url: string, h?: string) =>
|
||||
getShouldNotify(userSettingsValues.get(), url, h)
|
||||
|
||||
|
|
@ -187,3 +207,17 @@ export const toggleRoomNotifications = (url: string, h: string) => {
|
|||
|
||||
return publishSettings({alerts: [...alerts, {url, notify: true, exceptions: [h]}]})
|
||||
}
|
||||
|
||||
export const toggleRoomMuted = (url: string, h: string) => {
|
||||
const alerts = getSetting("alerts")
|
||||
const existing = alerts.find(spec({url}))
|
||||
|
||||
if (existing) {
|
||||
const current = existing.muted ?? []
|
||||
const muted = current.includes(h) ? remove(h, current) : append(h, current)
|
||||
|
||||
return publishSettings({alerts: alerts.map(s => (s.url === url ? {...s, muted} : s))})
|
||||
}
|
||||
|
||||
return publishSettings({alerts: [...alerts, {url, notify: true, exceptions: [], muted: [h]}]})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue