Space alerts dialog
This commit is contained in:
+2
-9
@@ -20,7 +20,6 @@ import {
|
|||||||
ALERT_ANDROID,
|
ALERT_ANDROID,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
getAddress,
|
|
||||||
displayProfile,
|
displayProfile,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
makeList,
|
makeList,
|
||||||
@@ -62,7 +61,6 @@ import {
|
|||||||
NOTIFIER_PUBKEY,
|
NOTIFIER_PUBKEY,
|
||||||
NOTIFIER_RELAY,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
deviceAlertAddresses,
|
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -445,10 +443,5 @@ export const makeAlert = async (params: AlertParams) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishAlert = async (params: AlertParams) => {
|
export const publishAlert = async (params: AlertParams) =>
|
||||||
const event = await signer.get().sign(await makeAlert(params))
|
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||||
|
|
||||||
deviceAlertAddresses.update($addresses => [...$addresses, getAddress(event)])
|
|
||||||
|
|
||||||
return publishThunk({event, relays: [NOTIFIER_RELAY]})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<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 {ucFirst} from "@lib/util"
|
|
||||||
import {decrypt} from "@welshman/signer"
|
import {decrypt} from "@welshman/signer"
|
||||||
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
@@ -32,12 +31,12 @@
|
|||||||
import {loadAlertStatuses, requestRelayClaim} from "@app/requests"
|
import {loadAlertStatuses, requestRelayClaim} from "@app/requests"
|
||||||
import {publishAlert, attemptAuth} from "@app/commands"
|
import {publishAlert, attemptAuth} from "@app/commands"
|
||||||
import type {AlertParams} from "@app/commands"
|
import type {AlertParams} from "@app/commands"
|
||||||
import {platform, canSendPushNotifications, getPushInfo} from "@app/push"
|
import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/push"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
url?: string
|
||||||
channel?: string
|
channel?: string
|
||||||
relay?: string
|
|
||||||
notifyChat?: boolean
|
notifyChat?: boolean
|
||||||
notifyThreads?: boolean
|
notifyThreads?: boolean
|
||||||
notifyCalendar?: boolean
|
notifyCalendar?: boolean
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
relay = "",
|
url = "",
|
||||||
channel = "email",
|
channel = "email",
|
||||||
notifyChat = true,
|
notifyChat = true,
|
||||||
notifyThreads = true,
|
notifyThreads = true,
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relay) {
|
if (!url) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please select a space",
|
message: "Please select a space",
|
||||||
@@ -111,9 +110,9 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claims = claim ? {[relay]: claim} : {}
|
const claims = claim ? {[url]: claim} : {}
|
||||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
|
||||||
const description = `for ${displayList(display)} on ${displayRelayUrl(relay)}`
|
const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
|
||||||
const params: AlertParams = {feed, claims, description}
|
const params: AlertParams = {feed, claims, description}
|
||||||
|
|
||||||
if (channel === "email") {
|
if (channel === "email") {
|
||||||
@@ -133,7 +132,7 @@
|
|||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
params[platform] = await getPushInfo()
|
params[platform] = await getPushInfo()
|
||||||
params.description = `${ucFirst(platform)} push notification ${description}.`
|
params.description = `${platformName} push notification ${description}.`
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -181,11 +180,13 @@
|
|||||||
channel = "email"
|
channel = "email"
|
||||||
}
|
}
|
||||||
|
|
||||||
requestRelayClaim(relay).then(code => {
|
if (url) {
|
||||||
if (code) {
|
requestRelayClaim(url).then(code => {
|
||||||
claim = code
|
if (code) {
|
||||||
}
|
claim = code
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
<p>Space*</p>
|
<p>Space*</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<select bind:value={relay} class="select select-bordered">
|
<select bind:value={url} class="select select-bordered">
|
||||||
<option value="" disabled selected>Choose a space URL</option>
|
<option value="" disabled selected>Choose a space URL</option>
|
||||||
{#each getMembershipUrls($userMembership) as url (url)}
|
{#each getMembershipUrls($userMembership) as url (url)}
|
||||||
<option value={url}>{displayRelayUrl(url)}</option>
|
<option value={url}>{displayRelayUrl(url)}</option>
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {getTagValue} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import 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 {loadAlertStatuses, loadAlerts} from "@app/requests"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {alerts} from "@app/state"
|
import {alerts} from "@app/state"
|
||||||
|
|
||||||
const startAlert = () => pushModal(AlertAdd)
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
channel?: string
|
||||||
|
hideSpaceField?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||||
loadAlertStatuses($pubkey!)
|
|
||||||
loadAlerts($pubkey!)
|
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||||
})
|
|
||||||
|
const filteredAlerts = $derived(
|
||||||
|
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||||
@@ -29,10 +34,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
{#each $alerts as alert (alert.event.id)}
|
{#each filteredAlerts as alert (alert.event.id)}
|
||||||
<AlertItem {alert} />
|
<AlertItem {alert} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-center opacity-75 py-12">No alerts found</p>
|
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {displayRelayUrl, getTagValue, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util"
|
import {displayRelayUrl, getTagValue} from "@welshman/util"
|
||||||
import {pubkey, deriveRelay} from "@welshman/app"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
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"
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
|
import Alerts from "@app/components/Alerts.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||||
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
|
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
|
||||||
@@ -23,10 +24,9 @@
|
|||||||
memberships,
|
memberships,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
deviceAlerts,
|
|
||||||
hasNip29,
|
hasNip29,
|
||||||
|
alerts,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {loadAlerts} from "@app/requests"
|
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
@@ -65,21 +66,11 @@
|
|||||||
|
|
||||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||||
|
|
||||||
const addAlert = () => {
|
const manageAlerts = () => {
|
||||||
const alert = $deviceAlerts.find(a => getTagValue("feed", a.tags)?.includes(url))
|
const component = hasAlerts ? Alerts : AlertAdd
|
||||||
const feed = getTagValue("feed", alert?.tags || [])
|
const params = {url, channel: "push", hideSpaceField: true}
|
||||||
|
|
||||||
const props = {
|
pushModal(component, params, {replaceState})
|
||||||
relay: url,
|
|
||||||
channel: "push",
|
|
||||||
notifyChat: feed ? feed.includes(String(MESSAGE)) : true,
|
|
||||||
notifyThreads: feed ? feed.includes(String(THREAD)) : true,
|
|
||||||
notifyCalendar: feed ? feed.includes(String(EVENT_TIME)) : true,
|
|
||||||
removeDuplicates: true,
|
|
||||||
hideSpaceField: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
pushModal(AlertAdd, props, {replaceState})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
@@ -92,11 +83,10 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
replaceState = Boolean(element?.closest(".drawer"))
|
replaceState = Boolean(element?.closest(".drawer"))
|
||||||
loadAlerts($pubkey!)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={element} class="flex h-screen flex-col justify-between">
|
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||||
<SecondaryNavSection>
|
<SecondaryNavSection>
|
||||||
<div>
|
<div>
|
||||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
||||||
@@ -192,9 +182,10 @@
|
|||||||
Where did my rooms go?
|
Where did my rooms go?
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div></SecondaryNavSection>
|
</div>
|
||||||
|
</SecondaryNavSection>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<button class="btn btn-neutral btn-sm w-full" onclick={addAlert}>
|
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
|
||||||
<Icon icon="bell" />
|
<Icon icon="bell" />
|
||||||
Manage Alerts
|
Manage Alerts
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {PushNotifications} from "@capacitor/push-notifications"
|
|||||||
import {parseJson, poll} from "@welshman/lib"
|
import {parseJson, 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 {VAPID_PUBLIC_KEY} from "@app/state"
|
import {VAPID_PUBLIC_KEY} from "@app/state"
|
||||||
|
|
||||||
export const platform = Capacitor.getPlatform()
|
export const platform = Capacitor.getPlatform()
|
||||||
|
|
||||||
|
export const platformName = platform === "ios" ? "iOS" : ucFirst(platform)
|
||||||
|
|
||||||
export const initializePushNotifications = () => {
|
export const initializePushNotifications = () => {
|
||||||
if (platform === "web") return
|
if (platform === "web") return
|
||||||
|
|
||||||
|
|||||||
+19
-24
@@ -65,7 +65,6 @@ import {
|
|||||||
getTag,
|
getTag,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
getAddress,
|
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
||||||
import {Nip59, decrypt} from "@welshman/signer"
|
import {Nip59, decrypt} from "@welshman/signer"
|
||||||
@@ -349,27 +348,21 @@ export const {
|
|||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
|
||||||
export const deviceAlertAddresses = synced<string[]>("deviceAlertAddresses", [])
|
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
tags: string[][]
|
tags: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
export const alerts = withGetter(
|
||||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
deriveEventsMapped<Alert>(repository, {
|
||||||
itemToEvent: item => item.event,
|
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||||
eventToItem: async event => {
|
itemToEvent: item => item.event,
|
||||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
eventToItem: async event => {
|
||||||
|
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||||
|
|
||||||
return {event, tags}
|
return {event, tags}
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
|
||||||
export const deviceAlerts = derived(
|
|
||||||
[deviceAlertAddresses, alerts],
|
|
||||||
([$deviceAlertAddresses, $alerts]) =>
|
|
||||||
$alerts.filter(a => $deviceAlertAddresses.includes(getAddress(a.event))),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Alert Statuses
|
// Alert Statuses
|
||||||
@@ -379,15 +372,17 @@ export type AlertStatus = {
|
|||||||
tags: string[][]
|
tags: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
export const alertStatuses = withGetter(
|
||||||
filters: [{kinds: [ALERT_STATUS]}],
|
deriveEventsMapped<AlertStatus>(repository, {
|
||||||
itemToEvent: item => item.event,
|
filters: [{kinds: [ALERT_STATUS]}],
|
||||||
eventToItem: async event => {
|
itemToEvent: item => item.event,
|
||||||
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
eventToItem: async event => {
|
||||||
|
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
|
||||||
|
|
||||||
return {event, tags}
|
return {event, tags}
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const deriveAlertStatus = (address: string) =>
|
export const deriveAlertStatus = (address: string) =>
|
||||||
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
||||||
|
|||||||
Reference in New Issue
Block a user