Get web push working
This commit is contained in:
parent
18a383edab
commit
6cca823ed4
12 changed files with 321 additions and 120 deletions
|
|
@ -13,5 +13,6 @@ VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.
|
||||||
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
||||||
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||||
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
||||||
|
VITE_VAPID_PUBLIC_KEY=
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
GLITCHTIP_AUTH_TOKEN=
|
GLITCHTIP_AUTH_TOKEN=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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 {randomId, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
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"
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,8 +15,10 @@ import {
|
||||||
AUTH_JOIN,
|
AUTH_JOIN,
|
||||||
ROOMS,
|
ROOMS,
|
||||||
COMMENT,
|
COMMENT,
|
||||||
ALERT_REQUEST_PUSH,
|
ALERT_EMAIL,
|
||||||
ALERT_REQUEST_EMAIL,
|
ALERT_WEB,
|
||||||
|
ALERT_IOS,
|
||||||
|
ALERT_ANDROID,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
displayProfile,
|
displayProfile,
|
||||||
|
|
@ -60,6 +63,7 @@ import {
|
||||||
NOTIFIER_RELAY,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
import {getPushInfo} from "@app/push"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
|
|
@ -369,41 +373,58 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
||||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeComment(params), relays})
|
publishThunk({event: makeComment(params), relays})
|
||||||
|
|
||||||
export type EmailAlertParams = {
|
export type AlertParams = {
|
||||||
feed: Feed
|
feed: Feed
|
||||||
|
description: string
|
||||||
|
claims: Record<string, string>
|
||||||
|
email?: {
|
||||||
cron: string
|
cron: string
|
||||||
email: string
|
email: string
|
||||||
description: string
|
handler: string[]
|
||||||
claims: Record<string, string>
|
}
|
||||||
|
web?: {
|
||||||
|
endpoint: string
|
||||||
|
p256dh: string
|
||||||
|
auth: string
|
||||||
|
}
|
||||||
|
ios?: {
|
||||||
|
nothing: string
|
||||||
|
}
|
||||||
|
android?: {
|
||||||
|
nothing: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeEmailAlert = async ({
|
export const makeAlert = async (params: AlertParams) => {
|
||||||
cron,
|
|
||||||
email,
|
|
||||||
feed,
|
|
||||||
claims,
|
|
||||||
description,
|
|
||||||
}: EmailAlertParams) => {
|
|
||||||
const tags = [
|
const tags = [
|
||||||
["feed", JSON.stringify(feed)],
|
["feed", JSON.stringify(params.feed)],
|
||||||
["cron", cron],
|
|
||||||
["email", email],
|
|
||||||
["locale", LOCALE],
|
["locale", LOCALE],
|
||||||
["timezone", TIMEZONE],
|
["timezone", TIMEZONE],
|
||||||
["description", description],
|
["description", params.description],
|
||||||
[
|
|
||||||
"handler",
|
|
||||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
|
||||||
"wss://relay.nostr.band/",
|
|
||||||
"web",
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const [relay, claim] of Object.entries(claims)) {
|
for (const [relay, claim] of Object.entries(params.claims)) {
|
||||||
tags.push(["claim", relay, claim])
|
tags.push(["claim", relay, claim])
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeEvent(ALERT_REQUEST_EMAIL, {
|
let kind: number
|
||||||
|
if (params.email) {
|
||||||
|
kind = ALERT_EMAIL
|
||||||
|
tags.push(...Object.entries(params.email).map(flatten))
|
||||||
|
} else if (params.web) {
|
||||||
|
kind = ALERT_WEB
|
||||||
|
tags.push(...Object.entries(params.web).map(flatten))
|
||||||
|
} else if (params.ios) {
|
||||||
|
kind = ALERT_IOS
|
||||||
|
tags.push(...Object.entries(params.ios).map(flatten))
|
||||||
|
} else if (params.android) {
|
||||||
|
kind = ALERT_ANDROID
|
||||||
|
tags.push(...Object.entries(params.android).map(flatten))
|
||||||
|
} else {
|
||||||
|
throw new Error("Alert has invalid params")
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeEvent(kind, {
|
||||||
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
|
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
|
||||||
tags: [
|
tags: [
|
||||||
["d", randomId()],
|
["d", randomId()],
|
||||||
|
|
@ -412,37 +433,5 @@ export const makeEmailAlert = async ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishEmailAlert = async (params: EmailAlertParams) =>
|
export const publishAlert = async (params: AlertParams) =>
|
||||||
publishThunk({event: await makeEmailAlert(params), relays: [NOTIFIER_RELAY]})
|
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||||
|
|
||||||
export type PushAlertParams = {
|
|
||||||
feed: Feed
|
|
||||||
description: string
|
|
||||||
claims: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makePushAlert = async ({feed, claims, description}: PushAlertParams) => {
|
|
||||||
const tags = [
|
|
||||||
["feed", JSON.stringify(feed)],
|
|
||||||
["locale", LOCALE],
|
|
||||||
["timezone", TIMEZONE],
|
|
||||||
["description", description],
|
|
||||||
["token", ""],
|
|
||||||
["platform", ""],
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const [relay, claim] of Object.entries(claims)) {
|
|
||||||
tags.push(["claim", relay, claim])
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeEvent(ALERT_REQUEST_PUSH, {
|
|
||||||
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
|
|
||||||
tags: [
|
|
||||||
["d", randomId()],
|
|
||||||
["p", NOTIFIER_PUBKEY],
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishPushAlert = async (params: PushAlertParams) =>
|
|
||||||
publishThunk({event: await makePushAlert(params), relays: [NOTIFIER_RELAY]})
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||||
|
|
@ -13,7 +14,9 @@
|
||||||
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, getMembershipRoomsByUrl, userMembership} from "@app/state"
|
||||||
import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
|
import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
|
||||||
import {publishEmailAlert, publishPushAlert} from "@app/commands"
|
import {publishAlert} from "@app/commands"
|
||||||
|
import type {AlertParams} from "@app/commands"
|
||||||
|
import {platform, canSendPushNotifications, getPushInfo} from "@app/push"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -45,7 +48,7 @@
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!email.includes("@")) {
|
if (channel === "email" && !email.includes("@")) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please provide an email address",
|
message: "Please provide an email address",
|
||||||
|
|
@ -83,23 +86,45 @@
|
||||||
|
|
||||||
if (notifyChat) {
|
if (notifyChat) {
|
||||||
display.push("chat")
|
display.push("chat")
|
||||||
filters.push({
|
filters.push({kinds: [MESSAGE]})
|
||||||
kinds: [MESSAGE],
|
|
||||||
"#h": getMembershipRoomsByUrl(relay, $userMembership),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claims = await requestRelayClaims([relay])
|
const claims = await requestRelayClaims([relay])
|
||||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
|
||||||
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
|
|
||||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
||||||
const thunk =
|
const description = `for ${displayList(display)} on ${displayRelayUrl(relay)}`
|
||||||
channel === "email"
|
const params: AlertParams = {feed, claims, description}
|
||||||
? await publishEmailAlert({cron, email, feed, claims, description})
|
|
||||||
: await publishPushAlert({feed, claims, description})
|
if (channel === "email") {
|
||||||
|
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||||
|
|
||||||
|
params.description = `${cadence} alert ${description}, sent via email.`
|
||||||
|
params.email = {
|
||||||
|
cron,
|
||||||
|
email,
|
||||||
|
handler: [
|
||||||
|
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||||
|
"wss://relay.nostr.band/",
|
||||||
|
"web",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
params[platform] = await getPushInfo()
|
||||||
|
} catch (e: any) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: String(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
params.description = `Push notification alert ${description}.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const thunk = await publishAlert(params)
|
||||||
|
|
||||||
await thunk.result
|
await thunk.result
|
||||||
await loadAlertStatuses($pubkey!)
|
await loadAlertStatuses($pubkey!)
|
||||||
|
|
@ -110,6 +135,12 @@
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!canSendPushNotifications()) {
|
||||||
|
channel = "email"
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
|
@ -118,17 +149,19 @@
|
||||||
Add an Alert
|
Add an Alert
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
{#if canSendPushNotifications()}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Alert Type*</p>
|
<p>Alert Type*</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<select bind:value={channel} class="select select-bordered">
|
<select bind:value={channel} class="select select-bordered">
|
||||||
<option value="push">Push Notification</option>
|
|
||||||
<option value="email">Email Digest</option>
|
<option value="email">Email Digest</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
</select>
|
</select>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
{#if channel === "email"}
|
{#if channel === "email"}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@
|
||||||
|
|
||||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||||
|
|
||||||
const addAlert = () => pushModal(AlertAdd, {relay: url, channel: "push"})
|
const addAlert = () => pushModal(AlertAdd, {relay: url, channel: "push"}, {replaceState})
|
||||||
|
|
||||||
const deleteAlert = () => pushModal(AlertDelete, {alert})
|
const deleteAlert = () => pushModal(AlertDelete, {alert}, {replaceState})
|
||||||
|
|
||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
let replaceState = $state(false)
|
let replaceState = $state(false)
|
||||||
|
|
|
||||||
71
src/app/push.ts
Normal file
71
src/app/push.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {VAPID_PUBLIC_KEY} from "@app/state"
|
||||||
|
|
||||||
|
export const platform = Capacitor.getPlatform()
|
||||||
|
|
||||||
|
export const canSendPushNotifications = () => ["web", "android", "ios"].includes(platform)
|
||||||
|
|
||||||
|
export const getWebPushInfo = async () => {
|
||||||
|
if (!("serviceWorker" in navigator)) {
|
||||||
|
throw new Error("Service Worker not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("PushManager" in window)) {
|
||||||
|
throw new Error("Push messaging not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission === "denied") {
|
||||||
|
throw new Error("Push notifications are blocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission !== "granted") {
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
|
||||||
|
if (permission !== "granted") {
|
||||||
|
throw new Error("Push notification permission denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready
|
||||||
|
let subscription = await registration.pushManager.getSubscription()
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: VAPID_PUBLIC_KEY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const {endpoint, keys} = subscription.toJSON()
|
||||||
|
|
||||||
|
if (!keys) {
|
||||||
|
throw new Error(`Failed to get push info: no keys were returned`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
p256dh: keys.p256dh,
|
||||||
|
auth: keys.auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIosPushInfo = async () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndroidPushInfo = async () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPushInfo = (): Promise<Record<string, string>> => {
|
||||||
|
switch (platform) {
|
||||||
|
case "web":
|
||||||
|
return getWebPushInfo()
|
||||||
|
case "ios":
|
||||||
|
return getIosPushInfo()
|
||||||
|
case "android":
|
||||||
|
return getAndroidPushInfo()
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid push platform: ${platform}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,8 +25,10 @@ import {
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
AUTH_INVITE,
|
AUTH_INVITE,
|
||||||
COMMENT,
|
COMMENT,
|
||||||
ALERT_REQUEST_EMAIL,
|
ALERT_EMAIL,
|
||||||
ALERT_REQUEST_PUSH,
|
ALERT_WEB,
|
||||||
|
ALERT_IOS,
|
||||||
|
ALERT_ANDROID,
|
||||||
ALERT_STATUS,
|
ALERT_STATUS,
|
||||||
matchFilters,
|
matchFilters,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
|
|
@ -349,7 +351,7 @@ export const makeCalendarFeed = ({
|
||||||
export const loadAlerts = (pubkey: string) =>
|
export const loadAlerts = (pubkey: string) =>
|
||||||
load({
|
load({
|
||||||
relays: [NOTIFIER_RELAY],
|
relays: [NOTIFIER_RELAY],
|
||||||
filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH], authors: [pubkey]}],
|
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], authors: [pubkey]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const loadAlertStatuses = (pubkey: string) =>
|
export const loadAlertStatuses = (pubkey: string) =>
|
||||||
|
|
|
||||||
|
|
@ -61,34 +61,40 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const goToMessage = async (url: string, room: string | undefined, id: string) => {
|
export const goToEvent = async (event: TrustedEvent, options: Record<string, any> = {}) => {
|
||||||
await goto(room ? makeRoomPath(url, room) : makeSpacePath(url, "chat"))
|
|
||||||
await sleep(300)
|
|
||||||
|
|
||||||
return scrollToEvent(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const goToEvent = async (event: TrustedEvent) => {
|
|
||||||
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) {
|
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) {
|
||||||
return await scrollToEvent(event.id)
|
await scrollToEvent(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const urls = Array.from(tracker.getRelays(event.id))
|
const urls = Array.from(tracker.getRelays(event.id))
|
||||||
|
const path = await getEventPath(event, urls)
|
||||||
|
|
||||||
|
if (path.includes('://')) {
|
||||||
|
window.open(path)
|
||||||
|
} else {
|
||||||
|
goto(path, options)
|
||||||
|
|
||||||
|
await sleep(300)
|
||||||
|
await scrollToEvent(event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||||
const room = getTagValue(ROOM, event.tags)
|
const room = getTagValue(ROOM, event.tags)
|
||||||
|
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
const url = urls[0]
|
const url = urls[0]
|
||||||
|
|
||||||
if (event.kind === THREAD) {
|
if (event.kind === THREAD) {
|
||||||
return goto(makeThreadPath(url, event.id))
|
return makeThreadPath(url, event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === EVENT_TIME) {
|
if (event.kind === EVENT_TIME) {
|
||||||
return goto(makeCalendarPath(url, event.id))
|
return makeCalendarPath(url, event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === MESSAGE) {
|
if (event.kind === MESSAGE) {
|
||||||
return goToMessage(url, room, event.id)
|
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]
|
||||||
|
|
@ -96,18 +102,18 @@ export const goToEvent = async (event: TrustedEvent) => {
|
||||||
|
|
||||||
if (id && kind) {
|
if (id && kind) {
|
||||||
if (parseInt(kind) === THREAD) {
|
if (parseInt(kind) === THREAD) {
|
||||||
return goto(makeThreadPath(url, id))
|
return makeThreadPath(url, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(kind) === EVENT_TIME) {
|
if (parseInt(kind) === EVENT_TIME) {
|
||||||
return goto(makeCalendarPath(url, id))
|
return makeCalendarPath(url, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(kind) === MESSAGE) {
|
if (parseInt(kind) === MESSAGE) {
|
||||||
return goToMessage(url, room, id)
|
return room ? makeRoomPath(url, room) : makeSpacePath(url, 'chat')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(entityLink(nip19.neventEncode({id: event.id, relays: urls})))
|
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,10 @@ import {
|
||||||
ROOM_JOIN,
|
ROOM_JOIN,
|
||||||
ROOM_ADD_USER,
|
ROOM_ADD_USER,
|
||||||
ROOM_REMOVE_USER,
|
ROOM_REMOVE_USER,
|
||||||
ALERT_REQUEST_EMAIL,
|
ALERT_EMAIL,
|
||||||
ALERT_REQUEST_PUSH,
|
ALERT_WEB,
|
||||||
|
ALERT_IOS,
|
||||||
|
ALERT_ANDROID,
|
||||||
ALERT_STATUS,
|
ALERT_STATUS,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
|
|
@ -91,6 +93,8 @@ export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
|
||||||
|
|
||||||
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
|
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
|
||||||
|
|
||||||
|
export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY
|
||||||
|
|
||||||
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
|
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
|
||||||
|
|
||||||
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
|
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
|
||||||
|
|
@ -343,7 +347,7 @@ export type Alert = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
export const alerts = deriveEventsMapped<Alert>(repository, {
|
||||||
filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH]}],
|
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||||
itemToEvent: item => item.event,
|
itemToEvent: item => item.event,
|
||||||
eventToItem: async event => {
|
eventToItem: async event => {
|
||||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export const isIntersecting = async (element: Element) =>
|
||||||
|
|
||||||
export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => {
|
export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => {
|
||||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||||
|
const elements = Array.from(document.querySelectorAll("[data-event]"))
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({behavior: "smooth", block: "center"})
|
element.scrollIntoView({behavior: "smooth", block: "center"})
|
||||||
|
|
@ -116,8 +117,8 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
|
||||||
}, 800 + 400)
|
}, 800 + 400)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else if (elements.length > 0) {
|
||||||
const lastElement = last(Array.from(document.querySelectorAll("[data-event]")))
|
const lastElement = last(elements)
|
||||||
|
|
||||||
if (lastElement && !isIntersecting(lastElement)) {
|
if (lastElement && !isIntersecting(lastElement)) {
|
||||||
lastElement.scrollIntoView({behavior: "smooth", block: "center"})
|
lastElement.scrollIntoView({behavior: "smooth", block: "center"})
|
||||||
|
|
@ -131,4 +132,6 @@ export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean>
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import * as lib from "@welshman/lib"
|
import * as lib from "@welshman/lib"
|
||||||
import * as util from "@welshman/util"
|
import * as util from "@welshman/util"
|
||||||
|
import * as feeds from "@welshman/feeds"
|
||||||
import * as router from "@welshman/router"
|
import * as router from "@welshman/router"
|
||||||
import * as welshmanSigner from "@welshman/signer"
|
import * as welshmanSigner from "@welshman/signer"
|
||||||
import * as net from "@welshman/net"
|
import * as net from "@welshman/net"
|
||||||
|
|
@ -84,6 +85,7 @@
|
||||||
...welshmanSigner,
|
...welshmanSigner,
|
||||||
...router,
|
...router,
|
||||||
...util,
|
...util,
|
||||||
|
...feeds,
|
||||||
...net,
|
...net,
|
||||||
...app,
|
...app,
|
||||||
...appState,
|
...appState,
|
||||||
|
|
@ -92,6 +94,13 @@
|
||||||
...notifications,
|
...notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Listen for navigation messages from service worker
|
||||||
|
navigator.serviceWorker?.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'NAVIGATE') {
|
||||||
|
goto(event.data.url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Nstart login
|
// Nstart login
|
||||||
if (window.location.hash?.startsWith("#nostr-login")) {
|
if (window.location.hash?.startsWith("#nostr-login")) {
|
||||||
const params = new URLSearchParams(window.location.hash.slice(1))
|
const params = new URLSearchParams(window.location.hash.slice(1))
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
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, getTagValue} from "@welshman/util"
|
||||||
|
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"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {scrollToEvent} from "@lib/html"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import {makeRoomPath, makeThreadPath} from "@app/routes"
|
import {goToEvent} from "@app/routes"
|
||||||
|
|
||||||
const {bech32} = $page.params
|
const {bech32} = $page.params
|
||||||
|
|
||||||
|
|
@ -22,19 +22,11 @@
|
||||||
let found = false
|
let found = false
|
||||||
|
|
||||||
load({
|
load({
|
||||||
relays: data.relays,
|
relays: [LOCAL_RELAY_URL, ...data.relays],
|
||||||
filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]),
|
filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]),
|
||||||
onEvent: (event: TrustedEvent) => {
|
onEvent: (event: TrustedEvent) => {
|
||||||
found = true
|
found = true
|
||||||
|
goToEvent(event, {replaceState: true})
|
||||||
if (event.kind === 9) {
|
|
||||||
goto(makeRoomPath(data.relays[0], getTagValue("h", event.tags)!), {replaceState: true})
|
|
||||||
scrollToEvent(event.id)
|
|
||||||
} else if (event.kind === 11) {
|
|
||||||
goto(makeThreadPath(data.relays[0], event.id), {replaceState: true})
|
|
||||||
} else {
|
|
||||||
goto("/", {replaceState: true})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
|
|
||||||
91
src/service-worker.js
Normal file
91
src/service-worker.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import * as nip19 from 'nostr-tools/nip19'
|
||||||
|
import {parse, renderAsText} from '@welshman/content'
|
||||||
|
import {MESSAGE, THREAD, COMMENT, EVENT_TIME} from '@welshman/util'
|
||||||
|
|
||||||
|
const renderOptions = {
|
||||||
|
createElement: tag => ({
|
||||||
|
_text: "",
|
||||||
|
set innerText(text) {
|
||||||
|
this._text = text
|
||||||
|
},
|
||||||
|
get innerHTML() {
|
||||||
|
return this._text
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
self.skipWaiting()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(self.clients.claim())
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener("push", e => {
|
||||||
|
console.log("Service Worker: Push event received", e)
|
||||||
|
|
||||||
|
let url = "/"
|
||||||
|
let body = "You have a new message"
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {event, relays = []} = e.data?.json() || {}
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
url += nip19.neventEncode({id: event.id, relays})
|
||||||
|
body = renderAsText(parse(event), renderOptions).toString()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Service Worker: Failed to parse push data", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.waitUntil(
|
||||||
|
self.registration.showNotification("New message", {
|
||||||
|
body,
|
||||||
|
data: {url},
|
||||||
|
icon: "/pwa-192x192.png",
|
||||||
|
badge: "/pwa-64x64.png",
|
||||||
|
tag: 'flotilla-notification',
|
||||||
|
requireInteraction: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", e => {
|
||||||
|
console.log("Service Worker: Notification click event", e)
|
||||||
|
|
||||||
|
e.notification.close()
|
||||||
|
|
||||||
|
if (e.action === "close") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default action or 'open' action
|
||||||
|
const url = e.notification.data?.url
|
||||||
|
|
||||||
|
e.waitUntil(
|
||||||
|
clients
|
||||||
|
.matchAll({
|
||||||
|
type: "window",
|
||||||
|
includeUncontrolled: true,
|
||||||
|
})
|
||||||
|
.then(clientList => {
|
||||||
|
// Check if app is already open and send navigation message
|
||||||
|
for (const client of clientList) {
|
||||||
|
if (client.url.includes(location.origin)) {
|
||||||
|
client.postMessage({
|
||||||
|
type: 'NAVIGATE',
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
|
||||||
|
return client.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open new window if app is not open
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(url)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue