Add fcm push notifications
This commit is contained in:
parent
6cca823ed4
commit
3655790e5f
19 changed files with 189 additions and 67 deletions
|
|
@ -12,6 +12,7 @@ dependencies {
|
||||||
implementation project(':capacitor-community-safe-area')
|
implementation project(':capacitor-community-safe-area')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-keyboard')
|
implementation project(':capacitor-keyboard')
|
||||||
|
implementation project(':capacitor-push-notifications')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,5 @@
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,8 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
|
||||||
include ':capacitor-keyboard'
|
include ':capacitor-keyboard'
|
||||||
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
|
include ':capacitor-push-notifications'
|
||||||
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,5 +49,9 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ def capacitor_pods
|
||||||
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area'
|
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
|
||||||
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
|
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
|
||||||
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/core": "^7.0.1",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
"@capacitor/keyboard": "^7.0.0",
|
"@capacitor/keyboard": "^7.0.0",
|
||||||
|
"@capacitor/push-notifications": "^7.0.1",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.35.0",
|
"@sentry/browser": "^8.35.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.4",
|
"@sveltejs/adapter-static": "^3.0.4",
|
||||||
|
|
|
||||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -1,6 +1,5 @@
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {Capacitor} from "@capacitor/core"
|
|
||||||
import {randomId, flatten, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
import {randomId, flatten, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
||||||
import type {Feed} from "@welshman/feeds"
|
import type {Feed} from "@welshman/feeds"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
|
@ -21,6 +20,7 @@ import {
|
||||||
ALERT_ANDROID,
|
ALERT_ANDROID,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
|
getAddress,
|
||||||
displayProfile,
|
displayProfile,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
makeList,
|
makeList,
|
||||||
|
|
@ -62,8 +62,8 @@ import {
|
||||||
NOTIFIER_PUBKEY,
|
NOTIFIER_PUBKEY,
|
||||||
NOTIFIER_RELAY,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
|
deviceAlertAddresses,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {getPushInfo} from "@app/push"
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
|
|
@ -388,10 +388,10 @@ export type AlertParams = {
|
||||||
auth: string
|
auth: string
|
||||||
}
|
}
|
||||||
ios?: {
|
ios?: {
|
||||||
nothing: string
|
device_token: string
|
||||||
}
|
}
|
||||||
android?: {
|
android?: {
|
||||||
nothing: string
|
device_token: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,5 +433,10 @@ export const makeAlert = async (params: AlertParams) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishAlert = async (params: AlertParams) =>
|
export const publishAlert = async (params: AlertParams) => {
|
||||||
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
const event = await signer.get().sign(await makeAlert(params))
|
||||||
|
|
||||||
|
deviceAlertAddresses.update($addresses => [...$addresses, getAddress(event)])
|
||||||
|
|
||||||
|
return publishThunk({event, relays: [NOTIFIER_RELAY]})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {ucFirst} from "@lib/util"
|
||||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
import {decrypt} from "@welshman/signer"
|
||||||
|
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
displayRelayUrl,
|
||||||
|
getTagValue,
|
||||||
|
getAddress,
|
||||||
|
THREAD,
|
||||||
|
MESSAGE,
|
||||||
|
EVENT_TIME,
|
||||||
|
COMMENT,
|
||||||
|
} from "@welshman/util"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey, signer, getThunkError} from "@welshman/app"
|
||||||
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 FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
|
import {alerts, getMembershipUrls, userMembership, NOTIFIER_PUBKEY} from "@app/state"
|
||||||
import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
|
import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
|
||||||
import {publishAlert} from "@app/commands"
|
import {publishAlert} from "@app/commands"
|
||||||
import type {AlertParams} from "@app/commands"
|
import type {AlertParams} from "@app/commands"
|
||||||
|
|
@ -114,20 +124,41 @@
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
params[platform] = await getPushInfo()
|
params[platform] = await getPushInfo()
|
||||||
|
params.description = `${ucFirst(platform)} push notification ${description}.`
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: String(e),
|
message: String(e),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
params.description = `Push notification alert ${description}.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const thunk = await publishAlert(params)
|
// If we don't do this we'll get an event rejection
|
||||||
|
await Pool.get().get(NOTIFIER_RELAY).auth.attemptAuth()
|
||||||
|
|
||||||
await thunk.result
|
const thunk = await publishAlert(params)
|
||||||
await loadAlertStatuses($pubkey!)
|
const error = await getThunkError(thunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to send your alert to the notification server (${error}).`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch our new status to make sure it's active
|
||||||
|
const address = getAddress(thunk.event)
|
||||||
|
const statusEvents = await loadAlertStatuses($pubkey!)
|
||||||
|
const statusEvent = statusEvents.find(event => getTagValue("d", event.tags) === address)
|
||||||
|
const statusTags = statusEvent
|
||||||
|
? parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, statusEvent.content))
|
||||||
|
: []
|
||||||
|
const {status = "error", message = "Your alert was not activated"}: Record<string, string> =
|
||||||
|
fromPairs(statusTags)
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
return pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
|
||||||
pushToast({message: "Your alert has been successfully created!"})
|
pushToast({message: "Your alert has been successfully created!"})
|
||||||
back()
|
back()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {parseJson, nthEq} from "@welshman/lib"
|
import {parseJson} from "@welshman/lib"
|
||||||
import {displayFeeds} from "@welshman/feeds"
|
import {displayFeeds} from "@welshman/feeds"
|
||||||
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
||||||
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 AlertDelete from "@app/components/AlertDelete.svelte"
|
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||||
import type {Alert} from "@app/state"
|
import type {Alert} from "@app/state"
|
||||||
import {alertStatuses} from "@app/state"
|
import {deriveAlertStatus} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -15,8 +15,7 @@
|
||||||
|
|
||||||
const {alert}: Props = $props()
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
const address = $derived(getAddress(alert.event))
|
const status = deriveAlertStatus(getAddress(alert.event))
|
||||||
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
|
||||||
const cron = $derived(getTagValue("cron", alert.tags))
|
const cron = $derived(getTagValue("cron", alert.tags))
|
||||||
const channel = $derived(getTagValue("channel", alert.tags))
|
const channel = $derived(getTagValue("channel", alert.tags))
|
||||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||||
|
|
@ -39,24 +38,24 @@
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex-inline gap-1">{description}</div>
|
<div class="flex-inline gap-1">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if status}
|
{#if $status}
|
||||||
{@const statusText = getTagValue("status", status.tags) || "error"}
|
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||||
{#if statusText === "ok"}
|
{#if statusText === "ok"}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
{:else if statusText === "pending"}
|
{:else if statusText === "pending"}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
Pending
|
Pending
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@
|
||||||
memberships,
|
memberships,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
|
deviceAlerts,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
alerts,
|
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {loadAlerts} from "@app/requests"
|
import {loadAlerts} from "@app/requests"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
const alert = $derived($alerts.find(a => getTagValue("feed", a.tags)?.includes(url)))
|
const alert = $derived($deviceAlerts.find(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,28 @@
|
||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
||||||
|
import {PushNotifications} from "@capacitor/push-notifications"
|
||||||
|
import {sleep, parseJson} from "@welshman/lib"
|
||||||
|
import {isSignedEvent} from "@welshman/util"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {VAPID_PUBLIC_KEY} from "@app/state"
|
import {VAPID_PUBLIC_KEY} from "@app/state"
|
||||||
|
|
||||||
export const platform = Capacitor.getPlatform()
|
export const platform = Capacitor.getPlatform()
|
||||||
|
|
||||||
|
export const initializePushNotifications = () => {
|
||||||
|
if (platform === "web") return
|
||||||
|
|
||||||
|
PushNotifications.addListener("pushNotificationActionPerformed", (action: ActionPerformed) => {
|
||||||
|
const event = parseJson(action.notification.data.event)
|
||||||
|
const parsedRelays = parseJson(action.notification.data.relays)
|
||||||
|
const relays = Array.isArray(parsedRelays) ? parsedRelays : []
|
||||||
|
|
||||||
|
if (isSignedEvent(event)) {
|
||||||
|
goto("/" + nip19.neventEncode({id: event.id, relays}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const canSendPushNotifications = () => ["web", "android", "ios"].includes(platform)
|
export const canSendPushNotifications = () => ["web", "android", "ios"].includes(platform)
|
||||||
|
|
||||||
export const getWebPushInfo = async () => {
|
export const getWebPushInfo = async () => {
|
||||||
|
|
@ -36,7 +56,7 @@ export const getWebPushInfo = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const {endpoint, keys} = subscription.toJSON()
|
const {keys} = subscription.toJSON()
|
||||||
|
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
throw new Error(`Failed to get push info: no keys were returned`)
|
throw new Error(`Failed to get push info: no keys were returned`)
|
||||||
|
|
@ -49,12 +69,36 @@ export const getWebPushInfo = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIosPushInfo = async () => {
|
export const getCapacitorPushInfo = async () => {
|
||||||
return {}
|
let status = await PushNotifications.checkPermissions()
|
||||||
}
|
|
||||||
|
|
||||||
export const getAndroidPushInfo = async () => {
|
if (status.receive === "prompt") {
|
||||||
return {}
|
status = await PushNotifications.requestPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.receive !== "granted") {
|
||||||
|
throw new Error("Failed to register for push notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_token = ""
|
||||||
|
let error = "Failed to register for push notifications"
|
||||||
|
|
||||||
|
PushNotifications.addListener("registration", (token: Token) => {
|
||||||
|
device_token = token.value
|
||||||
|
})
|
||||||
|
|
||||||
|
PushNotifications.addListener("registrationError", (_error: RegistrationError) => {
|
||||||
|
error = _error.error
|
||||||
|
})
|
||||||
|
|
||||||
|
await PushNotifications.register()
|
||||||
|
await sleep(100)
|
||||||
|
|
||||||
|
if (!device_token) {
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {device_token}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPushInfo = (): Promise<Record<string, string>> => {
|
export const getPushInfo = (): Promise<Record<string, string>> => {
|
||||||
|
|
@ -62,9 +106,8 @@ export const getPushInfo = (): Promise<Record<string, string>> => {
|
||||||
case "web":
|
case "web":
|
||||||
return getWebPushInfo()
|
return getWebPushInfo()
|
||||||
case "ios":
|
case "ios":
|
||||||
return getIosPushInfo()
|
|
||||||
case "android":
|
case "android":
|
||||||
return getAndroidPushInfo()
|
return getCapacitorPushInfo()
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid push platform: ${platform}`)
|
throw new Error(`Invalid push platform: ${platform}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ export const listenForNotifications = () => {
|
||||||
|
|
||||||
for (const [url, allRooms] of userRoomsByUrl.get()) {
|
for (const [url, allRooms] of userRoomsByUrl.get()) {
|
||||||
// Limit how many rooms we load at a time, since we have to send a separate filter
|
// Limit how many rooms we load at a time, since we have to send a separate filter
|
||||||
// for each one due to nip 29 breaking postel's law
|
// for each one due to relay29 being picky
|
||||||
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
|
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
|
||||||
|
|
||||||
load({
|
load({
|
||||||
|
|
@ -375,6 +375,7 @@ export const listenForNotifications = () => {
|
||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [THREAD], limit: 1},
|
{kinds: [THREAD], limit: 1},
|
||||||
|
{kinds: [MESSAGE], limit: 1},
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||||
],
|
],
|
||||||
|
|
@ -385,6 +386,7 @@ export const listenForNotifications = () => {
|
||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [THREAD], since: now()},
|
{kinds: [THREAD], since: now()},
|
||||||
|
{kinds: [MESSAGE], since: now()},
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const goToEvent = async (event: TrustedEvent, options: Record<string, any
|
||||||
const urls = Array.from(tracker.getRelays(event.id))
|
const urls = Array.from(tracker.getRelays(event.id))
|
||||||
const path = await getEventPath(event, urls)
|
const path = await getEventPath(event, urls)
|
||||||
|
|
||||||
if (path.includes('://')) {
|
if (path.includes("://")) {
|
||||||
window.open(path)
|
window.open(path)
|
||||||
} else {
|
} else {
|
||||||
goto(path, options)
|
goto(path, options)
|
||||||
|
|
@ -94,7 +94,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === MESSAGE) {
|
if (event.kind === MESSAGE) {
|
||||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, 'chat')
|
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||||
}
|
}
|
||||||
|
|
||||||
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
||||||
|
|
@ -110,7 +110,7 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(kind) === MESSAGE) {
|
if (parseInt(kind) === MESSAGE) {
|
||||||
return room ? makeRoomPath(url, room) : makeSpacePath(url, 'chat')
|
return room ? makeRoomPath(url, room) : makeSpacePath(url, "chat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,14 @@ import {
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Socket} from "@welshman/net"
|
import type {Socket} from "@welshman/net"
|
||||||
import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net"
|
import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net"
|
||||||
import {collection, custom} from "@welshman/store"
|
import {
|
||||||
|
collection,
|
||||||
|
custom,
|
||||||
|
deriveEvents,
|
||||||
|
deriveEventsMapped,
|
||||||
|
withGetter,
|
||||||
|
synced,
|
||||||
|
} from "@welshman/store"
|
||||||
import {
|
import {
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
WRAP,
|
WRAP,
|
||||||
|
|
@ -56,7 +63,9 @@ import {
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
getTag,
|
getTag,
|
||||||
|
getTagValue,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
|
getAddress,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
||||||
import {Nip59, decrypt} from "@welshman/signer"
|
import {Nip59, decrypt} from "@welshman/signer"
|
||||||
|
|
@ -81,7 +90,6 @@ import {
|
||||||
appContext,
|
appContext,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk, Relay} from "@welshman/app"
|
import type {Thunk, Relay} from "@welshman/app"
|
||||||
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
|
|
||||||
|
|
||||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||||
|
|
||||||
|
|
@ -341,6 +349,8 @@ export const {
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
|
||||||
|
export const deviceAlertAddresses = synced<string[]>("deviceAlertAddresses", [])
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
tags: string[][]
|
tags: string[][]
|
||||||
|
|
@ -356,6 +366,12 @@ export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deviceAlerts = derived(
|
||||||
|
[deviceAlertAddresses, alerts],
|
||||||
|
([$deviceAlertAddresses, $alerts]) =>
|
||||||
|
$alerts.filter(a => $deviceAlertAddresses.includes(getAddress(a.event))),
|
||||||
|
)
|
||||||
|
|
||||||
// Alert Statuses
|
// Alert Statuses
|
||||||
|
|
||||||
export type AlertStatus = {
|
export type AlertStatus = {
|
||||||
|
|
@ -373,6 +389,9 @@ export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deriveAlertStatus = (address: string) =>
|
||||||
|
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
||||||
|
|
||||||
// Membership
|
// Membership
|
||||||
|
|
||||||
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {loadUserData, listenForNotifications} from "@app/requests"
|
import {loadUserData, listenForNotifications} from "@app/requests"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
|
import {initializePushNotifications} from "@app/push"
|
||||||
import * as commands from "@app/commands"
|
import * as commands from "@app/commands"
|
||||||
import * as requests from "@app/requests"
|
import * as requests from "@app/requests"
|
||||||
import * as notifications from "@app/notifications"
|
import * as notifications from "@app/notifications"
|
||||||
|
|
@ -73,6 +74,9 @@
|
||||||
dropSession($session.pubkey)
|
dropSession($session.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize push notification handler asap
|
||||||
|
initializePushNotifications()
|
||||||
|
|
||||||
const {children} = $props()
|
const {children} = $props()
|
||||||
|
|
||||||
const ready = $state(defer<void>())
|
const ready = $state(defer<void>())
|
||||||
|
|
@ -95,8 +99,8 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for navigation messages from service worker
|
// Listen for navigation messages from service worker
|
||||||
navigator.serviceWorker?.addEventListener('message', (event) => {
|
navigator.serviceWorker?.addEventListener("message", event => {
|
||||||
if (event.data && event.data.type === 'NAVIGATE') {
|
if (event.data && event.data.type === "NAVIGATE") {
|
||||||
goto(event.data.url)
|
goto(event.data.url)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {Address, getIdFilters, getTagValue} from "@welshman/util"
|
import {Address, getIdFilters} from "@welshman/util"
|
||||||
import {LOCAL_RELAY_URL} from "@welshman/relay"
|
import {LOCAL_RELAY_URL} from "@welshman/relay"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,12 @@
|
||||||
import * as nip19 from 'nostr-tools/nip19'
|
/* global clients */
|
||||||
import {parse, renderAsText} from '@welshman/content'
|
|
||||||
import {MESSAGE, THREAD, COMMENT, EVENT_TIME} from '@welshman/util'
|
|
||||||
|
|
||||||
const renderOptions = {
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
createElement: tag => ({
|
|
||||||
_text: "",
|
|
||||||
set innerText(text) {
|
|
||||||
this._text = text
|
|
||||||
},
|
|
||||||
get innerHTML() {
|
|
||||||
return this._text
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener("install", event => {
|
||||||
self.skipWaiting()
|
self.skipWaiting()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('activate', event => {
|
self.addEventListener("activate", event => {
|
||||||
event.waitUntil(self.clients.claim())
|
event.waitUntil(self.clients.claim())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -26,26 +14,37 @@ self.addEventListener("push", e => {
|
||||||
console.log("Service Worker: Push event received", e)
|
console.log("Service Worker: Push event received", e)
|
||||||
|
|
||||||
let url = "/"
|
let url = "/"
|
||||||
|
let title = "New activity"
|
||||||
let body = "You have a new message"
|
let body = "You have a new message"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {event, relays = []} = e.data?.json() || {}
|
const data = e.data?.json()
|
||||||
|
|
||||||
if (event) {
|
if (data?.event) {
|
||||||
url += nip19.neventEncode({id: event.id, relays})
|
url += nip19.neventEncode({
|
||||||
body = renderAsText(parse(event), renderOptions).toString()
|
id: data.event.id,
|
||||||
|
relays: data.relays || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.title) {
|
||||||
|
title = data.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.body) {
|
||||||
|
body = data.body
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Service Worker: Failed to parse push data", e)
|
console.log("Service Worker: Failed to parse push data", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.waitUntil(
|
e.waitUntil(
|
||||||
self.registration.showNotification("New message", {
|
self.registration.showNotification(title, {
|
||||||
body,
|
body,
|
||||||
data: {url},
|
data: {url},
|
||||||
icon: "/pwa-192x192.png",
|
icon: "/pwa-192x192.png",
|
||||||
badge: "/pwa-64x64.png",
|
badge: "/pwa-64x64.png",
|
||||||
tag: 'flotilla-notification',
|
tag: "flotilla-notification",
|
||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -74,8 +73,8 @@ self.addEventListener("notificationclick", e => {
|
||||||
for (const client of clientList) {
|
for (const client of clientList) {
|
||||||
if (client.url.includes(location.origin)) {
|
if (client.url.includes(location.origin)) {
|
||||||
client.postMessage({
|
client.postMessage({
|
||||||
type: 'NAVIGATE',
|
type: "NAVIGATE",
|
||||||
url: url
|
url: url,
|
||||||
})
|
})
|
||||||
|
|
||||||
return client.focus()
|
return client.focus()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue