forked from coracle/flotilla
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c737b26c0 |
+23
-12
@@ -22,7 +22,6 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/node": "^25.9.1",
|
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
@@ -69,16 +68,16 @@
|
|||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vite-pwa/sveltekit": "^1.1.0",
|
"@vite-pwa/sveltekit": "^1.1.0",
|
||||||
"@welshman/app": "^0.8.16",
|
"@welshman/app": "^0.8.15",
|
||||||
"@welshman/content": "^0.8.16",
|
"@welshman/content": "^0.8.15",
|
||||||
"@welshman/editor": "^0.8.16",
|
"@welshman/editor": "^0.8.15",
|
||||||
"@welshman/feeds": "^0.8.16",
|
"@welshman/feeds": "^0.8.15",
|
||||||
"@welshman/lib": "^0.8.16",
|
"@welshman/lib": "^0.8.15",
|
||||||
"@welshman/net": "^0.8.16",
|
"@welshman/net": "^0.8.15",
|
||||||
"@welshman/router": "^0.8.16",
|
"@welshman/router": "^0.8.15",
|
||||||
"@welshman/signer": "^0.8.16",
|
"@welshman/signer": "^0.8.15",
|
||||||
"@welshman/store": "^0.8.16",
|
"@welshman/store": "^0.8.15",
|
||||||
"@welshman/util": "^0.8.16",
|
"@welshman/util": "^0.8.15",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
@@ -99,5 +98,17 @@
|
|||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.4.0"
|
"pnpm": {
|
||||||
|
"ignoredBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"sharp",
|
||||||
|
"nostr-signer-capacitor-plugin"
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.35.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2406
-2152
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
nostr-signer-capacitor-plugin: true
|
|
||||||
cbor-extract: false
|
|
||||||
esbuild: false
|
|
||||||
sharp: true
|
|
||||||
minimumReleaseAgeExclude:
|
|
||||||
- '@welshman/app@0.8.16'
|
|
||||||
- '@welshman/content@0.8.16'
|
|
||||||
- '@welshman/editor@0.8.16'
|
|
||||||
- '@welshman/feeds@0.8.16'
|
|
||||||
- '@welshman/lib@0.8.16'
|
|
||||||
- '@welshman/net@0.8.16'
|
|
||||||
- '@welshman/router@0.8.16'
|
|
||||||
- '@welshman/signer@0.8.16'
|
|
||||||
- '@welshman/store@0.8.16'
|
|
||||||
- '@welshman/util@0.8.16'
|
|
||||||
overrides:
|
|
||||||
sharp: 0.35.0-rc.0
|
|
||||||
+17
-94
@@ -20,7 +20,7 @@ import type {TrustedEvent} from "@welshman/util"
|
|||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import {signer} from "@welshman/app"
|
||||||
import {getLivekitEndpoint} from "$lib/livekit"
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
|
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
import {
|
import {
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
currentVoiceSession,
|
currentVoiceSession,
|
||||||
@@ -157,8 +157,6 @@ const setUpMicrophone = async (
|
|||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
preferredMicId: string | undefined,
|
preferredMicId: string | undefined,
|
||||||
participant: LocalParticipant,
|
participant: LocalParticipant,
|
||||||
signal?: AbortSignal,
|
|
||||||
settleSignal?: AbortSignal,
|
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (startMuted) {
|
if (startMuted) {
|
||||||
return true
|
return true
|
||||||
@@ -170,34 +168,15 @@ const setUpMicrophone = async (
|
|||||||
capture = {deviceId: preferredMicId}
|
capture = {deviceId: preferredMicId}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await participant.setMicrophoneEnabled(true, capture)
|
||||||
participant.setMicrophoneEnabled(true, capture),
|
|
||||||
whenTimeout(15_000, {message: "Microphone access timed out.", signal: settleSignal}),
|
|
||||||
whenAborted(signal),
|
|
||||||
])
|
|
||||||
muted = false
|
muted = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Timeout or microphone rejection: join muted, the call is still usable. A
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
// genuine abort is surfaced to the caller so it can tear down the room.
|
|
||||||
if (e instanceof AbortError) throw e
|
|
||||||
if (!(e instanceof TimeoutError)) {
|
|
||||||
pushToast({theme: "error", message: "Could not access microphone"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return muted
|
return muted
|
||||||
}
|
}
|
||||||
|
|
||||||
// The room whose events are allowed to mutate shared state. Abandoned rooms
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
// (after switching calls or an engine reconnect give-up) must not clobber it.
|
|
||||||
let activeRoom: LiveKitRoom | undefined
|
|
||||||
|
|
||||||
const makeOnRoomDisconnected = (room: LiveKitRoom) => (reason?: DisconnectReason) => {
|
|
||||||
// Ignore disconnects from rooms that are no longer the active session.
|
|
||||||
if (room !== activeRoom) return
|
|
||||||
|
|
||||||
activeRoom = undefined
|
|
||||||
room.removeAllListeners()
|
|
||||||
|
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
voiceMicMuted.set(true)
|
voiceMicMuted.set(true)
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
@@ -275,6 +254,9 @@ export const joinVoiceRoom = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
cancelJoinVoiceRoom()
|
cancelJoinVoiceRoom()
|
||||||
|
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (session) await leaveVoiceRoom()
|
||||||
|
|
||||||
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
||||||
voiceState.set(VoiceState.Joining)
|
voiceState.set(VoiceState.Joining)
|
||||||
|
|
||||||
@@ -283,43 +265,14 @@ export const joinVoiceRoom = async (
|
|||||||
const signal = controller.signal
|
const signal = controller.signal
|
||||||
const isActive = () => joinAbortController === controller
|
const isActive = () => joinAbortController === controller
|
||||||
|
|
||||||
// Self-cleaning controller: aborted in finally so whenTimeout/whenAborted
|
|
||||||
// helpers clear their timers/listeners once the races below have settled.
|
|
||||||
const settle = new AbortController()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Tear down any existing session before joining. Bound it so a slow leave
|
const {server_url, participant_token} = await fetchLivekitToken(url, h, signal)
|
||||||
// (camera/screenshare renegotiation can take ~15s) cannot block this join.
|
|
||||||
if (get(currentVoiceSession)) {
|
|
||||||
await Promise.race([
|
|
||||||
leaveVoiceRoom(),
|
|
||||||
whenTimeout(15_000, {message: "Leaving previous call timed out.", signal: settle.signal}),
|
|
||||||
whenAborted(signal),
|
|
||||||
]).catch(e => {
|
|
||||||
if (e instanceof AbortError) throw e
|
|
||||||
})
|
|
||||||
|
|
||||||
// leaveVoiceRoom flips voiceState to Disconnected; re-assert Joining.
|
|
||||||
voiceState.set(VoiceState.Joining)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal.aborted) throw new AbortError()
|
|
||||||
|
|
||||||
const {server_url, participant_token} = await Promise.race([
|
|
||||||
fetchLivekitToken(url, h, signal),
|
|
||||||
whenTimeout(15_000, {
|
|
||||||
message: "Connection timed out. Please check your network and try again.",
|
|
||||||
signal: settle.signal,
|
|
||||||
}),
|
|
||||||
whenAborted(signal),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (signal.aborted) throw new AbortError()
|
if (signal.aborted) throw new AbortError()
|
||||||
|
|
||||||
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
||||||
activeRoom = liveKitRoom
|
|
||||||
|
|
||||||
liveKitRoom.on(RoomEvent.Disconnected, makeOnRoomDisconnected(liveKitRoom))
|
liveKitRoom.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||||
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||||
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||||
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||||
@@ -337,13 +290,10 @@ export const joinVoiceRoom = async (
|
|||||||
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
||||||
whenTimeout(15_000, {
|
whenTimeout(15_000, {
|
||||||
message: "Connection timed out. Please check your network and try again.",
|
message: "Connection timed out. Please check your network and try again.",
|
||||||
signal: settle.signal,
|
|
||||||
}),
|
}),
|
||||||
whenAborted(signal),
|
whenAborted(signal),
|
||||||
])
|
])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (activeRoom === liveKitRoom) activeRoom = undefined
|
|
||||||
liveKitRoom.removeAllListeners()
|
|
||||||
liveKitRoom.disconnect()
|
liveKitRoom.disconnect()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -354,24 +304,7 @@ export const joinVoiceRoom = async (
|
|||||||
syncParticipantMedia(p)
|
syncParticipantMedia(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded against timeout/abort inside setUpMicrophone: a stuck permission
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
// prompt resolves to muted rather than hanging the join forever.
|
|
||||||
const muted = await setUpMicrophone(
|
|
||||||
startMuted,
|
|
||||||
preferredMicId,
|
|
||||||
liveKitRoom.localParticipant,
|
|
||||||
signal,
|
|
||||||
settle.signal,
|
|
||||||
)
|
|
||||||
|
|
||||||
// A cancel during the mic step must tear down the connected room rather
|
|
||||||
// than leaking it.
|
|
||||||
if (signal.aborted) {
|
|
||||||
if (activeRoom === liveKitRoom) activeRoom = undefined
|
|
||||||
liveKitRoom.removeAllListeners()
|
|
||||||
liveKitRoom.disconnect()
|
|
||||||
throw new AbortError()
|
|
||||||
}
|
|
||||||
|
|
||||||
voiceMicMuted.set(muted)
|
voiceMicMuted.set(muted)
|
||||||
currentVoiceSession.set({
|
currentVoiceSession.set({
|
||||||
@@ -388,7 +321,6 @@ export const joinVoiceRoom = async (
|
|||||||
if (e instanceof AbortError) return
|
if (e instanceof AbortError) return
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
settle.abort()
|
|
||||||
if (isActive()) joinAbortController = undefined
|
if (isActive()) joinAbortController = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,23 +348,14 @@ export const leaveVoiceRoom = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always tear down this room's connection and listeners.
|
voiceState.set(VoiceState.Disconnected)
|
||||||
if (activeRoom === session.room) activeRoom = undefined
|
videoPrimaryTileKey.set(undefined)
|
||||||
session.room.removeAllListeners()
|
voiceMicMuted.set(true)
|
||||||
|
currentVoiceSession.set(undefined)
|
||||||
|
resetVideoCallLayout()
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
|
speakingParticipants.set([])
|
||||||
// Only reset shared UI state if this session is still current. A slow leave
|
participantMediaState.set(new Map())
|
||||||
// that was superseded by a new join (bounded by a timeout in joinVoiceRoom)
|
|
||||||
// must not clobber the freshly-joined session when it finally completes.
|
|
||||||
if (get(currentVoiceSession) === session) {
|
|
||||||
voiceState.set(VoiceState.Disconnected)
|
|
||||||
videoPrimaryTileKey.set(undefined)
|
|
||||||
voiceMicMuted.set(true)
|
|
||||||
currentVoiceSession.set(undefined)
|
|
||||||
resetVideoCallLayout()
|
|
||||||
speakingParticipants.set([])
|
|
||||||
participantMediaState.set(new Map())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rejoinVoiceRoom = async (): Promise<void> => {
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
import {Push} from "@app/util/notifications"
|
||||||
import {relaysMostlyRestricted, notificationSettings, parseInviteLink} from "@app/core/state"
|
import {relaysMostlyRestricted, notificationSettings, parseInviteLink} from "@app/core/state"
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
import {
|
import {
|
||||||
attemptRelayAccess,
|
attemptRelayAccess,
|
||||||
addSpaceMembership,
|
addSpaceMembership,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {Push} from "@app/util/push"
|
import {Push} from "@app/util/notifications"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ export const device = withGetter(writable(randomId()))
|
|||||||
export const notificationSettings = withGetter(
|
export const notificationSettings = withGetter(
|
||||||
writable({
|
writable({
|
||||||
push: false,
|
push: false,
|
||||||
sound: true,
|
sound: false,
|
||||||
badge: false,
|
badge: false,
|
||||||
spaces: true,
|
spaces: true,
|
||||||
mentions: true,
|
mentions: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {db, kv, ss} from "@app/core/storage"
|
import {db, kv, ss} from "@app/core/storage"
|
||||||
|
import {Push} from "@app/util/notifications"
|
||||||
import {deactivateCurrentPomadeSession} from "@app/util/pomade"
|
import {deactivateCurrentPomadeSession} from "@app/util/pomade"
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
|
|
||||||
export const logout = async () => {
|
export const logout = async () => {
|
||||||
await deactivateCurrentPomadeSession()
|
await deactivateCurrentPomadeSession()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {kv} from "@app/core/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
export {Push} from "@app/util/push"
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -44,10 +44,7 @@ export const createScroller = ({
|
|||||||
: element.closest(".scroll-container")
|
: element.closest(".scroll-container")
|
||||||
|
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
const isHidden = (el: Element) =>
|
if (container) {
|
||||||
(el as HTMLElement).offsetParent === null || el.clientHeight === 0
|
|
||||||
|
|
||||||
if (container && !isHidden(container)) {
|
|
||||||
// While we have empty space, fill it
|
// While we have empty space, fill it
|
||||||
const {scrollY, innerHeight} = window
|
const {scrollY, innerHeight} = window
|
||||||
const {scrollHeight, scrollTop, clientHeight} = container
|
const {scrollHeight, scrollTop, clientHeight} = container
|
||||||
|
|||||||
+5
-9
@@ -44,15 +44,11 @@ export const whenAborted = (signal?: AbortSignal) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns a promise that rejects with TimeoutError after ms. Use with Promise.race. */
|
||||||
* Returns a promise that rejects with TimeoutError after ms. Use with Promise.race.
|
export const whenTimeout = (ms: number, opts: {message?: string} = {}) => {
|
||||||
* Pass an optional signal to clear the timer when that signal aborts (self-cleaning).
|
return new Promise<never>((_, reject) =>
|
||||||
*/
|
setTimeout(() => reject(new TimeoutError(opts.message)), ms),
|
||||||
export const whenTimeout = (ms: number, opts: {message?: string; signal?: AbortSignal} = {}) => {
|
)
|
||||||
return new Promise<never>((_, reject) => {
|
|
||||||
const timeout = setTimeout(() => reject(new TimeoutError(opts.message)), ms)
|
|
||||||
opts.signal?.addEventListener("abort", () => clearTimeout(timeout), {once: true})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
import {theme} from "@app/util/theme"
|
import {theme} from "@app/util/theme"
|
||||||
import {toast, pushToast} from "@app/util/toast"
|
import {toast, pushToast} from "@app/util/toast"
|
||||||
import * as notifications from "@app/util/notifications"
|
import * as notifications from "@app/util/notifications"
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
import {onPushNotificationAction} from "@app/util/push/adapters/common"
|
import {onPushNotificationAction} from "@app/util/push/adapters/common"
|
||||||
import * as storage from "@app/util/storage"
|
import * as storage from "@app/util/storage"
|
||||||
import {syncKeyboard} from "@app/util/keyboard"
|
import {syncKeyboard} from "@app/util/keyboard"
|
||||||
@@ -176,7 +175,7 @@
|
|||||||
unsubscribers.push(notifications.syncChecked())
|
unsubscribers.push(notifications.syncChecked())
|
||||||
|
|
||||||
// Initialize background notifications
|
// Initialize background notifications
|
||||||
unsubscribers.push(Push.sync())
|
unsubscribers.push(notifications.Push.sync())
|
||||||
|
|
||||||
// Listen for signer errors, report to user via toast
|
// Listen for signer errors, report to user via toast
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
|
|||||||
@@ -1,51 +1,13 @@
|
|||||||
<script context="module" lang="ts">
|
|
||||||
import {synced} from "@welshman/store"
|
|
||||||
import {kv} from "@app/core/storage"
|
|
||||||
|
|
||||||
const dmNotificationsPrompted = synced({
|
|
||||||
key: "dmNotificationsPrompted",
|
|
||||||
defaultValue: false,
|
|
||||||
storage: kv,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import type {MakeNonOptional} from "@welshman/lib"
|
import type {MakeNonOptional} from "@welshman/lib"
|
||||||
import {append, uniq} from "@welshman/lib"
|
import {append, uniq} from "@welshman/lib"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Chat from "@app/components/Chat.svelte"
|
import Chat from "@app/components/Chat.svelte"
|
||||||
import {splitChatId} from "@app/core/state"
|
import {splitChatId} from "@app/core/state"
|
||||||
import {notificationSettings} from "@app/core/state"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
|
|
||||||
const {chat} = $page.params as MakeNonOptional<typeof $page.params>
|
const {chat} = $page.params as MakeNonOptional<typeof $page.params>
|
||||||
const pubkeys = uniq(append($pubkey!, splitChatId(chat)))
|
const pubkeys = uniq(append($pubkey!, splitChatId(chat)))
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!$dmNotificationsPrompted) {
|
|
||||||
dmNotificationsPrompted.set(true)
|
|
||||||
|
|
||||||
const permission = await Push.request()
|
|
||||||
|
|
||||||
if (!permission.startsWith("granted")) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: `Failed to request notification permissions (${permission}).`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationSettings.update(current => ({
|
|
||||||
...current,
|
|
||||||
push: true,
|
|
||||||
messages: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
pushToast({message: "Notifications enabled!"})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Chat {pubkeys} />
|
<Chat {pubkeys} />
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
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 {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {clearBadges} from "@app/util/notifications"
|
import {Push, clearBadges} from "@app/util/notifications"
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
import {notificationSettings} from "@app/core/state"
|
import {notificationSettings} from "@app/core/state"
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user