Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalmi committed Dec 9, 2024
2 parents e221508 + 8fba7f4 commit c4931bd
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 191 deletions.
201 changes: 27 additions & 174 deletions src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,195 +139,48 @@ self.addEventListener("install", (event) => {
self.skipWaiting()
})

const enum PushType {
Mention = 1,
Reaction = 2,
Zap = 3,
Repost = 4,
DirectMessage = 5,
}

interface PushNotification {
type: PushType
data: object
}

interface CompactMention {
id: string
created_at: number
content: string
author: CompactProfile
mentions: Array<CompactProfile>
}

interface CompactReaction {
id: string
created_at: number
content: string
author: CompactProfile
event?: string
amount?: number
}

interface CompactProfile {
pubkey: string
name?: string
avatar?: string
interface PushData {
event: {
id: string
pubkey: string
created_at: number
kind: number
tags: string[][]
content: string
sig: string
}
title: string
body: string
icon: string
url: string
}

self.addEventListener("notificationclick", (event) => {
const ev = JSON.parse(event.notification.data) as PushNotification
const data = event.notification.data as PushData

event.waitUntil(
(async () => {
const windows = await self.clients.matchAll({type: "window"})
const url = () => {
if (ev.type === PushType.Zap || ev.type === PushType.Reaction) {
const mention = ev.data as CompactReaction
if (mention.event) {
return `/${nip19.noteEncode(mention.event)}`
}
} else if (ev.type === PushType.DirectMessage) {
/*
const reaction = ev.data as CompactReaction
return `/messages/${encodeTLVEntries(NostrPrefix.Chat17, {
type: TLVEntryType.Author,
value: reaction.author.pubkey,
length: 32,
})}`
*/
}
return (ev.data as {id?: string}).id
? `/${nip19.npubEncode((ev.data as {id: string}).id)}`
: "/notifications"
}
// Extract pathname from the url
const url = new URL(data.url).pathname

for (const client of windows) {
if (client.url === url() && "focus" in client) return client.focus()
if (client.url === url && "focus" in client) return client.focus()
}
if (self.clients.openWindow) return self.clients.openWindow(url())
if (self.clients.openWindow) return self.clients.openWindow(url)
})()
)
})

self.addEventListener("push", async (e) => {
console.debug(e)
const data = e.data?.json() as PushNotification | undefined
const data = e.data?.json() as PushData | undefined
console.debug(data)
if (data) {
switch (data.type) {
case PushType.Mention: {
const evx = data.data as CompactMention
await self.registration.showNotification(
`${displayNameOrDefault(evx.author)} replied`,
makeNotification(data)
)
break
}
case PushType.Reaction: {
const evx = data.data as CompactReaction
await self.registration.showNotification(
`${displayNameOrDefault(evx.author)} reacted`,
makeNotification(data)
)
break
}
case PushType.Zap: {
const evx = data.data as CompactReaction
await self.registration.showNotification(
`${displayNameOrDefault(evx.author)} zapped${evx.amount ? ` ${formatShort(evx.amount)} sats` : ""}`,
makeNotification(data)
)
break
}
case PushType.Repost: {
const evx = data.data as CompactReaction
await self.registration.showNotification(
`${displayNameOrDefault(evx.author)} reposted`,
makeNotification(data)
)
break
}
case PushType.DirectMessage: {
const evx = data.data as CompactReaction
await self.registration.showNotification(
`${displayNameOrDefault(evx.author)} sent you a DM`,
makeNotification(data)
)
break
}
}
}
})

const MentionNostrEntityRegex =
/(nostr:n(?:pub|profile|event|ote|addr)1[acdefghjklmnpqrstuvwxyz023456789]+)/g

function replaceMentions(content: string, profiles: Array<CompactProfile>) {
return content
.split(MentionNostrEntityRegex)
.map((link) => {
if (MentionNostrEntityRegex.test(link)) {
try {
const decoded = nip19.decode(link)
if (
["npub", "nprofile"].includes(decoded.type) ||
["nevent", "note"].includes(decoded.type)
) {
const px = profiles.find((a) => a.pubkey === nip19.decode(link).data)
return `@${displayNameOrDefault(px ?? {pubkey: link})}`
}
} catch (e) {
// ignore
}
}
return link
if (data) {
await self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
data: data,
})
.join("")
}

function displayNameOrDefault(p: CompactProfile) {
if ((p.name?.length ?? 0) > 0) {
return p.name
}
return nip19.npubEncode(p.pubkey).slice(0, 12)
}

function makeNotification(n: PushNotification) {
const evx = n.data as CompactMention | CompactReaction

const body = () => {
if (n.type === PushType.Mention) {
return (
"mentions" in evx ? replaceMentions(evx.content, evx.mentions) : evx.content
).substring(0, 250)
} else if (n.type === PushType.Reaction) {
if (evx.content === "+") return "💜"
if (evx.content === "-") return "👎"
return evx.content
} else if (n.type === PushType.DirectMessage) {
return ""
} else if (n.type === PushType.Repost) {
return ""
}
return evx.content.substring(0, 250)
}
const ret = {
body: body(),
icon:
evx.author.avatar ??
`https://nostr.api.v0l.io/api/v1/avatar/robots/${evx.author.pubkey}.webp`,
timestamp: evx.created_at * 1000,
tag: evx.id,
data: JSON.stringify(n),
}
console.debug(ret)
return ret
}

function formatShort(n: number) {
if (n > 1000) {
return (n / 1000).toFixed(1)
} else {
return n.toFixed(0)
}
}
})
1 change: 0 additions & 1 deletion src/shared/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const Modal = ({onClose, children, hasBackground = true}: ModalProps) => {
document.addEventListener("keydown", handleEscapeKey)

const handleMouseDown = (e: MouseEvent) => {
console.log("mousedown", e.target, modalRef.current)
if (modalRef.current && e.target === modalRef.current) {
setIsMouseDownOnBackdrop(true)
e.preventDefault()
Expand Down
23 changes: 14 additions & 9 deletions src/utils/SnortApi.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {NDKEvent, NDKFilter} from "@nostr-dev-kit/ndk"
import socialGraph from "@/utils/socialGraph"
import {NDKEvent} from "@nostr-dev-kit/ndk"
import {ndk} from "irisdb-nostr"

export const ApiHost = "https://api.snort.social"
export const ApiHost = "https://notifications.iris.to"

export interface PushNotifications {
endpoint: string
p256dh: string
auth: string
scope: string
}

/**
Expand All @@ -28,11 +27,15 @@ export default class SnortApi {
}

getPushNotificationInfo() {
return this.#getJson<{publicKey: string}>("api/v1/notifications/info")
return this.#getJson<{vapid_public_key: string}>("info")
}

registerPushNotifications(sub: PushNotifications) {
return this.#getJsonAuthd<void>("api/v1/notifications/register", "POST", sub)
registerPushNotifications(sub: PushNotifications, filter: NDKFilter) {
return this.#getJsonAuthd<void>(`subscriptions`, "POST", {
web_push_subscriptions: [sub],
webhooks: [],
filter,
})
}

async #getJsonAuthd<T>(
Expand All @@ -53,11 +56,13 @@ export default class SnortApi {
})
await event.sign()
const nostrEvent = await event.toNostrEvent()
console.log(nostrEvent, JSON.stringify(nostrEvent))

// Ensure the event is encoded correctly
const encodedEvent = btoa(JSON.stringify(nostrEvent))

return this.#getJson<T>(path, method, body, {
...headers,
authorization: `Nostr ${window.btoa(JSON.stringify(nostrEvent))}`,
authorization: `Nostr ${encodedEvent}`,
})
}

Expand All @@ -81,7 +86,7 @@ export default class SnortApi {
const text = (await rsp.text()) as string | null
if ((text?.length ?? 0) > 0) {
const obj = JSON.parse(text!)
if ("error" in obj) {
if (typeof obj === "object" && "error" in obj) {
throw new Error(obj.error, obj.code)
}
return obj as T
Expand Down
42 changes: 35 additions & 7 deletions src/utils/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {NDKTag, NDKEvent, NDKUser} from "@nostr-dev-kit/ndk"
import {getZapAmount, getZappingUser} from "./nostr"
import {SortedMap} from "./SortedMap/SortedMap"
import socialGraph from "@/utils/socialGraph"
import {profileCache} from "@/utils/memcache"
import {base64} from "@scure/base"
import SnortApi from "./SnortApi"
Expand Down Expand Up @@ -115,16 +116,43 @@ export async function subscribeToNotifications() {
const reg = await navigator.serviceWorker.ready
if (reg) {
const api = new SnortApi()
const {vapid_public_key: newVapidKey} = await api.getPushNotificationInfo()

// Check for existing subscription
const existingSub = await reg.pushManager.getSubscription()
if (existingSub) {
const existingKey = new Uint8Array(existingSub.options.applicationServerKey!)
const newKey = new Uint8Array(Buffer.from(newVapidKey, "base64"))

// Only subscribe if the keys are different
if (
existingKey.length === newKey.length &&
existingKey.every((byte, i) => byte === newKey[i])
) {
return // Already subscribed with the same key
}

await existingSub.unsubscribe()
}

const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: (await api.getPushNotificationInfo()).publicKey,
})
await api.registerPushNotifications({
endpoint: sub.endpoint,
p256dh: base64.encode(new Uint8Array(sub.getKey("p256dh")!)),
auth: base64.encode(new Uint8Array(sub.getKey("auth")!)),
scope: `${location.protocol}//${location.hostname}`,
applicationServerKey: newVapidKey,
})

const myKey = [...socialGraph().getUsersByFollowDistance(0)][0]
const filter = {
"#p": [myKey],
kinds: [1, 6, 7],
}
await api.registerPushNotifications(
{
endpoint: sub.endpoint,
p256dh: base64.encode(new Uint8Array(sub.getKey("p256dh")!)),
auth: base64.encode(new Uint8Array(sub.getKey("auth")!)),
},
filter
)
}
}
} catch (e) {
Expand Down

0 comments on commit c4931bd

Please sign in to comment.