forked from coracle/flotilla
Remove old alerts
This commit is contained in:
@@ -14,8 +14,6 @@ VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
||||
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||
VITE_NOTIFIER_RELAY=anchor.coracle.social
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {randomInt, map, displayList, identity, TIMEZONE} from "@welshman/lib"
|
||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {alertsById, userSpaceUrls} from "@app/core/state"
|
||||
import {requestRelayClaim} from "@app/core/requests"
|
||||
import {createAlert} from "@app/core/commands"
|
||||
import {canSendPushNotifications} from "@app/util/push"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
channel?: string
|
||||
notifyChat?: boolean
|
||||
notifyThreads?: boolean
|
||||
notifyCalendar?: boolean
|
||||
hideSpaceField?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
url = "",
|
||||
channel = "email",
|
||||
notifyChat = true,
|
||||
notifyThreads = true,
|
||||
notifyCalendar = true,
|
||||
hideSpaceField = false,
|
||||
}: Props = $props()
|
||||
|
||||
const timezoneOffset = parseInt(TIMEZONE.split(":")?.[0] || "00")
|
||||
const minute = randomInt(0, 59)
|
||||
const hour = (17 - timezoneOffset) % 24
|
||||
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||
const DAILY = `0 ${minute} ${hour} * * *`
|
||||
|
||||
let loading = $state(false)
|
||||
let cron = $state(WEEKLY)
|
||||
let email = $state(
|
||||
map(a => getTagValue("email", a.tags), $alertsById.values()).filter(identity)[0] || "",
|
||||
)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = async () => {
|
||||
if (channel === "email" && !email.includes("@")) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide an email address",
|
||||
})
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please select a space",
|
||||
})
|
||||
}
|
||||
|
||||
if (!notifyThreads && !notifyCalendar && !notifyChat) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please select something to be notified about",
|
||||
})
|
||||
}
|
||||
|
||||
const filters: Filter[] = []
|
||||
const display: string[] = []
|
||||
|
||||
if (notifyThreads) {
|
||||
display.push("threads")
|
||||
filters.push({kinds: [THREAD]})
|
||||
filters.push({kinds: [COMMENT], "#k": [String(THREAD)]})
|
||||
}
|
||||
|
||||
if (notifyCalendar) {
|
||||
display.push("calendar events")
|
||||
filters.push({kinds: [EVENT_TIME]})
|
||||
filters.push({kinds: [COMMENT], "#k": [String(EVENT_TIME)]})
|
||||
}
|
||||
|
||||
if (notifyChat) {
|
||||
display.push("chat")
|
||||
filters.push({kinds: [MESSAGE]})
|
||||
}
|
||||
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const claim = url ? await requestRelayClaim(url) : undefined
|
||||
|
||||
const {error} = await createAlert({
|
||||
feed: makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url)),
|
||||
claims: claim ? {[url]: claim} : {},
|
||||
description: `for ${displayList(display)} on ${displayRelayUrl(url)}`,
|
||||
email: channel === "email" ? {cron, email} : undefined,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
back()
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!canSendPushNotifications()) {
|
||||
channel = "email"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
Add an Alert
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
Enable notifications to keep up to date on activity you care about.
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if canSendPushNotifications()}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Alert Type*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={channel} class="select select-bordered">
|
||||
<option value="email">Email Digest</option>
|
||||
<option value="push">Push Notification</option>
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
{#if channel === "email"}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email Address*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input placeholder="email@example.com" bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Frequency*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={cron} class="select select-bordered">
|
||||
<option value={WEEKLY}>Weekly</option>
|
||||
<option value={DAILY}>Daily</option>
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
{#if !hideSpaceField}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Space*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={url} class="select select-bordered">
|
||||
<option value="" disabled selected>Choose a space URL</option>
|
||||
{#each $userSpaceUrls as url (url)}
|
||||
<option value={url}>{displayRelayUrl(url)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Notifications*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||
Threads
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||
Calendar
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||
Chat
|
||||
</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Confirm</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {deleteAlert} from "@app/core/commands"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const confirm = () => {
|
||||
deleteAlert(alert)
|
||||
pushToast({message: "Your alert has been deleted!"})
|
||||
history.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm {confirm} message="You'll no longer receive messages for this alert." />
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {parseJson} from "@welshman/lib"
|
||||
import {displayFeeds} from "@welshman/feeds"
|
||||
import {getTagValue, getTagValues} from "@welshman/util"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||
import AlertStatus from "@app/components/AlertStatus.svelte"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const cron = $derived(getTagValue("cron", alert.tags))
|
||||
const channel = $derived(getTagValue("channel", alert.tags))
|
||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||
const description = $derived(
|
||||
getTagValue("description", alert.tags) ||
|
||||
[
|
||||
`${cron?.endsWith("1") ? "Weekly" : "Daily"} alert for events`,
|
||||
displayFeeds(feeds.map(parseJson)),
|
||||
`sent via ${channel}.`,
|
||||
].join(" "),
|
||||
)
|
||||
|
||||
const startDelete = () => pushModal(AlertDelete, {alert})
|
||||
</script>
|
||||
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<Button class="py-1" onclick={startDelete}>
|
||||
<Icon icon={TrashBin2} />
|
||||
</Button>
|
||||
<div class="flex-inline gap-1">{description}</div>
|
||||
</div>
|
||||
<AlertStatus {alert} />
|
||||
</div>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {getAddress, getTagValue} from "@welshman/util"
|
||||
import type {Alert} from "@app/core/state"
|
||||
import {deriveAlertStatus} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const status = deriveAlertStatus(getAddress(alert.event))
|
||||
</script>
|
||||
|
||||
{#if $status}
|
||||
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||
{#if statusText === "ok"}
|
||||
<span
|
||||
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)}>
|
||||
Active
|
||||
</span>
|
||||
{:else if statusText === "pending"}
|
||||
<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"
|
||||
data-tip={getTagValue("message", $status.tags)}>
|
||||
Pending
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
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)}>
|
||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip="The notification server did not respond to your request.">
|
||||
Inactive
|
||||
</span>
|
||||
{/if}
|
||||
@@ -1,155 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {sleep, filter} from "@welshman/lib"
|
||||
import {getTagValue, getAddress, RelayMode} from "@welshman/util"
|
||||
import {isRelayFeed, findFeed} from "@welshman/feeds"
|
||||
import {getPubkeyRelays, pubkey} from "@welshman/app"
|
||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import AlertItem from "@app/components/AlertItem.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {
|
||||
dmAlert,
|
||||
alertsById,
|
||||
deriveAlertStatus,
|
||||
getAlertFeed,
|
||||
userSettingsValues,
|
||||
} from "@app/core/state"
|
||||
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||
import {clearBadges} from "../util/notifications"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
channel?: string
|
||||
hideSpaceField?: boolean
|
||||
}
|
||||
|
||||
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||
|
||||
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
|
||||
|
||||
const filteredAlerts = $derived(
|
||||
filter(alert => {
|
||||
const feed = getAlertFeed(alert)
|
||||
|
||||
// Skip non-feeds and DM alerts
|
||||
if (!feed || alert === $dmAlert) return false
|
||||
|
||||
// If we have a space url, only match feeds for this space
|
||||
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
|
||||
|
||||
return true
|
||||
}, $alertsById.values()),
|
||||
)
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||
|
||||
const uncheckDmAlert = async (message: string) => {
|
||||
await sleep(100)
|
||||
|
||||
directMessagesNotificationToggle.checked = false
|
||||
pushToast({theme: "error", message})
|
||||
}
|
||||
|
||||
const onDirectMessagesNotificationToggle = async () => {
|
||||
if ($dmAlert) {
|
||||
deleteAlert($dmAlert)
|
||||
} else {
|
||||
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
|
||||
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
|
||||
}
|
||||
|
||||
const {error} = await createDmAlert()
|
||||
|
||||
if (error) {
|
||||
return uncheckDmAlert(error)
|
||||
}
|
||||
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
}
|
||||
}
|
||||
|
||||
const onShowBadgeOnUnreadToggle = async () => {
|
||||
$userSettingsValues.show_notifications_badge = !$userSettingsValues.show_notifications_badge
|
||||
|
||||
if (!$userSettingsValues.show_notifications_badge) {
|
||||
await clearBadges()
|
||||
}
|
||||
}
|
||||
|
||||
const onDirectMessagesNotificationSoundToggle = async () => {
|
||||
$userSettingsValues.play_notification_sound = !$userSettingsValues.play_notification_sound
|
||||
}
|
||||
|
||||
let directMessagesNotificationToggle: HTMLInputElement
|
||||
</script>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Inbox} />
|
||||
Alerts
|
||||
</strong>
|
||||
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Alert
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#each filteredAlerts as alert (alert.event.id)}
|
||||
<AlertItem {alert} />
|
||||
{:else}
|
||||
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Bell} />
|
||||
Notifications
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Notify me about new direct messages</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:this={directMessagesNotificationToggle}
|
||||
checked={Boolean($dmAlert)}
|
||||
oninput={onDirectMessagesNotificationToggle} />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Show badge for unread direct messages</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={Boolean($userSettingsValues.show_notifications_badge)}
|
||||
oninput={onShowBadgeOnUnreadToggle} />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Play sound for new direct messages</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={Boolean($userSettingsValues.play_notification_sound)}
|
||||
oninput={onDirectMessagesNotificationSoundToggle} />
|
||||
</div>
|
||||
{#if $dmStatus}
|
||||
{@const status = getTagValue("status", $dmStatus.tags) || "error"}
|
||||
{#if status !== "ok"}
|
||||
<div class="alert alert-error border border-solid border-error bg-transparent text-error">
|
||||
<p>
|
||||
{getTagValue("message", $dmStatus.tags) ||
|
||||
"The notification server did not respond to your request."}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,19 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {RelayMode} from "@welshman/util"
|
||||
import {waitForThunkCompletion, getPubkeyRelays, pubkey} from "@welshman/app"
|
||||
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
||||
import Check from "@assets/icons/check.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ChatStart from "@app/components/ChatStart.svelte"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {dmAlert} from "@app/core/state"
|
||||
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||
import {userSettingsValues} from "@app/core/state"
|
||||
import {publishSettings} from "@app/core/commands"
|
||||
|
||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||
|
||||
@@ -22,39 +18,9 @@
|
||||
history.back()
|
||||
}
|
||||
|
||||
const enableAlerts = async () => {
|
||||
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please set up your messaging relays before enabling alerts.",
|
||||
})
|
||||
}
|
||||
const enableAlerts = () => publishSettings({...$userSettingsValues, alerts_messages: true})
|
||||
|
||||
enablingAlert = true
|
||||
|
||||
try {
|
||||
const {error} = await createDmAlert()
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
} finally {
|
||||
enablingAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
const disableAlerts = async () => {
|
||||
disablingAlert = true
|
||||
|
||||
try {
|
||||
await waitForThunkCompletion(deleteAlert($dmAlert!))
|
||||
} finally {
|
||||
disablingAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
let enablingAlert = $state(false)
|
||||
let disablingAlert = $state(false)
|
||||
const disableAlerts = () => publishSettings({...$userSettingsValues, alerts_messages: false})
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
@@ -66,19 +32,15 @@
|
||||
<Icon size={5} icon={Check} />
|
||||
Mark all read
|
||||
</Button>
|
||||
{#if (!enablingAlert && $dmAlert) || disablingAlert}
|
||||
<Button class="btn btn-neutral" onclick={disableAlerts} disabled={disablingAlert}>
|
||||
{#if !disablingAlert}
|
||||
<Icon size={4} icon={BellOff} />
|
||||
{/if}
|
||||
<Spinner loading={disablingAlert}>Disable alerts</Spinner>
|
||||
{#if $userSettingsValues.alerts_messages}
|
||||
<Button class="btn btn-neutral" onclick={disableAlerts}>
|
||||
<Icon size={4} icon={BellOff} />
|
||||
Disable alerts
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral" onclick={enableAlerts} disabled={enablingAlert}>
|
||||
{#if !enablingAlert}
|
||||
<Icon size={4} icon={Bell} />
|
||||
{/if}
|
||||
<Spinner loading={enablingAlert}>Enable alerts</Spinner>
|
||||
<Button class="btn btn-neutral" onclick={enableAlerts}>
|
||||
<Icon size={4} icon={Bell} />
|
||||
Enable alerts
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
let notificationCount = $state($notifications.size)
|
||||
|
||||
const playSound = () => {
|
||||
if (enabled && $userSettingsValues.play_notification_sound) {
|
||||
if (enabled && $userSettingsValues.alerts_sound) {
|
||||
audioElement?.play()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {some} from "@welshman/lib"
|
||||
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
|
||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
|
||||
import {deriveRelay, pubkey} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
@@ -34,8 +33,6 @@
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
||||
import SpaceReports from "@app/components/SpaceReports.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import Alerts from "@app/components/Alerts.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||
@@ -47,7 +44,6 @@
|
||||
deriveOtherRooms,
|
||||
userSpaceUrls,
|
||||
hasNip29,
|
||||
alertsById,
|
||||
deriveUserCanCreateRoom,
|
||||
deriveUserIsSpaceAdmin,
|
||||
deriveEventsForUrl,
|
||||
@@ -68,9 +64,6 @@
|
||||
const members = deriveSpaceMembers(url)
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
||||
const hasAlerts = $derived(
|
||||
some(a => getTagValue("feed", a.tags)?.includes(url), $alertsById.values()),
|
||||
)
|
||||
|
||||
const spaceKinds = derived(
|
||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||
@@ -101,13 +94,6 @@
|
||||
|
||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||
|
||||
const manageAlerts = () => {
|
||||
const component = hasAlerts ? Alerts : AlertAdd
|
||||
const params = {url, channel: "push", hideSpaceField: true}
|
||||
|
||||
pushModal(component, params, {replaceState})
|
||||
}
|
||||
|
||||
let showMenu = $state(false)
|
||||
let replaceState = $state(false)
|
||||
let element: Element | undefined = $state()
|
||||
@@ -258,9 +244,9 @@
|
||||
<Button class="btn btn-neutral btn-sm" onclick={showDetail}>
|
||||
<SocketStatusIndicator {url} />
|
||||
</Button>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={manageAlerts}>
|
||||
<Link href="/settings/alerts" class="btn btn-neutral btn-sm">
|
||||
<Icon icon={Bell} />
|
||||
Manage Alerts
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-180
@@ -1,33 +1,24 @@
|
||||
import {nwc} from "@getalby/sdk"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {get, derived} from "svelte/store"
|
||||
import type {Override, MakeOptional} from "@welshman/lib"
|
||||
import {
|
||||
first,
|
||||
sha256,
|
||||
randomId,
|
||||
append,
|
||||
remove,
|
||||
flatten,
|
||||
poll,
|
||||
uniq,
|
||||
equals,
|
||||
TIMEZONE,
|
||||
LOCALE,
|
||||
parseJson,
|
||||
fromPairs,
|
||||
last,
|
||||
simpleCache,
|
||||
normalizeUrl,
|
||||
nthNe,
|
||||
} from "@welshman/lib"
|
||||
import {decrypt, Nip01Signer} from "@welshman/signer"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import type {UploadTask} from "@welshman/editor"
|
||||
import type {Feed} from "@welshman/feeds"
|
||||
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} from "@welshman/feeds"
|
||||
import type {TrustedEvent, EventContent, Profile} from "@welshman/util"
|
||||
import {
|
||||
WRAP,
|
||||
DELETE,
|
||||
REPORT,
|
||||
PROFILE,
|
||||
@@ -39,10 +30,6 @@ import {
|
||||
RELAY_LEAVE,
|
||||
ROOMS,
|
||||
COMMENT,
|
||||
ALERT_EMAIL,
|
||||
ALERT_WEB,
|
||||
ALERT_IOS,
|
||||
ALERT_ANDROID,
|
||||
APP_DATA,
|
||||
isSignedEvent,
|
||||
makeEvent,
|
||||
@@ -55,8 +42,6 @@ import {
|
||||
getRelayTagValues,
|
||||
toNostrURI,
|
||||
RelayMode,
|
||||
getAddress,
|
||||
getTagValue,
|
||||
getTagValues,
|
||||
uploadBlob,
|
||||
canUploadBlob,
|
||||
@@ -85,18 +70,15 @@ import {
|
||||
waitForThunkError,
|
||||
getPubkeyRelays,
|
||||
userBlossomServerList,
|
||||
shouldUnwrap,
|
||||
getThunkError,
|
||||
} from "@welshman/app"
|
||||
import {compressFile} from "@lib/html"
|
||||
import {kv, db} from "@app/core/storage"
|
||||
import type {SettingsValues, Alert} from "@app/core/state"
|
||||
import type {SettingsValues} from "@app/core/state"
|
||||
import {
|
||||
SETTINGS,
|
||||
PROTECTED,
|
||||
INDEXER_RELAYS,
|
||||
NOTIFIER_PUBKEY,
|
||||
NOTIFIER_RELAY,
|
||||
DEFAULT_BLOSSOM_SERVERS,
|
||||
userSpaceUrls,
|
||||
userSettingsValues,
|
||||
@@ -107,8 +89,6 @@ import {
|
||||
relaysMostlyRestricted,
|
||||
deriveSocket,
|
||||
} from "@app/core/state"
|
||||
import {loadAlertStatuses} from "@app/core/requests"
|
||||
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -373,164 +353,6 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||
publishThunk({event: makeComment(params), relays})
|
||||
|
||||
// Alerts
|
||||
|
||||
export type AlertParamsEmail = {
|
||||
cron: string
|
||||
email: string
|
||||
handler: string[]
|
||||
}
|
||||
|
||||
export type AlertParamsWeb = {
|
||||
endpoint: string
|
||||
p256dh: string
|
||||
auth: string
|
||||
}
|
||||
|
||||
export type AlertParamsIos = {
|
||||
device_token: string
|
||||
bundle_identifier: string
|
||||
}
|
||||
|
||||
export type AlertParamsAndroid = {
|
||||
device_token: string
|
||||
}
|
||||
|
||||
export type AlertParams = {
|
||||
feed: Feed
|
||||
description: string
|
||||
claims?: Record<string, string>
|
||||
email?: AlertParamsEmail
|
||||
web?: AlertParamsWeb
|
||||
ios?: AlertParamsIos
|
||||
android?: AlertParamsAndroid
|
||||
}
|
||||
|
||||
export const makeAlert = async (params: AlertParams) => {
|
||||
const tags = [
|
||||
["feed", JSON.stringify(params.feed)],
|
||||
["locale", LOCALE],
|
||||
["timezone", TIMEZONE],
|
||||
["description", params.description],
|
||||
]
|
||||
|
||||
for (const [relay, claim] of Object.entries(params.claims || [])) {
|
||||
tags.push(["claim", relay, claim])
|
||||
}
|
||||
|
||||
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)),
|
||||
tags: [
|
||||
["d", randomId()],
|
||||
["p", NOTIFIER_PUBKEY],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const publishAlert = async (params: AlertParams) =>
|
||||
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||
|
||||
export const deleteAlert = (alert: Alert) => {
|
||||
const relays = [NOTIFIER_RELAY]
|
||||
const tags = [["p", NOTIFIER_PUBKEY]]
|
||||
|
||||
return publishDelete({event: alert.event, relays, tags, protect: false})
|
||||
}
|
||||
|
||||
export type CreateAlertParams = Override<
|
||||
AlertParams,
|
||||
{
|
||||
email?: MakeOptional<AlertParamsEmail, "handler">
|
||||
}
|
||||
>
|
||||
|
||||
export type CreateAlertResult = {
|
||||
ok?: true
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const createAlert = async (params: CreateAlertParams): Promise<CreateAlertResult> => {
|
||||
if (params.email) {
|
||||
const cadence = params.email.cron.endsWith("1") ? "Weekly" : "Daily"
|
||||
const handler = [
|
||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||
"wss://relay.nostr.band/",
|
||||
"web",
|
||||
]
|
||||
|
||||
params.email = {handler, ...params.email}
|
||||
params.description = `${cadence} alert ${params.description}, sent via email.`
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
params[platform] = await getPushInfo()
|
||||
params.description = `${platformName} push notification ${params.description}.`
|
||||
} catch (e: any) {
|
||||
return {error: String(e)}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't do this we'll get an event rejection
|
||||
await Pool.get().get(NOTIFIER_RELAY).auth.attemptAuth(sign)
|
||||
|
||||
const thunk = await publishAlert(params as AlertParams)
|
||||
const error = await waitForThunkError(thunk)
|
||||
|
||||
if (error) {
|
||||
return {error}
|
||||
}
|
||||
|
||||
// Fetch our new status to make sure it's active
|
||||
const $pubkey = pubkey.get()!
|
||||
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 {error: message}
|
||||
}
|
||||
|
||||
return {ok: true}
|
||||
}
|
||||
|
||||
export const createDmAlert = async () => {
|
||||
if (!shouldUnwrap.get()) {
|
||||
shouldUnwrap.set(true)
|
||||
}
|
||||
|
||||
const $pubkey = pubkey.get()!
|
||||
|
||||
return createAlert({
|
||||
description: `for direct messages.`,
|
||||
feed: makeIntersectionFeed(
|
||||
feedFromFilters([{kinds: [WRAP], "#p": [$pubkey]}]),
|
||||
makeRelayFeed(...getPubkeyRelays($pubkey, RelayMode.Messaging)),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Settings
|
||||
|
||||
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
||||
|
||||
+10
-70
@@ -51,13 +51,7 @@ import {
|
||||
deriveEventsByIdForUrl,
|
||||
getEventsByIdForUrl,
|
||||
} from "@welshman/store"
|
||||
import {isKindFeed, findFeed} from "@welshman/feeds"
|
||||
import {
|
||||
ALERT_ANDROID,
|
||||
ALERT_EMAIL,
|
||||
ALERT_IOS,
|
||||
ALERT_STATUS,
|
||||
ALERT_WEB,
|
||||
APP_DATA,
|
||||
CLIENT_AUTH,
|
||||
COMMENT,
|
||||
@@ -94,7 +88,6 @@ import {
|
||||
getListTags,
|
||||
getPubkeyTagValues,
|
||||
getRelayTagValues,
|
||||
getTagValue,
|
||||
getTagValues,
|
||||
isRelayUrl,
|
||||
normalizeRelayUrl,
|
||||
@@ -105,7 +98,6 @@ import {
|
||||
ManagementMethod,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, RelayProfile, PublishedRoomMeta, List, Filter} from "@welshman/util"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {routerContext, Router} from "@welshman/router"
|
||||
import {
|
||||
pubkey,
|
||||
@@ -114,7 +106,6 @@ import {
|
||||
createSearch,
|
||||
userFollowList,
|
||||
ensurePlaintext,
|
||||
signer,
|
||||
makeOutboxLoader,
|
||||
appContext,
|
||||
deriveRelay,
|
||||
@@ -283,8 +274,11 @@ export type SettingsValues = {
|
||||
relay_auth: RelayAuthMode
|
||||
send_delay: number
|
||||
font_size: number
|
||||
play_notification_sound: boolean
|
||||
show_notifications_badge: boolean
|
||||
alerts_spaces: boolean
|
||||
alerts_mentions: boolean
|
||||
alerts_messages: boolean
|
||||
alerts_sound: boolean
|
||||
alerts_badge: boolean
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
@@ -301,8 +295,11 @@ export const defaultSettings: SettingsValues = {
|
||||
relay_auth: RelayAuthMode.Conservative,
|
||||
send_delay: 0,
|
||||
font_size: 1.1,
|
||||
play_notification_sound: true,
|
||||
show_notifications_badge: true,
|
||||
alerts_spaces: true,
|
||||
alerts_mentions: true,
|
||||
alerts_messages: true,
|
||||
alerts_sound: true,
|
||||
alerts_badge: true,
|
||||
}
|
||||
|
||||
export const settingsByPubkey = deriveItemsByKey({
|
||||
@@ -341,63 +338,6 @@ export const relaysPendingTrust = writable<string[]>([])
|
||||
|
||||
export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
||||
|
||||
// Alerts
|
||||
|
||||
export type Alert = {
|
||||
event: TrustedEvent
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alertsById = deriveItemsByKey<Alert>({
|
||||
repository,
|
||||
getKey: alert => alert.event.id,
|
||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const getAlertFeed = (alert: Alert) =>
|
||||
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
||||
|
||||
export const dmAlert = derived(alertsById, $alertsById => {
|
||||
for (const alert of $alertsById.values()) {
|
||||
if (findFeed(getAlertFeed(alert), f => isKindFeed(f) && f.includes(WRAP))) {
|
||||
return alert
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Alert Statuses
|
||||
|
||||
export type AlertStatus = {
|
||||
event: TrustedEvent
|
||||
tags: string[][]
|
||||
}
|
||||
|
||||
export const alertStatusesByAddress = deriveItemsByKey<AlertStatus>({
|
||||
repository,
|
||||
filters: [{kinds: [ALERT_STATUS]}],
|
||||
getKey: alertStatus => getTagValue("d", alertStatus.event.tags)!,
|
||||
eventToItem: async event => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if ($signer) {
|
||||
const tags = parseJson(await decrypt($signer, NOTIFIER_PUBKEY, event.content))
|
||||
|
||||
return {event, tags}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const deriveAlertStatus = makeDeriveItem(alertStatusesByAddress)
|
||||
|
||||
// Chats
|
||||
|
||||
export type Chat = {
|
||||
|
||||
@@ -208,7 +208,7 @@ export const badgeCount = derived(notifications, notifications => {
|
||||
})
|
||||
|
||||
export const handleBadgeCountChanges = async (count: number) => {
|
||||
if (get(userSettingsValues).show_notifications_badge) {
|
||||
if (get(userSettingsValues).alerts_badge) {
|
||||
try {
|
||||
await Badge.set({count})
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
||||
import {PushNotifications} from "@capacitor/push-notifications"
|
||||
import {parseJson, sleep, poll} from "@welshman/lib"
|
||||
import {isSignedEvent} from "@welshman/util"
|
||||
import {goto} from "$app/navigation"
|
||||
import {ucFirst} from "@lib/util"
|
||||
import {VAPID_PUBLIC_KEY} from "@app/core/state"
|
||||
|
||||
export const platform = Capacitor.getPlatform()
|
||||
|
||||
export const platformName = platform === "ios" ? "iOS" : ucFirst(platform)
|
||||
|
||||
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 getWebPushInfo = async () => {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
throw new Error("Service Worker not supported")
|
||||
}
|
||||
|
||||
if (!("PushManager" in window)) {
|
||||
throw new Error("Push notifications are 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
|
||||
|
||||
// This will hang on firefox in development builds, but works in production
|
||||
let subscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (!subscription) {
|
||||
subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: VAPID_PUBLIC_KEY,
|
||||
})
|
||||
}
|
||||
|
||||
const {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 type PushInfo = {
|
||||
device_token: string
|
||||
bundle_identifier?: string
|
||||
}
|
||||
|
||||
export const getCapacitorPushInfo = async () => {
|
||||
let status = await PushNotifications.checkPermissions()
|
||||
|
||||
if (status.receive === "prompt") {
|
||||
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 poll({
|
||||
condition: () => Boolean(device_token),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!device_token) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
const info: PushInfo = {device_token}
|
||||
|
||||
if (platform === "ios") {
|
||||
info.bundle_identifier = "social.flotilla"
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
export const getPushInfo = (): Promise<Record<string, string>> =>
|
||||
new Promise((resolve, reject) => {
|
||||
sleep(3000).then(() => reject("Failed to request notification permissions"))
|
||||
|
||||
switch (platform) {
|
||||
case "web":
|
||||
getWebPushInfo().then(resolve, reject)
|
||||
break
|
||||
case "ios":
|
||||
case "android":
|
||||
getCapacitorPushInfo().then(resolve, reject)
|
||||
break
|
||||
default:
|
||||
reject(`Invalid push platform: ${platform}`)
|
||||
}
|
||||
})
|
||||
@@ -34,7 +34,6 @@
|
||||
import * as appState from "@app/core/state"
|
||||
import {theme} from "@app/util/theme"
|
||||
import {toast, pushToast} from "@app/util/toast"
|
||||
import {initializePushNotifications} from "@app/util/push"
|
||||
import * as notifications from "@app/util/notifications"
|
||||
import * as storage from "@app/util/storage"
|
||||
import {syncKeyboard} from "@app/util/keyboard"
|
||||
@@ -64,9 +63,6 @@
|
||||
...notifications,
|
||||
})
|
||||
|
||||
// Initialize push notification handler asap
|
||||
initializePushNotifications()
|
||||
|
||||
// Listen for navigation messages from service worker
|
||||
navigator.serviceWorker?.addEventListener("message", event => {
|
||||
if (event.data && event.data.type === "NAVIGATE") {
|
||||
|
||||
@@ -1,7 +1,75 @@
|
||||
<script lang="ts">
|
||||
import Alerts from "@app/components/Alerts.svelte"
|
||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {clearBadges} from "@app/util/notifications"
|
||||
import {userSettingsValues} from "@app/core/state"
|
||||
import {publishSettings} from "@app/core/commands"
|
||||
|
||||
const reset = () => {
|
||||
settings = {...$userSettingsValues}
|
||||
}
|
||||
|
||||
const onsubmit = preventDefault(async () => {
|
||||
await publishSettings($state.snapshot(settings))
|
||||
|
||||
pushToast({message: "Your settings have been saved!"})
|
||||
})
|
||||
|
||||
let settings = $state({...$userSettingsValues})
|
||||
|
||||
$effect(() => {
|
||||
if (!$userSettingsValues.alerts_badge) {
|
||||
clearBadges()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="content column">
|
||||
<Alerts />
|
||||
</div>
|
||||
<form class="content column gap-4" {onsubmit}>
|
||||
<div class="card2 bg-alt col-4 shadow-md">
|
||||
<strong class="text-lg">Alert Settings</strong>
|
||||
<div class="flex justify-between">
|
||||
<p>Show badge for unread alerts</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.alerts_badge} />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Play sound for new messages</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.alerts_sound} />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Inbox} />
|
||||
Space Activity
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Notify me about new messages</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.alerts_spaces} />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Always notify me when mentioned</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" checked={settings.alerts_mentions} />
|
||||
</div>
|
||||
<!-- todo: add list of muted spaces -->
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Bell} />
|
||||
Direct Messages
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<p>Notify me about new messages</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={settings.alerts_messages} />
|
||||
</div>
|
||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||
<Button class="btn btn-neutral" onclick={reset}>Discard Changes</Button>
|
||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PLATFORM_NAME, RelayAuthMode, userSettingsValues} from "@app/core/state"
|
||||
@@ -24,54 +23,33 @@
|
||||
</script>
|
||||
|
||||
<form class="content column gap-4" {onsubmit}>
|
||||
<div class="card2 bg-alt col-4 shadow-md">
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<strong class="text-lg">Privacy Settings</strong>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Authenticate with unknown relays?</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
onchange={onAuthModeChange}
|
||||
checked={settings.auth_mode === RelayAuthMode.Aggressive} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Report errors?</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={settings.report_errors} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Allow {PLATFORM_NAME} to send error reports to help improve the app.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Report usage?</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_usage} />
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Allow {PLATFORM_NAME} to collect anonymous usage data.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<p>Authenticate with unknown relays?</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
onchange={onAuthModeChange}
|
||||
checked={settings.auth_mode === RelayAuthMode.Aggressive} />
|
||||
<p class="col-span-2 text-sm opacity-70">
|
||||
Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<p>Report errors?</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_errors} />
|
||||
<p class="col-span-2 text-sm opacity-70">
|
||||
Allow {PLATFORM_NAME} to send error reports to help improve the app.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<p>Report usage?</p>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_usage} />
|
||||
<p class="col-span-2 text-sm opacity-70">
|
||||
Allow {PLATFORM_NAME} to collect anonymous usage data.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||
<Button class="btn btn-neutral" onclick={reset}>Discard Changes</Button>
|
||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/* global clients */
|
||||
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
|
||||
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 title = "New activity"
|
||||
let body = "You have a new message"
|
||||
|
||||
try {
|
||||
const data = e.data?.json()
|
||||
|
||||
if (data?.event) {
|
||||
url += nip19.neventEncode({
|
||||
id: data.event.id,
|
||||
relays: data.relays || [],
|
||||
})
|
||||
}
|
||||
|
||||
if (data?.title) {
|
||||
title = data.title
|
||||
}
|
||||
|
||||
if (data?.body) {
|
||||
body = data.body
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Service Worker: Failed to parse push data", e)
|
||||
}
|
||||
|
||||
e.waitUntil(
|
||||
self.registration.showNotification(title, {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user