forked from coracle/flotilla
Clean up and fix push notifications implementation
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ VITE_PLATFORM_RELAYS=
|
|||||||
VITE_PLATFORM_ACCENT="#7161FF"
|
VITE_PLATFORM_ACCENT="#7161FF"
|
||||||
VITE_PLATFORM_SECONDARY="#EB5E28"
|
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||||
VITE_PUSH_SERVER=
|
VITE_PUSH_SERVER=https://nps.flotilla.social/
|
||||||
VITE_PUSH_BRIDGE=
|
VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
||||||
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one
|
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one
|
||||||
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||||
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||||
|
|||||||
+6
-3
@@ -11,12 +11,15 @@ const config: CapacitorConfig = {
|
|||||||
adjustMarginsForEdgeToEdge: false,
|
adjustMarginsForEdgeToEdge: false,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
SystemBars: {
|
||||||
|
insetsHandling: "disable",
|
||||||
|
},
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash",
|
||||||
},
|
},
|
||||||
Keyboard: {
|
Keyboard: {
|
||||||
style: "DARK",
|
style: "DARK",
|
||||||
resizeOnFullScreen: true,
|
// resizeOnFullScreen: true,
|
||||||
},
|
},
|
||||||
Badge: {
|
Badge: {
|
||||||
persist: true,
|
persist: true,
|
||||||
@@ -25,7 +28,7 @@ const config: CapacitorConfig = {
|
|||||||
},
|
},
|
||||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||||
// server: {
|
// server: {
|
||||||
// url: "http://192.168.1.115:1847",
|
// url: "http://192.168.1.65:1847",
|
||||||
// cleartext: true
|
// cleartext: true
|
||||||
// },
|
// },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios"
|
|||||||
|
|
||||||
export const PUSH_SERVER = import.meta.env.VITE_PUSH_SERVER
|
export const PUSH_SERVER = import.meta.env.VITE_PUSH_SERVER
|
||||||
|
|
||||||
export const PUSH_BRIDGE = import.meta.env.VITE_PUSH_BRIDGE
|
export const PUSH_BRIDGE = normalizeRelayUrl(import.meta.env.VITE_PUSH_BRIDGE)
|
||||||
|
|
||||||
export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY
|
export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
assoc,
|
assoc,
|
||||||
poll,
|
poll,
|
||||||
prop,
|
prop,
|
||||||
sha256,
|
hash,
|
||||||
textEncoder,
|
textEncoder,
|
||||||
parseJson,
|
parseJson,
|
||||||
flatten,
|
flatten,
|
||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
postJson,
|
postJson,
|
||||||
fetchJson,
|
fetchJson,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
import type {TrustedEvent, RelayProfile, Filter} from "@welshman/util"
|
||||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
import {
|
import {
|
||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
makeEvent,
|
makeEvent,
|
||||||
RelayMode,
|
RelayMode,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
|
import {buildUrl} from '@lib/util'
|
||||||
import {
|
import {
|
||||||
makeSpacePath,
|
makeSpacePath,
|
||||||
makeChatPath,
|
makeChatPath,
|
||||||
@@ -73,6 +74,7 @@ import {
|
|||||||
chatsById,
|
chatsById,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
userSettings,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
userGroupList,
|
userGroupList,
|
||||||
getSpaceUrlsFromGroupList,
|
getSpaceUrlsFromGroupList,
|
||||||
@@ -336,12 +338,13 @@ export const clearBadges = async () => {
|
|||||||
// Local notifications
|
// Local notifications
|
||||||
|
|
||||||
interface IPushAdapter {
|
interface IPushAdapter {
|
||||||
request: () => Promise<string>
|
request: (prompt: boolean) => Promise<string>
|
||||||
|
cancel: () => Promise<void>
|
||||||
start: () => Unsubscriber
|
start: () => Unsubscriber
|
||||||
}
|
}
|
||||||
|
|
||||||
class CapacitorNotifications implements IPushAdapter {
|
class CapacitorNotifications implements IPushAdapter {
|
||||||
async sync() {
|
async sync(signal: AbortSignal) {
|
||||||
const token = get(pushToken)
|
const token = get(pushToken)
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -355,21 +358,23 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
const secret = get(pushSecret)
|
const secret = get(pushSecret)
|
||||||
|
|
||||||
if (secret) {
|
if (secret) {
|
||||||
const {callback} = await fetchJson(`${PUSH_SERVER}/subscription/${secret}`)
|
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", secret), {signal})
|
||||||
|
|
||||||
if (callback) {
|
if (res.ok) {
|
||||||
return {secret, callback}
|
const {callback} = await res.json()
|
||||||
} else {
|
|
||||||
pushSecret.set(undefined)
|
if (callback) {
|
||||||
|
return {secret, callback}
|
||||||
|
} else {
|
||||||
|
pushSecret.set(undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ios = Capacitor.getPlatform() === "ios"
|
const ios = Capacitor.getPlatform() === "ios"
|
||||||
const channel = ios ? "apns" : "fcm"
|
const channel = ios ? "apns" : "fcm"
|
||||||
const topic = "social.flotilla"
|
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
||||||
const data = ios ? {token, topic} : {token}
|
const json = await postJson(url, {token}, {signal})
|
||||||
|
|
||||||
const json = await postJson(`${PUSH_SERVER}/subscription/${channel}`, data)
|
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
pushSecret.set(json.sk)
|
pushSecret.set(json.sk)
|
||||||
@@ -382,10 +387,13 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canPush = (relay?: RelayProfile) =>
|
||||||
|
Boolean(relay?.self && relay?.supported_nips?.map(String)?.includes("9a"))
|
||||||
|
|
||||||
const getPushStuff = async (url: string) => {
|
const getPushStuff = async (url: string) => {
|
||||||
let relay = await loadRelay(url)
|
let relay = await loadRelay(url)
|
||||||
|
|
||||||
if (!relay?.self || !relay?.supported_nips?.includes("9a")) {
|
if (!canPush(relay)) {
|
||||||
relay = await loadRelay(PUSH_BRIDGE)
|
relay = await loadRelay(PUSH_BRIDGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,9 +414,10 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
console.warn(`Failed to subscribe ${relay} to ${key} notifications: unsupported`)
|
console.warn(`Failed to subscribe ${relay} to ${key} notifications: unsupported`)
|
||||||
} else {
|
} else {
|
||||||
const {url, pubkey} = stuff
|
const {url, pubkey} = stuff
|
||||||
const identifier = await sha256(textEncoder.encode(info.callback + relay + key))
|
const identifier = String(hash(info.callback + relay + key))
|
||||||
|
|
||||||
const thunk = publishThunk({
|
const thunk = publishThunk({
|
||||||
|
signal,
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(30390, {
|
event: makeEvent(30390, {
|
||||||
content: await signer
|
content: await signer
|
||||||
@@ -454,10 +463,10 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async request() {
|
async request(prompt = true) {
|
||||||
let status = await PushNotifications.checkPermissions()
|
let status = await PushNotifications.checkPermissions()
|
||||||
|
|
||||||
if (status.receive === "prompt") {
|
if (prompt && status.receive === "prompt") {
|
||||||
status = await PushNotifications.requestPermissions()
|
status = await PushNotifications.requestPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,53 +474,50 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
return status.receive
|
return status.receive
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = ""
|
let token = get(pushToken)
|
||||||
|
|
||||||
PushNotifications.addListener("registration", ({value}: Token) => {
|
if (!token) {
|
||||||
token = value
|
PushNotifications.addListener("registration", ({value}: Token) => {
|
||||||
})
|
token = value
|
||||||
|
})
|
||||||
|
|
||||||
PushNotifications.addListener("registrationError", (error: RegistrationError) => {
|
PushNotifications.addListener("registrationError", (error: RegistrationError) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
await PushNotifications.register()
|
await Promise.all([
|
||||||
await poll({
|
PushNotifications.register(),
|
||||||
condition: () => Boolean(token),
|
poll({
|
||||||
signal: AbortSignal.timeout(5000),
|
condition: () => Boolean(token),
|
||||||
})
|
signal: AbortSignal.timeout(5000),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
if (token) {
|
|
||||||
pushToken.set(token)
|
pushToken.set(token)
|
||||||
|
|
||||||
return "granted"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToken.set(undefined)
|
return token ? "granted" : "denied"
|
||||||
|
|
||||||
return "denied"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.sync().then(() => {
|
const controller = new AbortController()
|
||||||
PushNotifications.addListener(
|
|
||||||
"pushNotificationActionPerformed",
|
|
||||||
async (action: ActionPerformed) => {
|
|
||||||
const event = parseJson(action.notification.data.event)
|
|
||||||
const relays = [action.notification.data.relay]
|
|
||||||
|
|
||||||
goto(await getEventPath(event, relays))
|
this.sync(controller.signal)
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => PushNotifications.removeAllListeners()
|
return () => controller.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
await PushNotifications.unregister()
|
||||||
|
|
||||||
|
pushSecret.set(undefined)
|
||||||
|
pushToken.set(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebNotifications implements IPushAdapter {
|
class WebNotifications implements IPushAdapter {
|
||||||
async request() {
|
async request(prompt = true) {
|
||||||
if (Notification?.permission === "default") {
|
if (prompt && Notification?.permission === "default") {
|
||||||
await Notification.requestPermission()
|
await Notification.requestPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +567,10 @@ class WebNotifications implements IPushAdapter {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Push {
|
export class Push {
|
||||||
@@ -580,39 +590,24 @@ export class Push {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static start() {
|
static start() {
|
||||||
return Push._getAdapter().start()
|
const adapter = Push._getAdapter()
|
||||||
|
const promise = adapter.request(false)
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
|
promise.then(permissions => {
|
||||||
|
if (permissions === "granted" && !controller.signal.aborted) {
|
||||||
|
controller.signal.addEventListener("abort", adapter.start())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Push._unsubscriber = () => controller.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
static request() {
|
static request() {
|
||||||
return Push._getAdapter().request()
|
return Push._getAdapter().request()
|
||||||
}
|
}
|
||||||
|
|
||||||
static resume() {
|
static cancel() {
|
||||||
const unsubscribe = userSettingsValues.subscribe(({alerts_push}) => {
|
return Push._getAdapter().cancel()
|
||||||
if (alerts_push) {
|
|
||||||
const promise = Push.request()
|
|
||||||
const controller = new AbortController()
|
|
||||||
|
|
||||||
promise.then(permissions => {
|
|
||||||
if (permissions === "granted" && !controller.signal.aborted) {
|
|
||||||
controller.signal.addEventListener("abort", Push.start())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Push._unsubscriber = () => controller.abort()
|
|
||||||
} else {
|
|
||||||
Push.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
Push.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static stop() {
|
|
||||||
Push._unsubscriber?.()
|
|
||||||
Push._unsubscriber = undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,11 @@ export const day = (seconds: number) => Math.floor(seconds / DAY)
|
|||||||
export const daysBetween = (start: number, end: number) => [...range(start, end, DAY)].map(day)
|
export const daysBetween = (start: number, end: number) => [...range(start, end, DAY)].map(day)
|
||||||
|
|
||||||
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
||||||
|
|
||||||
|
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
||||||
|
const url = new URL(base)
|
||||||
|
|
||||||
|
url.pathname = '/' + pathname.join('/')
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@src/app.css"
|
import "@src/app.css"
|
||||||
import "@capacitor-community/safe-area"
|
import "@capacitor-community/safe-area"
|
||||||
|
import {PushNotifications} from "@capacitor/push-notifications"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import type {Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
@@ -43,6 +44,17 @@
|
|||||||
|
|
||||||
const policies = [authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy]
|
const policies = [authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy]
|
||||||
|
|
||||||
|
PushNotifications.addListener(
|
||||||
|
"pushNotificationActionPerformed",
|
||||||
|
async (action: ActionPerformed) => {
|
||||||
|
console.log('====== action', JSON.stringify(action))
|
||||||
|
const event = parseJson(action.notification.data.event)
|
||||||
|
const relays = [action.notification.data.relay]
|
||||||
|
|
||||||
|
goto(await getEventPath(event, relays))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Add stuff to window for convenience
|
// Add stuff to window for convenience
|
||||||
Object.assign(window, {
|
Object.assign(window, {
|
||||||
get,
|
get,
|
||||||
@@ -129,7 +141,7 @@
|
|||||||
unsubscribers.push(syncKeyboard())
|
unsubscribers.push(syncKeyboard())
|
||||||
|
|
||||||
// Initialize background notifications
|
// Initialize background notifications
|
||||||
unsubscribers.push(notifications.Push.resume())
|
unsubscribers.push(notifications.Push.start())
|
||||||
|
|
||||||
// Listen for signer errors, report to user via toast
|
// Listen for signer errors, report to user via toast
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
const onsubmit = preventDefault(async () => {
|
const onsubmit = preventDefault(async () => {
|
||||||
await publishSettings($state.snapshot(settings))
|
await publishSettings($state.snapshot(settings))
|
||||||
|
|
||||||
|
if (settings.alerts_push) {
|
||||||
|
await Alerts.start()
|
||||||
|
} else {
|
||||||
|
await Alerts.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
pushToast({message: "Your settings have been saved!"})
|
pushToast({message: "Your settings have been saved!"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user