Update notification subscriptions reactively
This commit is contained in:
parent
9b3a8258ce
commit
143a1dd39b
6 changed files with 162 additions and 64 deletions
20
package.json
20
package.json
|
|
@ -60,16 +60,16 @@
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.8",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "^0.8.1",
|
"@welshman/app": "^0.8.2",
|
||||||
"@welshman/content": "^0.8.1",
|
"@welshman/content": "^0.8.2",
|
||||||
"@welshman/editor": "^0.8.1",
|
"@welshman/editor": "^0.8.2",
|
||||||
"@welshman/feeds": "^0.8.1",
|
"@welshman/feeds": "^0.8.2",
|
||||||
"@welshman/lib": "^0.8.1",
|
"@welshman/lib": "^0.8.2",
|
||||||
"@welshman/net": "^0.8.1",
|
"@welshman/net": "^0.8.2",
|
||||||
"@welshman/router": "^0.8.1",
|
"@welshman/router": "^0.8.2",
|
||||||
"@welshman/signer": "^0.8.1",
|
"@welshman/signer": "^0.8.2",
|
||||||
"@welshman/store": "^0.8.1",
|
"@welshman/store": "^0.8.2",
|
||||||
"@welshman/util": "^0.8.1",
|
"@welshman/util": "^0.8.2",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"daisyui": "^4.12.24",
|
"daisyui": "^4.12.24",
|
||||||
"date-picker-svelte": "^2.17.0",
|
"date-picker-svelte": "^2.17.0",
|
||||||
|
|
|
||||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getProfile, loadProfile} from "@welshman/app"
|
import {getProfile, loadProfile} from "@welshman/app"
|
||||||
|
import {isMobile} from '@lib/html'
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const {pubkeys, size = 7}: Props = $props()
|
const {pubkeys, size = 7}: Props = $props()
|
||||||
|
const limit = isMobile ? 7 : 15
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
for (const pubkey of pubkeys) {
|
||||||
loadProfile(pubkey)
|
loadProfile(pubkey)
|
||||||
|
|
@ -21,7 +23,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex pr-3">
|
<div class="flex pr-3">
|
||||||
{#each visiblePubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
{#each visiblePubkeys.toSorted().slice(0, limit) as pubkey (pubkey)}
|
||||||
<div
|
<div
|
||||||
class="z-feature -mr-3 inline-block flex h-8 w-8 items-center justify-center rounded-full bg-base-100">
|
class="z-feature -mr-3 inline-block flex h-8 w-8 items-center justify-center rounded-full bg-base-100">
|
||||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||||
|
|
|
||||||
|
|
@ -338,19 +338,7 @@ export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
||||||
// Push notifications
|
// Push notifications
|
||||||
|
|
||||||
export const notificationSettings = withGetter(
|
export const notificationSettings = withGetter(
|
||||||
writable<{
|
writable({
|
||||||
push: boolean
|
|
||||||
sound: boolean
|
|
||||||
badge: boolean
|
|
||||||
spaces: boolean
|
|
||||||
mentions: boolean
|
|
||||||
messages: boolean
|
|
||||||
token?: string
|
|
||||||
subscription?: {
|
|
||||||
key: string
|
|
||||||
callback: string
|
|
||||||
}
|
|
||||||
}>({
|
|
||||||
push: false,
|
push: false,
|
||||||
sound: false,
|
sound: false,
|
||||||
badge: false,
|
badge: false,
|
||||||
|
|
@ -360,6 +348,17 @@ export const notificationSettings = withGetter(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const notificationState = withGetter(
|
||||||
|
writable<{
|
||||||
|
token?: string
|
||||||
|
subscription?: {
|
||||||
|
key: string
|
||||||
|
callback: string
|
||||||
|
}
|
||||||
|
}>({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {Capacitor} from "@capacitor/core"
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {PushNotifications} from "@capacitor/push-notifications"
|
import {PushNotifications} from "@capacitor/push-notifications"
|
||||||
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
||||||
import {synced, throttled} from "@welshman/store"
|
import {synced, throttled, deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
tracker,
|
tracker,
|
||||||
|
|
@ -12,9 +12,9 @@ import {
|
||||||
relaysByUrl,
|
relaysByUrl,
|
||||||
signer,
|
signer,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
getPubkeyRelays,
|
|
||||||
loadRelay,
|
loadRelay,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
|
userMessagingRelayList,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
|
|
@ -32,6 +32,8 @@ import {
|
||||||
now,
|
now,
|
||||||
groupBy,
|
groupBy,
|
||||||
postJson,
|
postJson,
|
||||||
|
nth,
|
||||||
|
nthEq,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {TrustedEvent, RelayProfile, Filter} from "@welshman/util"
|
import type {TrustedEvent, RelayProfile, Filter} from "@welshman/util"
|
||||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
|
|
@ -40,12 +42,14 @@ import {
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
THREAD,
|
THREAD,
|
||||||
COMMENT,
|
COMMENT,
|
||||||
|
DELETE,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
|
getRelaysFromList,
|
||||||
matchFilters,
|
matchFilters,
|
||||||
sortEventsDesc,
|
sortEventsDesc,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
RelayMode,
|
Address,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {buildUrl} from "@lib/util"
|
import {buildUrl} from "@lib/util"
|
||||||
import {
|
import {
|
||||||
|
|
@ -66,6 +70,7 @@ import {
|
||||||
PUSH_BRIDGE,
|
PUSH_BRIDGE,
|
||||||
PUSH_SERVER,
|
PUSH_SERVER,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
|
notificationState,
|
||||||
chatsById,
|
chatsById,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
|
@ -75,6 +80,7 @@ import {
|
||||||
getSpaceRoomsFromGroupList,
|
getSpaceRoomsFromGroupList,
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
|
splitRoomId,
|
||||||
makeRoomId,
|
makeRoomId,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {kv} from "@app/core/storage"
|
import {kv} from "@app/core/storage"
|
||||||
|
|
@ -342,6 +348,7 @@ if (Capacitor.isNativePlatform()) {
|
||||||
PushNotifications.addListener(
|
PushNotifications.addListener(
|
||||||
"pushNotificationActionPerformed",
|
"pushNotificationActionPerformed",
|
||||||
async (action: ActionPerformed) => {
|
async (action: ActionPerformed) => {
|
||||||
|
console.log('=======', JSON.stringify(action))
|
||||||
const event = parseJson(action.notification.data.event)
|
const event = parseJson(action.notification.data.event)
|
||||||
const relays = [action.notification.data.relay]
|
const relays = [action.notification.data.relay]
|
||||||
|
|
||||||
|
|
@ -362,7 +369,7 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
return status.receive
|
return status.receive
|
||||||
}
|
}
|
||||||
|
|
||||||
let {token} = notificationSettings.get()
|
let {token} = notificationState.get()
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
PushNotifications.addListener("registration", ({value}: Token) => {
|
PushNotifications.addListener("registration", ({value}: Token) => {
|
||||||
|
|
@ -381,32 +388,38 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
notificationSettings.update(assoc("token", token))
|
notificationState.update(assoc("token", token))
|
||||||
}
|
}
|
||||||
|
|
||||||
return token ? "granted" : "denied"
|
return token ? "granted" : "denied"
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncServer(signal: AbortSignal) {
|
async syncServer(signal: AbortSignal) {
|
||||||
const {token} = notificationSettings.get()
|
const {token, subscription} = notificationState.get()
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Attempted to sync push server without a token")
|
throw new Error("Attempted to sync push server without a token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm"
|
const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm"
|
||||||
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
||||||
const json = await postJson(url, {token}, {signal})
|
const json = await postJson(url, {token}, {signal})
|
||||||
|
|
||||||
if (json?.callback && json?.id) {
|
if (json?.callback && json?.key) {
|
||||||
notificationSettings.update(assoc("subscription", json))
|
notificationState.update(assoc("subscription", json))
|
||||||
} else {
|
} else {
|
||||||
console.warn("Failed to register with push server")
|
console.warn("Failed to register with push server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async syncRelays(signal: AbortSignal) {
|
async syncRelays(signal: AbortSignal) {
|
||||||
const {subscription} = notificationSettings.get()
|
const {subscription} = notificationState.get()
|
||||||
|
|
||||||
|
if (signal.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
throw new Error("Attempted to sync relays without a subscription")
|
throw new Error("Attempted to sync relays without a subscription")
|
||||||
|
|
@ -433,10 +446,6 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
filters: Filter[],
|
filters: Filter[],
|
||||||
ignore: Filter[] = [],
|
ignore: Filter[] = [],
|
||||||
) => {
|
) => {
|
||||||
if (signal.aborted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const stuff = await getPushStuff(relay)
|
const stuff = await getPushStuff(relay)
|
||||||
|
|
||||||
if (!stuff) {
|
if (!stuff) {
|
||||||
|
|
@ -475,25 +484,114 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const relay of get(userSpaceUrls)) {
|
const unsyncRelay = async (key: string, relay: string) => {
|
||||||
const {muted_rooms} = getSettings()
|
const stuff = await getPushStuff(relay)
|
||||||
|
|
||||||
|
if (!stuff) {
|
||||||
|
console.warn(`Failed to unsubscribe ${relay} from ${key} notifications: unsupported`)
|
||||||
|
} else {
|
||||||
|
const identifier = String(hash(subscription.callback + relay + key))
|
||||||
|
const address = new Address(30390, pubkey.get()!, identifier).toString()
|
||||||
|
|
||||||
|
const thunk = publishThunk({
|
||||||
|
signal,
|
||||||
|
relays: [stuff.url],
|
||||||
|
event: makeEvent(DELETE, {tags: [["a", address]]}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const error = await waitForThunkError(thunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.warn(`Failed to unsubscribe ${relay} from ${key} notifications:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncedSpaceUrls = new Set<string>()
|
||||||
|
|
||||||
|
const syncSpaceRelay = (url: string) => {
|
||||||
|
const {spaces, mentions} = notificationSettings.get()
|
||||||
const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
|
const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
|
||||||
const ignore = [{"#h": [muted_rooms]}]
|
const mutedRooms = getSettings().muted_rooms.map(splitRoomId).filter(nthEq(0, url)).map(nth(1))
|
||||||
|
|
||||||
syncRelay("spaces", relay, filters, ignore)
|
if (spaces) {
|
||||||
|
syncRelay("spaces", url, filters, [{"#h": [mutedRooms]}])
|
||||||
|
} else {
|
||||||
|
unsyncRelay("spaces", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const $pubkey = pubkey.get()!
|
if (mentions) {
|
||||||
|
const mentionFilters = filters.map(assoc("#p", [pubkey.get()!]))
|
||||||
|
|
||||||
for (const relay of getPubkeyRelays($pubkey, RelayMode.Messaging)) {
|
if (!spaces) {
|
||||||
const filters = [{kinds: DM_KINDS, "#p": [$pubkey]}]
|
syncRelay("mentions", url, mentionFilters)
|
||||||
|
} else if (mutedRooms.length > 0) {
|
||||||
syncRelay("messages", relay, filters)
|
syncRelay("mentions", url, mentionFilters.map(assoc('#h', [mutedRooms])))
|
||||||
|
} else {
|
||||||
|
unsyncRelay("mentions", url)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
unsyncRelay("mentions", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncSpaceRelays = () => {
|
||||||
|
const $userSpaceUrls = get(userSpaceUrls)
|
||||||
|
const {spaces, mentions} = notificationSettings.get()
|
||||||
|
|
||||||
|
for (const url of $userSpaceUrls) {
|
||||||
|
syncSpaceRelay(url)
|
||||||
|
syncedSpaceUrls.add(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const url of syncedSpaceUrls) {
|
||||||
|
if (!$userSpaceUrls.includes(url)) {
|
||||||
|
unsyncRelay("spaces", url)
|
||||||
|
syncedSpaceUrls.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncedMessagingUrls = new Set<string>()
|
||||||
|
|
||||||
|
const syncMessagingRelay = (url: string) => {
|
||||||
|
const {messages} = notificationSettings.get()
|
||||||
|
|
||||||
|
if (messages) {
|
||||||
|
syncRelay("messages", url, [{kinds: DM_KINDS, "#p": [pubkey.get()!]}])
|
||||||
|
} else {
|
||||||
|
unsyncRelay("messages", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMessagingRelays = () => {
|
||||||
|
const messagingRelayUrls = getRelaysFromList(get(userMessagingRelayList))
|
||||||
|
|
||||||
|
for (const url of messagingRelayUrls) {
|
||||||
|
syncMessagingRelay(url)
|
||||||
|
syncedMessagingUrls.add(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const url of syncedMessagingUrls) {
|
||||||
|
if (!messagingRelayUrls.includes(url)) {
|
||||||
|
unsyncRelay("messages", url)
|
||||||
|
syncedMessagingUrls.delete(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribers = [
|
||||||
|
userSpaceUrls.subscribe(syncSpaceRelays),
|
||||||
|
userMessagingRelayList.subscribe(syncMessagingRelays),
|
||||||
|
userSettingsValues.subscribe(syncSpaceRelays),
|
||||||
|
userSettingsValues.subscribe(syncMessagingRelays),
|
||||||
|
]
|
||||||
|
|
||||||
|
signal.addEventListener("abort", () => unsubscribers.forEach(call))
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const {token} = notificationSettings.get()
|
const {token} = notificationState.get()
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const {signal} = controller
|
const {signal} = controller
|
||||||
|
|
||||||
|
|
@ -502,13 +600,8 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
} else {
|
} else {
|
||||||
call(async () => {
|
call(async () => {
|
||||||
try {
|
try {
|
||||||
if (!notificationSettings.get().subscription) {
|
|
||||||
await this.syncServer(signal)
|
await this.syncServer(signal)
|
||||||
}
|
|
||||||
|
|
||||||
if (notificationSettings.get().subscription) {
|
|
||||||
await this.syncRelays(signal)
|
await this.syncRelays(signal)
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
@ -523,9 +616,7 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async disable() {
|
async disable() {
|
||||||
const {subscription, ...settings} = notificationSettings.get()
|
const {subscription} = notificationState.get()
|
||||||
|
|
||||||
await PushNotifications.unregister()
|
|
||||||
|
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", subscription.key), {
|
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", subscription.key), {
|
||||||
|
|
@ -537,7 +628,8 @@ class CapacitorNotifications implements IPushAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationSettings.set({...settings, push: false})
|
notificationSettings.update(assoc('push', false))
|
||||||
|
notificationState.set({})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
import {setupAnalytics} from "@app/util/analytics"
|
import {setupAnalytics} from "@app/util/analytics"
|
||||||
import {authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy} from "@app/util/policies"
|
import {authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy} from "@app/util/policies"
|
||||||
import {kv, db} from "@app/core/storage"
|
import {kv, db} from "@app/core/storage"
|
||||||
import {userSettingsValues, notificationSettings} from "@app/core/state"
|
import {userSettingsValues, notificationSettings, notificationState} from "@app/core/state"
|
||||||
import {syncApplicationData} from "@app/core/sync"
|
import {syncApplicationData} from "@app/core/sync"
|
||||||
import * as commands from "@app/core/commands"
|
import * as commands from "@app/core/commands"
|
||||||
import * as requests from "@app/core/requests"
|
import * as requests from "@app/core/requests"
|
||||||
|
|
@ -107,6 +107,11 @@
|
||||||
store: notificationSettings,
|
store: notificationSettings,
|
||||||
storage: kv,
|
storage: kv,
|
||||||
}),
|
}),
|
||||||
|
sync({
|
||||||
|
key: "notificationState",
|
||||||
|
store: notificationState,
|
||||||
|
storage: kv,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Set up our storage adapters
|
// Set up our storage adapters
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue