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 Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||||
import Bell from "@assets/icons/bell.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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||||
|
|
@ -25,7 +26,12 @@
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
import {roomLists, rooms} from "@app/core"
|
import {roomLists, rooms} from "@app/core"
|
||||||
import {deriveRoomMembers, deriveUserIsRoomAdmin, deriveUserRooms} from "@app/rooms"
|
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"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -43,6 +49,7 @@
|
||||||
const meta = $derived($room?.meta)
|
const meta = $derived($room?.meta)
|
||||||
const isFavorite = $derived($userRooms.includes(h))
|
const isFavorite = $derived($userRooms.includes(h))
|
||||||
const shouldNotify = deriveShouldNotify(url, h)
|
const shouldNotify = deriveShouldNotify(url, h)
|
||||||
|
const isMuted = deriveIsMuted(url, h)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|
@ -61,6 +68,10 @@
|
||||||
const toggleShouldNotify = () => {
|
const toggleShouldNotify = () => {
|
||||||
toggleRoomNotifications(url, h)
|
toggleRoomNotifications(url, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleMuted = () => {
|
||||||
|
toggleRoomMuted(url, h)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
|
|
@ -143,6 +154,23 @@
|
||||||
<div class="card card-sm flex flex-col gap-4">
|
<div class="card card-sm flex flex-col gap-4">
|
||||||
<strong class="text-lg">Room Settings</strong>
|
<strong class="text-lg">Room Settings</strong>
|
||||||
<div class="flex items-center justify-between">
|
<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">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={Bell} />
|
<Icon icon={Bell} />
|
||||||
<span>Notifications</span>
|
<span>Notifications</span>
|
||||||
|
|
@ -151,15 +179,9 @@
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle"
|
class="toggle"
|
||||||
checked={$shouldNotify}
|
checked={$shouldNotify}
|
||||||
|
disabled={$isMuted}
|
||||||
onchange={toggleShouldNotify} />
|
onchange={toggleShouldNotify} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
const shouldNotifyForSpace = deriveShouldNotify(url)
|
const shouldNotifyForSpace = deriveShouldNotify(url)
|
||||||
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
||||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
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)
|
const roomName = $derived($room?.meta?.name() || h)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {deriveEventsByIdByUrl} from "@app/repository"
|
||||||
import {app, fromApp} from "@app/core"
|
import {app, fromApp} from "@app/core"
|
||||||
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
||||||
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
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 {chatsById} from "@app/chats"
|
||||||
import {dufflepud, DUFFLEPUD_URL, PLATFORM_RELAYS} from "@app/env"
|
import {dufflepud, DUFFLEPUD_URL, PLATFORM_RELAYS} from "@app/env"
|
||||||
import {kv} from "@app/storage"
|
import {kv} from "@app/storage"
|
||||||
|
|
@ -235,11 +235,12 @@ export const latestActivityByPath = derived(
|
||||||
{kinds: [MESSAGE, ...CONTENT_KINDS]},
|
{kinds: [MESSAGE, ...CONTENT_KINDS]},
|
||||||
makeCommentFilter(CONTENT_KINDS),
|
makeCommentFilter(CONTENT_KINDS),
|
||||||
]),
|
]),
|
||||||
|
userSettingsValues,
|
||||||
],
|
],
|
||||||
identity,
|
identity,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl]) => {
|
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl, $settings]) => {
|
||||||
const activity = new Map<string, TrustedEvent>()
|
const activity = new Map<string, TrustedEvent>()
|
||||||
|
|
||||||
for (const {pubkeys, messages} of $chatsById.values()) {
|
for (const {pubkeys, messages} of $chatsById.values()) {
|
||||||
|
|
@ -256,7 +257,8 @@ export const latestActivityByPath = derived(
|
||||||
|
|
||||||
if ($relays.get(url)?.hasNip(29)) {
|
if ($relays.get(url)?.hasNip(29)) {
|
||||||
for (const [h, [latestEvent]] of groupBy(e => tagValue(tagSpec("h"), e.tags), events)) {
|
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)
|
activity.set(makeRoomPath(url, h), latestEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "@capacitor/push-notifications"
|
} from "@capacitor/push-notifications"
|
||||||
import type {PluginListenerHandle} from "@capacitor/core"
|
import type {PluginListenerHandle} from "@capacitor/core"
|
||||||
import {goto} from "$app/navigation"
|
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 {LOCAL_RELAY_URL} from "@welshman/net"
|
||||||
import type {RepositoryUpdate} from "@welshman/net"
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,7 +24,7 @@ import {merged, withGetter} from "@welshman/store"
|
||||||
import {User} from "@welshman/app"
|
import {User} from "@welshman/app"
|
||||||
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
||||||
import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
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"
|
import {makeEventPath, goToSpace} from "@app/routes"
|
||||||
|
|
||||||
export type PushSubscription = {
|
export type PushSubscription = {
|
||||||
|
|
@ -165,23 +165,28 @@ export const syncRelaySubscriptions = (
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
]).subscribe(
|
]).subscribe(
|
||||||
throttle(3000, ([$spaceUrls, {spaces, mentions}, {alerts}]) => {
|
throttle(3000, ([$spaceUrls, {spaces, mentions}, $settings]) => {
|
||||||
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)]
|
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)]
|
||||||
|
|
||||||
for (const url of $spaceUrls) {
|
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 filters: Filter[] = []
|
||||||
const ignore: Filter[] = []
|
const ignore: Filter[] = []
|
||||||
|
|
||||||
if (spaces) {
|
if (spaces) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
if (exceptions.length > 0) {
|
const skipped = uniq([...exceptions, ...muted])
|
||||||
ignore.push({"#h": exceptions})
|
|
||||||
|
if (skipped.length > 0) {
|
||||||
|
ignore.push({"#h": skipped})
|
||||||
}
|
}
|
||||||
filters.push(...baseFilters)
|
filters.push(...baseFilters)
|
||||||
} else {
|
} else {
|
||||||
if (exceptions.length > 0) {
|
const included = exceptions.filter(h => !muted.includes(h))
|
||||||
filters.push(...baseFilters.map(f => ({...f, "#h": exceptions})))
|
|
||||||
|
if (included.length > 0) {
|
||||||
|
filters.push(...baseFilters.map(f => ({...f, "#h": included})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export type SpaceNotificationSettings = {
|
||||||
url: string
|
url: string
|
||||||
notify: boolean
|
notify: boolean
|
||||||
exceptions: string[]
|
exceptions: string[]
|
||||||
|
// Absent in settings published before muting existed
|
||||||
|
muted?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SettingsValues = {
|
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 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}))
|
const pref = alerts.find(spec({url}))
|
||||||
|
|
||||||
if (!pref) return true
|
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)
|
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) =>
|
export const shouldNotify = (url: string, h?: string) =>
|
||||||
getShouldNotify(userSettingsValues.get(), url, h)
|
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]}]})
|
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