mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-11 11:27:03 +00:00
Move alerts to their own page, add direct message alerts
This commit is contained in:
22
package.json
22
package.json
@@ -57,17 +57,17 @@
|
|||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "^0.4.4",
|
"@welshman/app": "^0.4.6",
|
||||||
"@welshman/content": "^0.4.4",
|
"@welshman/content": "^0.4.6",
|
||||||
"@welshman/editor": "^0.4.4",
|
"@welshman/editor": "^0.4.6",
|
||||||
"@welshman/feeds": "^0.4.4",
|
"@welshman/feeds": "^0.4.6",
|
||||||
"@welshman/lib": "^0.4.4",
|
"@welshman/lib": "^0.4.6",
|
||||||
"@welshman/net": "^0.4.4",
|
"@welshman/net": "^0.4.6",
|
||||||
"@welshman/relay": "^0.4.4",
|
"@welshman/relay": "^0.4.6",
|
||||||
"@welshman/router": "^0.4.4",
|
"@welshman/router": "^0.4.6",
|
||||||
"@welshman/signer": "^0.4.4",
|
"@welshman/signer": "^0.4.6",
|
||||||
"@welshman/store": "^0.4.4",
|
"@welshman/store": "^0.4.6",
|
||||||
"@welshman/util": "^0.4.4",
|
"@welshman/util": "^0.4.6",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
|
|||||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
@@ -1,17 +1,8 @@
|
|||||||
<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 {decrypt} from "@welshman/signer"
|
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||||
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||||
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, signer, waitForThunkError} from "@welshman/app"
|
import {pubkey, signer, waitForThunkError} from "@welshman/app"
|
||||||
@@ -23,17 +14,10 @@
|
|||||||
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 {
|
import {alerts, getMembershipUrls, userMembership} from "@app/core/state"
|
||||||
alerts,
|
import {requestRelayClaim} from "@app/core/requests"
|
||||||
getMembershipUrls,
|
import {createAlert} from "@app/core/commands"
|
||||||
userMembership,
|
import {canSendPushNotifications} from "@app/util/push"
|
||||||
NOTIFIER_PUBKEY,
|
|
||||||
NOTIFIER_RELAY,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {loadAlertStatuses, requestRelayClaim} from "@app/core/requests"
|
|
||||||
import {publishAlert, attemptAuth} from "@app/core/commands"
|
|
||||||
import type {AlertParams} from "@app/core/commands"
|
|
||||||
import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/util/push"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -112,66 +96,20 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const claim = url ? await requestRelayClaim(url) : undefined
|
const claim = url ? await requestRelayClaim(url) : undefined
|
||||||
const claims = claim ? {[url]: claim} : {}
|
|
||||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
|
|
||||||
const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
|
|
||||||
const params: AlertParams = {feed, claims, description}
|
|
||||||
|
|
||||||
if (channel === "email") {
|
const {error} = await createAlert({
|
||||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
feed: makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url)),
|
||||||
|
claims: claim ? {[url]: claim} : {},
|
||||||
params.description = `${cadence} alert ${description}, sent via email.`
|
description: `for ${displayList(display)} on ${displayRelayUrl(url)}`,
|
||||||
params.email = {
|
email: channel === "email" ? {cron, email} : undefined,
|
||||||
cron,
|
})
|
||||||
email,
|
|
||||||
handler: [
|
|
||||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
|
||||||
"wss://relay.nostr.band/",
|
|
||||||
"web",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
params[platform] = await getPushInfo()
|
|
||||||
params.description = `${platformName} push notification ${description}.`
|
|
||||||
} catch (e: any) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: String(e),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't do this we'll get an event rejection
|
|
||||||
await attemptAuth(NOTIFIER_RELAY)
|
|
||||||
|
|
||||||
const thunk = await publishAlert(params)
|
|
||||||
const error = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({
|
pushToast({theme: "error", message: error})
|
||||||
theme: "error",
|
} else {
|
||||||
message: `Failed to send your alert to the notification server (${error}).`,
|
pushToast({message: "Your alert has been successfully created!"})
|
||||||
})
|
back()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -189,6 +127,9 @@
|
|||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
Add an Alert
|
Add an Alert
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Enable notifications to keep up to date on activity you care about.
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#if canSendPushNotifications()}
|
{#if canSendPushNotifications()}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import type {Alert} from "@app/core/state"
|
import type {Alert} from "@app/core/state"
|
||||||
import {NOTIFIER_RELAY, NOTIFIER_PUBKEY} from "@app/core/state"
|
import {deleteAlert} from "@app/core/commands"
|
||||||
import {publishDelete} from "@app/core/commands"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,10 +11,7 @@
|
|||||||
const {alert}: Props = $props()
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
const relays = [NOTIFIER_RELAY]
|
deleteAlert(alert)
|
||||||
const tags = [["p", NOTIFIER_PUBKEY]]
|
|
||||||
|
|
||||||
publishDelete({event: alert.event, relays, tags, protect: false})
|
|
||||||
pushToast({message: "Your alert has been deleted!"})
|
pushToast({message: "Your alert has been deleted!"})
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
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 AlertStatus from "@app/components/AlertStatus.svelte"
|
||||||
import type {Alert} from "@app/core/state"
|
import type {Alert} from "@app/core/state"
|
||||||
import {deriveAlertStatus} from "@app/core/state"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
const {alert}: Props = $props()
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
const status = deriveAlertStatus(getAddress(alert.event))
|
|
||||||
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,32 +38,5 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<div class="flex-inline gap-1">{description}</div>
|
<div class="flex-inline gap-1">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $status}
|
<AlertStatus {alert} />
|
||||||
{@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}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
42
src/app/components/AlertStatus.svelte
Normal file
42
src/app/components/AlertStatus.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<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,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getTagValue} from "@welshman/util"
|
import {sleep} from "@welshman/lib"
|
||||||
|
import {getTagValue, getAddress} from "@welshman/util"
|
||||||
|
import {isRelayFeed, findFeed} from "@welshman/feeds"
|
||||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -7,7 +9,9 @@
|
|||||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
import AlertItem from "@app/components/AlertItem.svelte"
|
import AlertItem from "@app/components/AlertItem.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {alerts} from "@app/core/state"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {alerts, dmAlert, deriveAlertStatus, userInboxRelays, getAlertFeed} from "@app/core/state"
|
||||||
|
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
@@ -17,29 +21,92 @@
|
|||||||
|
|
||||||
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||||
|
|
||||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
|
||||||
|
|
||||||
const filteredAlerts = $derived(
|
const filteredAlerts = $derived(
|
||||||
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
|
$alerts.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
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||||
|
|
||||||
|
const uncheckDmAlert = async (message: string) => {
|
||||||
|
await sleep(100)
|
||||||
|
|
||||||
|
toggle.checked = false
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggle = async () => {
|
||||||
|
if ($dmAlert) {
|
||||||
|
deleteAlert($dmAlert)
|
||||||
|
} else {
|
||||||
|
if ($userInboxRelays.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!"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toggle: HTMLInputElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
<div class="col-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||||
<strong class="flex items-center gap-3">
|
<div class="flex items-center justify-between">
|
||||||
<Icon icon={Inbox} />
|
<strong class="flex items-center gap-3">
|
||||||
Alerts
|
<Icon icon={Inbox} />
|
||||||
</strong>
|
Alerts
|
||||||
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
</strong>
|
||||||
<Icon icon={AddCircle} />
|
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||||
Add Alert
|
<Icon icon={AddCircle} />
|
||||||
</Button>
|
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>
|
||||||
<div class="col-4">
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
||||||
{#each filteredAlerts as alert (alert.event.id)}
|
<div class="flex justify-between">
|
||||||
<AlertItem {alert} />
|
<p>Notify me about new direct messages</p>
|
||||||
{:else}
|
<input
|
||||||
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
type="checkbox"
|
||||||
{/each}
|
class="toggle toggle-primary"
|
||||||
|
bind:this={toggle}
|
||||||
|
checked={Boolean($dmAlert)}
|
||||||
|
oninput={onToggle} />
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {WRAP} from "@welshman/util"
|
|
||||||
import {repository} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
@@ -10,7 +8,8 @@
|
|||||||
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 {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/core/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
|
import {enableGiftWraps} from "@app/core/commands"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
const {next} = $props()
|
const {next} = $props()
|
||||||
@@ -20,12 +19,7 @@
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
||||||
const enableChat = async () => {
|
const enableChat = async () => {
|
||||||
canDecrypt.set(true)
|
enableGiftWraps()
|
||||||
|
|
||||||
for (const event of repository.query([{kinds: [WRAP]}])) {
|
|
||||||
ensureUnwrapped(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearModals()
|
clearModals()
|
||||||
goto(nextUrl)
|
goto(nextUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {waitForThunkCompletion} from "@welshman/app"
|
||||||
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
||||||
import Check from "@assets/icons/check.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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ChatStart from "@app/components/ChatStart.svelte"
|
import ChatStart from "@app/components/ChatStart.svelte"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {dmAlert, userInboxRelays} from "@app/core/state"
|
||||||
|
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||||
|
|
||||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
|
|
||||||
@@ -13,6 +20,40 @@
|
|||||||
setChecked("/chat/*")
|
setChecked("/chat/*")
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enableAlerts = async () => {
|
||||||
|
if ($userInboxRelays.length === 0) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please set up your messaging relays before enabling alerts.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
@@ -24,4 +65,19 @@
|
|||||||
<Icon size={5} icon={Check} />
|
<Icon size={5} icon={Check} />
|
||||||
Mark all read
|
Mark all read
|
||||||
</Button>
|
</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>
|
||||||
|
</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>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
|
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||||
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
|
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -29,6 +31,32 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link replaceState href="/settings/alerts">
|
||||||
|
<CardButton>
|
||||||
|
{#snippet icon()}
|
||||||
|
<div><Icon icon={Bell} size={7} /></div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Alerts</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Set up email digests and push notifications</div>
|
||||||
|
{/snippet}
|
||||||
|
</CardButton>
|
||||||
|
</Link>
|
||||||
|
<Link replaceState href="/settings/wallet">
|
||||||
|
<CardButton>
|
||||||
|
{#snippet icon()}
|
||||||
|
<div><Icon icon={Wallet} size={7} /></div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Wallet</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Connect a bitcoin wallet for sending social tips</div>
|
||||||
|
{/snippet}
|
||||||
|
</CardButton>
|
||||||
|
</Link>
|
||||||
<Link replaceState href="/settings/relays">
|
<Link replaceState href="/settings/relays">
|
||||||
<CardButton>
|
<CardButton>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
@@ -42,7 +70,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link replaceState href="/settings">
|
<Link replaceState href="/settings/content">
|
||||||
<CardButton>
|
<CardButton>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<div><Icon icon={Settings} size={7} /></div>
|
<div><Icon icon={Settings} size={7} /></div>
|
||||||
|
|||||||
@@ -7,9 +7,8 @@
|
|||||||
DELETE,
|
DELETE,
|
||||||
isReplaceable,
|
isReplaceable,
|
||||||
getAddress,
|
getAddress,
|
||||||
getRelaysFromList,
|
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {pubkey, userRelaySelections, publishThunk, repository} from "@welshman/app"
|
import {pubkey, publishThunk, repository} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
@@ -20,7 +19,13 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {logout} from "@app/core/commands"
|
import {logout} from "@app/core/commands"
|
||||||
import {INDEXER_RELAYS, PLATFORM_NAME, userMembership, getMembershipUrls} from "@app/core/state"
|
import {
|
||||||
|
INDEXER_RELAYS,
|
||||||
|
PLATFORM_NAME,
|
||||||
|
userMembership,
|
||||||
|
getMembershipUrls,
|
||||||
|
userWriteRelays,
|
||||||
|
} from "@app/core/state"
|
||||||
|
|
||||||
let progress: number | undefined = $state(undefined)
|
let progress: number | undefined = $state(undefined)
|
||||||
let confirmText = $state("")
|
let confirmText = $state("")
|
||||||
@@ -43,7 +48,7 @@
|
|||||||
const denominator = chunks.length + 2
|
const denominator = chunks.length + 2
|
||||||
const relays = uniq([
|
const relays = uniq([
|
||||||
...INDEXER_RELAYS,
|
...INDEXER_RELAYS,
|
||||||
...getRelaysFromList($userRelaySelections),
|
...$userWriteRelays,
|
||||||
...getMembershipUrls($userMembership),
|
...getMembershipUrls($userMembership),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
class="underline transition-all"
|
class="underline transition-all"
|
||||||
class:link={isSending}
|
class:link={isSending}
|
||||||
class:opacity-25={!isSending}
|
class:opacity-25={!isSending}
|
||||||
|
class:pointer-events-none={!isSending}
|
||||||
onclick={stopPropagation(abort)}>
|
onclick={stopPropagation(abort)}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {nwc} from "@getalby/sdk"
|
import {nwc} from "@getalby/sdk"
|
||||||
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 type {Override, MakeOptional} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
randomId,
|
randomId,
|
||||||
append,
|
append,
|
||||||
@@ -11,10 +12,15 @@ import {
|
|||||||
equals,
|
equals,
|
||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
LOCALE,
|
LOCALE,
|
||||||
|
parseJson,
|
||||||
|
fromPairs,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
|
import {decrypt} from "@welshman/signer"
|
||||||
import type {Feed} from "@welshman/feeds"
|
import type {Feed} from "@welshman/feeds"
|
||||||
|
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} from "@welshman/feeds"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
|
WRAP,
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
@@ -44,6 +50,8 @@ import {
|
|||||||
toNostrURI,
|
toNostrURI,
|
||||||
getRelaysFromList,
|
getRelaysFromList,
|
||||||
RelayMode,
|
RelayMode,
|
||||||
|
getAddress,
|
||||||
|
getTagValue,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
@@ -54,7 +62,6 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
profilesByPubkey,
|
profilesByPubkey,
|
||||||
relaySelectionsByPubkey,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
userRelaySelections,
|
userRelaySelections,
|
||||||
@@ -66,8 +73,9 @@ import {
|
|||||||
tagEventForComment,
|
tagEventForComment,
|
||||||
tagEventForQuote,
|
tagEventForQuote,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
|
getPubkeyRelays,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {SettingsValues} from "@app/core/state"
|
import type {SettingsValues, Alert} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
PROTECTED,
|
PROTECTED,
|
||||||
@@ -77,14 +85,18 @@ import {
|
|||||||
NOTIFIER_RELAY,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
|
canDecrypt,
|
||||||
|
ensureUnwrapped,
|
||||||
|
userInboxRelays,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
import {loadAlertStatuses} from "@app/core/requests"
|
||||||
|
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {preferencesStorageProvider} from "@src/lib/storage"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
export const getPubkeyHints = (pubkey: string) => {
|
export const getPubkeyHints = (pubkey: string) => {
|
||||||
const selections = relaySelectionsByPubkey.get().get(pubkey)
|
const relays = getPubkeyRelays(pubkey, RelayMode.Write)
|
||||||
const relays = selections ? getRelaysFromList(selections, RelayMode.Write) : []
|
|
||||||
const hints = relays.length ? relays : INDEXER_RELAYS
|
const hints = relays.length ? relays : INDEXER_RELAYS
|
||||||
|
|
||||||
return hints
|
return hints
|
||||||
@@ -417,27 +429,35 @@ export const publishComment = ({relays, ...params}: CommentParams & {relays: str
|
|||||||
|
|
||||||
// Alerts
|
// 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 = {
|
export type AlertParams = {
|
||||||
feed: Feed
|
feed: Feed
|
||||||
description: string
|
description: string
|
||||||
claims: Record<string, string>
|
claims?: Record<string, string>
|
||||||
email?: {
|
email?: AlertParamsEmail
|
||||||
cron: string
|
web?: AlertParamsWeb
|
||||||
email: string
|
ios?: AlertParamsIos
|
||||||
handler: string[]
|
android?: AlertParamsAndroid
|
||||||
}
|
|
||||||
web?: {
|
|
||||||
endpoint: string
|
|
||||||
p256dh: string
|
|
||||||
auth: string
|
|
||||||
}
|
|
||||||
ios?: {
|
|
||||||
device_token: string
|
|
||||||
bundle_identifier: string
|
|
||||||
}
|
|
||||||
android?: {
|
|
||||||
device_token: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeAlert = async (params: AlertParams) => {
|
export const makeAlert = async (params: AlertParams) => {
|
||||||
@@ -448,7 +468,7 @@ export const makeAlert = async (params: AlertParams) => {
|
|||||||
["description", params.description],
|
["description", params.description],
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const [relay, claim] of Object.entries(params.claims)) {
|
for (const [relay, claim] of Object.entries(params.claims || [])) {
|
||||||
tags.push(["claim", relay, claim])
|
tags.push(["claim", relay, claim])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,6 +501,88 @@ 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]})
|
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 attemptAuth(NOTIFIER_RELAY)
|
||||||
|
|
||||||
|
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 (!get(canDecrypt)) {
|
||||||
|
enableGiftWraps()
|
||||||
|
}
|
||||||
|
|
||||||
|
return createAlert({
|
||||||
|
description: `for direct messages.`,
|
||||||
|
feed: makeIntersectionFeed(
|
||||||
|
feedFromFilters([{kinds: [WRAP], "#p": [pubkey.get()!]}]),
|
||||||
|
makeRelayFeed(...get(userInboxRelays)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
||||||
@@ -532,3 +634,13 @@ export const payInvoice = async (invoice: string) => {
|
|||||||
.then(() => getWebLn().sendPayment(invoice))
|
.then(() => getWebLn().sendPayment(invoice))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gift Wraps
|
||||||
|
|
||||||
|
export const enableGiftWraps = () => {
|
||||||
|
canDecrypt.set(true)
|
||||||
|
|
||||||
|
for (const event of repository.query([{kinds: [WRAP]}])) {
|
||||||
|
ensureUnwrapped(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
identity,
|
identity,
|
||||||
groupBy,
|
groupBy,
|
||||||
always,
|
always,
|
||||||
|
tryCatch,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Socket} from "@welshman/net"
|
import type {Socket} from "@welshman/net"
|
||||||
import {Pool, load, AuthStateEvent, SocketEvent, netContext} from "@welshman/net"
|
import {Pool, load, AuthStateEvent, SocketEvent, netContext} from "@welshman/net"
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
withGetter,
|
withGetter,
|
||||||
synced,
|
synced,
|
||||||
} from "@welshman/store"
|
} from "@welshman/store"
|
||||||
|
import {isKindFeed, findFeed} from "@welshman/feeds"
|
||||||
import {
|
import {
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
WRAP,
|
WRAP,
|
||||||
@@ -70,6 +72,8 @@ import {
|
|||||||
getTagValues,
|
getTagValues,
|
||||||
verifyEvent,
|
verifyEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
|
RelayMode,
|
||||||
|
getRelaysFromList,
|
||||||
} 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"
|
||||||
@@ -94,6 +98,8 @@ import {
|
|||||||
appContext,
|
appContext,
|
||||||
getThunkError,
|
getThunkError,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
|
userRelaySelections,
|
||||||
|
userInboxRelaySelections,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk, Relay} from "@welshman/app"
|
import type {Thunk, Relay} from "@welshman/app"
|
||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {preferencesStorageProvider} from "@src/lib/storage"
|
||||||
@@ -375,6 +381,18 @@ export const relaysPendingTrust = writable<string[]>([])
|
|||||||
|
|
||||||
export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
||||||
|
|
||||||
|
// Relay selections
|
||||||
|
|
||||||
|
export const userReadRelays = derived(userRelaySelections, $l =>
|
||||||
|
getRelaysFromList($l, RelayMode.Read),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const userWriteRelays = derived(userRelaySelections, $l =>
|
||||||
|
getRelaysFromList($l, RelayMode.Write),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const userInboxRelays = derived(userInboxRelaySelections, $l => getRelaysFromList($l))
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
@@ -394,6 +412,17 @@ export const alerts = withGetter(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const getAlertFeed = (alert: Alert) =>
|
||||||
|
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
||||||
|
|
||||||
|
export const dmAlert = derived(alerts, $alerts =>
|
||||||
|
$alerts.find(alert => {
|
||||||
|
const feed = getAlertFeed(alert)
|
||||||
|
|
||||||
|
return findFeed(feed, f => isKindFeed(f) && f.includes(WRAP))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Alert Statuses
|
// Alert Statuses
|
||||||
|
|
||||||
export type AlertStatus = {
|
export type AlertStatus = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 type {ActionPerformed, RegistrationError, Token} from "@capacitor/push-notifications"
|
||||||
import {PushNotifications} from "@capacitor/push-notifications"
|
import {PushNotifications} from "@capacitor/push-notifications"
|
||||||
import {parseJson, poll} from "@welshman/lib"
|
import {parseJson, sleep, poll} from "@welshman/lib"
|
||||||
import {isSignedEvent} from "@welshman/util"
|
import {isSignedEvent} from "@welshman/util"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {ucFirst} from "@lib/util"
|
import {ucFirst} from "@lib/util"
|
||||||
@@ -50,6 +50,8 @@ export const getWebPushInfo = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const registration = await navigator.serviceWorker.ready
|
const registration = await navigator.serviceWorker.ready
|
||||||
|
|
||||||
|
// This will hang on firefox in development builds, but works in production
|
||||||
let subscription = await registration.pushManager.getSubscription()
|
let subscription = await registration.pushManager.getSubscription()
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
@@ -118,14 +120,19 @@ export const getCapacitorPushInfo = async () => {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPushInfo = (): Promise<Record<string, string>> => {
|
export const getPushInfo = (): Promise<Record<string, string>> =>
|
||||||
switch (platform) {
|
new Promise((resolve, reject) => {
|
||||||
case "web":
|
sleep(3000).then(() => reject("Failed to request notification permissions"))
|
||||||
return getWebPushInfo()
|
|
||||||
case "ios":
|
switch (platform) {
|
||||||
case "android":
|
case "web":
|
||||||
return getCapacitorPushInfo()
|
getWebPushInfo().then(resolve, reject)
|
||||||
default:
|
break
|
||||||
throw new Error(`Invalid push platform: ${platform}`)
|
case "ios":
|
||||||
}
|
case "android":
|
||||||
}
|
getCapacitorPushInfo().then(resolve, reject)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
reject(`Invalid push platform: ${platform}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import {
|
|||||||
THREAD,
|
THREAD,
|
||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
|
getPubkeyTagValues,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
|
ensureUnwrapped,
|
||||||
makeChatId,
|
makeChatId,
|
||||||
entityLink,
|
entityLink,
|
||||||
decodeRelay,
|
decodeRelay,
|
||||||
@@ -74,24 +76,28 @@ export const getPrimaryNavItemIndex = ($page: Page) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const goToEvent = async (event: TrustedEvent, options: Record<string, any> = {}) => {
|
export const goToEvent = async (event: TrustedEvent, options: Record<string, any> = {}) => {
|
||||||
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) {
|
const unwrapped = await ensureUnwrapped(event)
|
||||||
await scrollToEvent(event.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const urls = Array.from(tracker.getRelays(event.id))
|
if (unwrapped) {
|
||||||
const path = await getEventPath(event, urls)
|
const urls = Array.from(tracker.getRelays(unwrapped.id))
|
||||||
|
const path = await getEventPath(unwrapped, urls)
|
||||||
|
|
||||||
if (path.includes("://")) {
|
if (path.includes("://")) {
|
||||||
window.open(path)
|
window.open(path)
|
||||||
} else {
|
} else {
|
||||||
goto(path, options)
|
goto(path, options)
|
||||||
|
|
||||||
await sleep(300)
|
await sleep(300)
|
||||||
await scrollToEvent(event.id)
|
await scrollToEvent(unwrapped.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
||||||
|
if (event.kind === DIRECT_MESSAGE || event.kind === DIRECT_MESSAGE_FILE) {
|
||||||
|
return makeChatPath([event.pubkey, ...getPubkeyTagValues(event.tags)])
|
||||||
|
}
|
||||||
|
|
||||||
const room = getTagValue(ROOM, event.tags)
|
const room = getTagValue(ROOM, event.tags)
|
||||||
|
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
WRAP,
|
WRAP,
|
||||||
|
ALERT_STATUS,
|
||||||
|
ALERT_EMAIL,
|
||||||
|
ALERT_WEB,
|
||||||
|
ALERT_IOS,
|
||||||
|
ALERT_ANDROID,
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
THREAD,
|
THREAD,
|
||||||
@@ -39,7 +44,6 @@
|
|||||||
RELAYS,
|
RELAYS,
|
||||||
BLOSSOM_SERVERS,
|
BLOSSOM_SERVERS,
|
||||||
ROOMS,
|
ROOMS,
|
||||||
getRelaysFromList,
|
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||||
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
||||||
@@ -67,7 +71,6 @@
|
|||||||
signerLog,
|
signerLog,
|
||||||
dropSession,
|
dropSession,
|
||||||
defaultStorageAdapters,
|
defaultStorageAdapters,
|
||||||
userInboxRelaySelections,
|
|
||||||
loginWithNip01,
|
loginWithNip01,
|
||||||
loginWithNip46,
|
loginWithNip46,
|
||||||
EventsStorageAdapter,
|
EventsStorageAdapter,
|
||||||
@@ -96,6 +99,7 @@
|
|||||||
canDecrypt,
|
canDecrypt,
|
||||||
getSetting,
|
getSetting,
|
||||||
relaysMostlyRestricted,
|
relaysMostlyRestricted,
|
||||||
|
userInboxRelays,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {loadUserData, listenForNotifications} from "@app/core/requests"
|
import {loadUserData, listenForNotifications} from "@app/core/requests"
|
||||||
import {theme} from "@app/util/theme"
|
import {theme} from "@app/util/theme"
|
||||||
@@ -290,6 +294,11 @@
|
|||||||
INBOX_RELAYS,
|
INBOX_RELAYS,
|
||||||
ROOMS,
|
ROOMS,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
|
ALERT_STATUS,
|
||||||
|
ALERT_EMAIL,
|
||||||
|
ALERT_WEB,
|
||||||
|
ALERT_IOS,
|
||||||
|
ALERT_ANDROID,
|
||||||
].includes(e.kind)
|
].includes(e.kind)
|
||||||
) {
|
) {
|
||||||
return 1
|
return 1
|
||||||
@@ -437,19 +446,19 @@
|
|||||||
// Listen for chats, populate chat-based notifications
|
// Listen for chats, populate chat-based notifications
|
||||||
let controller: AbortController
|
let controller: AbortController
|
||||||
|
|
||||||
derived([pubkey, canDecrypt, userInboxRelaySelections], identity).subscribe(
|
derived([pubkey, canDecrypt, userInboxRelays], identity).subscribe(
|
||||||
([$pubkey, $canDecrypt, $userInboxRelaySelections]) => {
|
([$pubkey, $canDecrypt, $userInboxRelays]) => {
|
||||||
controller?.abort()
|
controller?.abort()
|
||||||
controller = new AbortController()
|
controller = new AbortController()
|
||||||
|
|
||||||
if ($pubkey && $canDecrypt) {
|
if ($pubkey && $canDecrypt) {
|
||||||
request({
|
request({
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
relays: $userInboxRelays,
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
|
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
|
||||||
{kinds: [WRAP], "#p": [$pubkey], limit: 100},
|
{kinds: [WRAP], "#p": [$pubkey], limit: 100},
|
||||||
],
|
],
|
||||||
relays: getRelaysFromList($userInboxRelaySelections),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onDestroy} from "svelte"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import Chat from "@app/components/Chat.svelte"
|
import Chat from "@app/components/Chat.svelte"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {notifications, setChecked} from "@app/util/notifications"
|
||||||
|
|
||||||
onDestroy(() => {
|
// We have to watch this one, since on mobile the badge will be visible when active
|
||||||
setChecked($page.url.pathname)
|
$effect(() => {
|
||||||
|
if ($notifications.has($page.url.pathname)) {
|
||||||
|
setChecked($page.url.pathname)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
import Moon from "@assets/icons/moon.svg?dataurl"
|
import Moon from "@assets/icons/moon.svg?dataurl"
|
||||||
import InfoSquare from "@assets/icons/info-square.svg?dataurl"
|
import InfoSquare from "@assets/icons/info-square.svg?dataurl"
|
||||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||||
|
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
||||||
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Page from "@lib/components/Page.svelte"
|
import Page from "@lib/components/Page.svelte"
|
||||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||||
@@ -30,37 +32,45 @@
|
|||||||
|
|
||||||
<SecondaryNav>
|
<SecondaryNav>
|
||||||
<SecondaryNavSection>
|
<SecondaryNavSection>
|
||||||
|
<SecondaryNavItem class="w-full !justify-between">
|
||||||
|
<strong class="ellipsize flex items-center gap-3"> Your Settings </strong>
|
||||||
|
</SecondaryNavItem>
|
||||||
<div in:fly|local>
|
<div in:fly|local>
|
||||||
<SecondaryNavItem href="/settings/profile">
|
<SecondaryNavItem href="/settings/profile">
|
||||||
<Icon icon={UserCircle} /> Profile
|
<Icon icon={UserCircle} /> Profile
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local>
|
<div in:fly|local={{delay: 50}}>
|
||||||
|
<SecondaryNavItem href="/settings/alerts">
|
||||||
|
<Icon icon={Bell} /> Alerts
|
||||||
|
</SecondaryNavItem>
|
||||||
|
</div>
|
||||||
|
<div in:fly|local={{delay: 100}}>
|
||||||
<SecondaryNavItem href="/settings/wallet">
|
<SecondaryNavItem href="/settings/wallet">
|
||||||
<Icon icon={Wallet} /> Wallet
|
<Icon icon={Wallet} /> Wallet
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local={{delay: 50}}>
|
<div in:fly|local={{delay: 150}}>
|
||||||
<SecondaryNavItem href="/settings/relays">
|
<SecondaryNavItem href="/settings/relays">
|
||||||
<Icon icon={Server} /> Relays
|
<Icon icon={Server} /> Relays
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local={{delay: 100}}>
|
<div in:fly|local={{delay: 200}}>
|
||||||
<SecondaryNavItem href="/settings">
|
<SecondaryNavItem href="/settings/content">
|
||||||
<Icon icon={Settings} /> Settings
|
<Icon icon={GalleryMinimalistic} /> Content
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local={{delay: 150}}>
|
<div in:fly|local={{delay: 250}}>
|
||||||
<SecondaryNavItem onclick={toggleTheme}>
|
<SecondaryNavItem onclick={toggleTheme}>
|
||||||
<Icon icon={Moon} /> Theme
|
<Icon icon={Moon} /> Theme
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local={{delay: 200}}>
|
<div in:fly|local={{delay: 300}}>
|
||||||
<SecondaryNavItem href="/settings/about">
|
<SecondaryNavItem href="/settings/about">
|
||||||
<Icon icon={InfoSquare} /> About
|
<Icon icon={InfoSquare} /> About
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<div in:fly|local={{delay: 250}}>
|
<div in:fly|local={{delay: 350}}>
|
||||||
<SecondaryNavItem class="text-error hover:text-error" onclick={logout}>
|
<SecondaryNavItem class="text-error hover:text-error" onclick={logout}>
|
||||||
<Icon icon={Exit} /> Log Out
|
<Icon icon={Exit} /> Log Out
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
|
|||||||
7
src/routes/settings/alerts/+page.svelte
Normal file
7
src/routes/settings/alerts/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Alerts from "@app/components/Alerts.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content column">
|
||||||
|
<Alerts />
|
||||||
|
</div>
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||||
import SignerStatus from "@app/components/SignerStatus.svelte"
|
import SignerStatus from "@app/components/SignerStatus.svelte"
|
||||||
import InfoKeys from "@app/components/InfoKeys.svelte"
|
import InfoKeys from "@app/components/InfoKeys.svelte"
|
||||||
import Alerts from "@app/components/Alerts.svelte"
|
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {clip} from "@app/util/toast"
|
import {clip} from "@app/util/toast"
|
||||||
@@ -141,7 +140,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<SignerStatus />
|
<SignerStatus />
|
||||||
</div>
|
</div>
|
||||||
<Alerts />
|
|
||||||
<div class="card2 bg-alt shadow-xl">
|
<div class="card2 bg-alt shadow-xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="flex items-center gap-3">
|
<strong class="flex items-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user