- Joining
{displayRelayUrl(url)}
+
+
+
+
+
Enable notifications for this space
+
+ Get notified about new activity in this space. You can change this later in settings.
+
- {/snippet}
- {#snippet info()}
-
Are you sure you'd like to join this space?
- {/snippet}
-
+
+
+
+
+
+ Connection Status
+ {#if error}
+ Error
+ {:else}
+
+ {/if}
+
+ {#if error}
+
+ {/if}
+
-
+
Go back
- Join Space
+
+ {error ? "Request Access" : "Join Space"}
+
diff --git a/src/app/components/SpaceJoinConfirm.svelte b/src/app/components/SpaceJoinConfirm.svelte
deleted file mode 100644
index 475085bf..00000000
--- a/src/app/components/SpaceJoinConfirm.svelte
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte
index 9f687957..0ad6fd4b 100644
--- a/src/app/components/SpaceMenu.svelte
+++ b/src/app/components/SpaceMenu.svelte
@@ -18,6 +18,8 @@
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
+ import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
+ import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
@@ -46,7 +48,11 @@
deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
deriveEventsForUrl,
+ userSettingsValues,
+ notificationSettings,
+ deriveIsMuted,
} from "@app/core/state"
+ import {setSpaceNotifications} from "@app/core/commands"
import {notifications} from "@app/util/notifications"
import {pushModal} from "@app/util/modal"
import {makeSpacePath, makeChatPath} from "@app/util/routes"
@@ -93,6 +99,12 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
+ const isMuted = deriveIsMuted(url)
+
+ const toggleSpaceNotifications = () => {
+ setSpaceNotifications(url, !isMuted)
+ }
+
let showMenu = $state(false)
let replaceState = $state(false)
let element: Element | undefined = $state()
@@ -111,6 +123,9 @@
+ {#if isMuted}
+
+ {/if}
@@ -155,6 +170,19 @@
{/if}
+
+ {#if $notificationSettings.push}
+
+
+ {isMuted ? "Turn on" : "Turn off"} notifications
+
+ {:else}
+
+
+ Enable notifications
+
+ {/if}
+
{#if $userSpaceUrls.includes(url)}
diff --git a/src/app/components/SpaceMenuRoomItem.svelte b/src/app/components/SpaceMenuRoomItem.svelte
index c34a6e88..4c1ed3a8 100644
--- a/src/app/components/SpaceMenuRoomItem.svelte
+++ b/src/app/components/SpaceMenuRoomItem.svelte
@@ -1,11 +1,12 @@
- {#if $userSettingsValues.muted_rooms.includes(id)}
-
+ {#if showDifferenceIcon}
+
{/if}
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts
index 642b68e7..ba6fdf14 100644
--- a/src/app/core/commands.ts
+++ b/src/app/core/commands.ts
@@ -73,7 +73,7 @@ import {
getThunkError,
} from "@welshman/app"
import {compressFile} from "@lib/html"
-import type {SettingsValues} from "@app/core/state"
+import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
import {
SETTINGS,
PROTECTED,
@@ -82,6 +82,7 @@ import {
userSpaceUrls,
userSettingsValues,
getSetting,
+ getSettings,
userGroupList,
shouldIgnoreError,
stripPrefix,
@@ -356,6 +357,48 @@ export const addTrustedRelay = async (url: string) =>
export const removeTrustedRelay = async (url: string) =>
publishSettings({trusted_relays: remove(url, getSetting("trusted_relays"))})
+// Space and room notification settings
+
+export const setSpaceNotifications = async (url: string, notify: boolean) => {
+ const {alerts} = getSettings()
+ const existing = alerts.find((s: SpaceNotificationSettings) => s.url === url)
+
+ let updated: typeof alerts
+
+ if (existing) {
+ // Clear exceptions when changing the space notification setting
+ updated = alerts.map((s: SpaceNotificationSettings) =>
+ s.url === url ? {...s, notify, exceptions: []} : s,
+ )
+ } else {
+ updated = [...alerts, {url, notify, exceptions: []}]
+ }
+
+ return publishSettings({alerts: updated})
+}
+
+export const toggleRoomNotifications = async (url: string, h: string) => {
+ const {alerts} = getSettings()
+ const existing = alerts.find((s: SpaceNotificationSettings) => s.url === url)
+
+ let updated: typeof alerts
+
+ if (!existing) {
+ // No space settings yet, create one with this room as an exception (default is notify: true)
+ updated = [...alerts, {url, notify: true, exceptions: [h]}]
+ } else {
+ // Toggle exception status
+ const hasException = existing.exceptions.includes(h)
+ const exceptions = hasException ? remove(h, existing.exceptions) : append(h, existing.exceptions)
+
+ updated = alerts.map((s: SpaceNotificationSettings) =>
+ s.url === url ? {...s, exceptions} : s,
+ )
+ }
+
+ return publishSettings({alerts: updated})
+}
+
// Join request
export type JoinRequestParams = {
diff --git a/src/app/core/state.ts b/src/app/core/state.ts
index 0fafd4e1..d02750c3 100644
--- a/src/app/core/state.ts
+++ b/src/app/core/state.ts
@@ -271,6 +271,12 @@ export enum RelayAuthMode {
Conservative = "conservative",
}
+export type SpaceNotificationSettings = {
+ url: string
+ notify: boolean
+ exceptions: string[]
+}
+
export type SettingsValues = {
show_media: boolean
hide_sensitive: boolean
@@ -280,7 +286,7 @@ export type SettingsValues = {
relay_auth: RelayAuthMode
send_delay: number
font_size: number
- muted_rooms: string[]
+ alerts: SpaceNotificationSettings[]
}
export type Settings = {
@@ -297,7 +303,7 @@ export const defaultSettings: SettingsValues = {
relay_auth: RelayAuthMode.Conservative,
send_delay: 0,
font_size: 1.1,
- muted_rooms: [],
+ alerts: [],
}
export const settingsByPubkey = deriveItemsByKey({
@@ -993,3 +999,18 @@ export const parseInviteLink = (invite: string): InviteData | undefined => {
})
)
}
+
+// Hierarchical notification helpers
+
+export const isMuted = (url: string, h?: string) => {
+ const {alerts} = getSettings()
+ const pref = alerts.find(spec({url}))
+
+ if (!pref) return true
+ if (!h) return pref.notify
+ if (pref.notify) return !pref.exceptions.includes(h)
+ if (!pref.notify) return pref.exceptions.includes(h)
+}
+
+export const deriveIsMuted = (url: string, h?: string) =>
+ derived(userSettingsValues, () => isMuted(url, h))
diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts
index 5405367f..cdcecc45 100644
--- a/src/app/util/notifications.ts
+++ b/src/app/util/notifications.ts
@@ -64,6 +64,7 @@ import {
getEventPath,
goToEvent,
} from "@app/util/routes"
+import type {SpaceNotificationSettings} from "@app/core/state"
import {
DM_KINDS,
CONTENT_KINDS,
@@ -83,6 +84,7 @@ import {
userSpaceUrls,
splitRoomId,
makeRoomId,
+ isMuted,
device,
} from "@app/core/state"
import {kv} from "@app/core/storage"
@@ -282,7 +284,7 @@ export const onNotification = call(() => {
if (!unsubscribe) {
unsubscribe = on(repository, "update", ({added}) => {
const $pubkey = pubkey.get()
- const {muted_rooms} = getSettings()
+ const {alerts} = getSettings()
for (const event of added) {
if (event.pubkey == $pubkey) {
@@ -290,11 +292,8 @@ export const onNotification = call(() => {
}
const h = getTagValue("h", event.tags)
- const muted = Array.from(tracker.getRelays(event.id)).every(
- url => h && muted_rooms.includes(makeRoomId(url, h)),
- )
- if (muted) {
+ if (Array.from(tracker.getRelays(event.id)).every(url => isMuted(url, h))) {
continue
}
@@ -491,7 +490,7 @@ class CapacitorNotifications implements IPushAdapter {
}
}
- _unsyncRelay = async (relay: string, keys: string[]) => {
+ _unsyncRelay = async (relay: string, key: string) => {
const stuff = await this._getPushStuff(relay)
if (!stuff) {
@@ -499,18 +498,11 @@ class CapacitorNotifications implements IPushAdapter {
return
}
- const {url} = stuff
- const tags: string[][] = []
- for (const key of keys) {
- const identifier = this._getSubscriptionIdentifier(relay, key)
- const address = new Address(30390, pubkey.get()!, identifier).toString()
-
- tags.push(["a", address])
- }
-
- const thunk = publishThunk({relays: [url], event: makeEvent(DELETE, {tags})})
-
- const error = await waitForThunkError(thunk)
+ const relays = [stuff.url]
+ const identifier = this._getSubscriptionIdentifier(relay, key)
+ const address = new Address(30390, pubkey.get()!, identifier).toString()
+ const event = makeEvent(DELETE, {tags: [["a", address]]})
+ const error = await waitForThunkError(publishThunk({relays, event}))
if (error) {
console.warn(`Failed to unsubscribe ${relay} from notifications:`, error)
@@ -521,32 +513,43 @@ class CapacitorNotifications implements IPushAdapter {
signal.addEventListener(
"abort",
merged([userSpaceUrls, notificationSettings, userSettingsValues]).subscribe(
- throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {muted_rooms}]) => {
- const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
+ throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
+ const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
for (const url of $userSpaceUrls) {
- if (!spaces && !mentions) {
- this._unsyncRelay(url, ["spaces", "mentions"])
- } else if (!spaces) {
- this._unsyncRelay(url, ["spaces"])
- } else if (!mentions) {
- this._unsyncRelay(url, ["mentions"])
- }
-
- const mutedRooms = muted_rooms.map(splitRoomId).filter(nthEq(0, url)).map(nth(1))
+ const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
+ const filters: Filter[] = []
+ const ignore: Filter[] = []
+ // Build filters based on spaces setting
if (spaces) {
- this._syncRelay(url, "spaces", filters, [{"#h": [mutedRooms]}])
+ if (notify) {
+ // notify=true: exceptions are opt-out (exclude those rooms)
+ if (exceptions.length > 0) {
+ ignore.push({"#h": exceptions})
+ }
+ // Include all other content
+ filters.push(...baseFilters)
+ } else {
+ // notify=false: exceptions are opt-in (only include those rooms)
+ if (exceptions.length > 0) {
+ filters.push(
+ ...baseFilters.map(f => ({...f, "#h": exceptions})),
+ )
+ }
+ }
}
+ // Build filters for mentions - always notify for p-tagged content
if (mentions) {
- const mentionFilters = filters.map(assoc("#p", [pubkey.get()!]))
+ filters.push(...baseFilters.map(f => ({...f, "#p": [pubkey.get()!]})))
+ }
- if (!spaces) {
- this._syncRelay(url, "mentions", mentionFilters)
- } else if (mutedRooms.length > 0) {
- this._syncRelay(url, "mentions", mentionFilters.map(assoc("#h", [mutedRooms])))
- }
+ // Sync or unsync based on whether we have filters
+ if (filters.length > 0) {
+ this._syncRelay(url, "spaces", filters, ignore)
+ } else {
+ this._unsyncRelay(url, "spaces")
}
}
}),
@@ -563,7 +566,7 @@ class CapacitorNotifications implements IPushAdapter {
if (messages) {
this._syncRelay(url, "messages", [{kinds: DM_KINDS, "#p": [pubkey.get()!]}])
} else {
- this._unsyncRelay(url, ["messages"])
+ this._unsyncRelay(url, "messages")
}
}
}),
@@ -619,11 +622,11 @@ class CapacitorNotifications implements IPushAdapter {
notificationState.set({})
- await Promise.all(get(userSpaceUrls).map(url => this._unsyncRelay(url, ["spaces", "mentions"])))
+ await Promise.all(get(userSpaceUrls).map(url => this._unsyncRelay(url, "spaces")))
await Promise.all(
getRelaysFromList(get(userMessagingRelayList)).map(url =>
- this._unsyncRelay(url, ["messages"]),
+ this._unsyncRelay(url, "messages"),
),
)
}
diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte
index 777c8c72..3e6880fa 100644
--- a/src/routes/discover/+page.svelte
+++ b/src/routes/discover/+page.svelte
@@ -20,7 +20,7 @@
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
import RelaySummary from "@app/components/RelaySummary.svelte"
- import SpaceCheck from "@app/components/SpaceCheck.svelte"
+ import SpaceJoin from "@app/components/SpaceJoin.svelte"
import {groupListPubkeysByUrl, parseInviteLink} from "@app/core/state"
import {pushModal} from "@app/util/modal"
@@ -36,9 +36,7 @@
})
const relaySearch = _derived(throttled(1000, relays), $relays => {
- const options = $relays.filter(
- r => $groupListPubkeysByUrl.has(r.url) && r.url !== inviteData?.url,
- )
+ const options = $relays.filter(r => $groupListPubkeysByUrl.has(r.url))
return createSearch(options, {
getValue: (relay: RelayProfile) => relay.url,
@@ -60,7 +58,7 @@
if (claim) {
pushModal(SpaceInviteAccept, {invite: term})
} else {
- pushModal(SpaceCheck, {url})
+ pushModal(SpaceJoin, {url})
}
}
@@ -69,7 +67,7 @@
let showScanner = $state(false)
let element: Element
- const options = $derived($relaySearch.searchOptions(term))
+ const options = $derived($relaySearch.searchOptions(term).filter(r => r.url !== inviteData?.url))
const inviteData = $derived(parseInviteLink(term))
onMount(() => {
diff --git a/src/routes/settings/alerts/+page.svelte b/src/routes/settings/alerts/+page.svelte
index d98e5ad8..63a9e334 100644
--- a/src/routes/settings/alerts/+page.svelte
+++ b/src/routes/settings/alerts/+page.svelte
@@ -105,23 +105,6 @@
-
-
Muted Rooms
- {#each muted_rooms as id (id)}
- {@const [url, h] = splitRoomId(id)}
-
- removeMutedRoom(id)}>
-
-
- Room " " on {displayRelayUrl(url)}
-
- {:else}
-
No muted rooms found.
- {/each}
-