Require braces on conditionals
This commit is contained in:
parent
ef6069e7a4
commit
da6a63870a
88 changed files with 717 additions and 240 deletions
|
|
@ -86,7 +86,9 @@ export const boot = async (
|
||||||
await shell.waitFor({state: "attached", timeout: ms(15)})
|
await shell.waitFor({state: "attached", timeout: ms(15)})
|
||||||
break
|
break
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (attempt === 3) throw e
|
if (attempt === 3) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
await page.reload()
|
await page.reload()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ export const watchFaults = (): FaultWatch => {
|
||||||
const record = (line: string, fault: boolean) => {
|
const record = (line: string, fault: boolean) => {
|
||||||
log.push(line)
|
log.push(line)
|
||||||
|
|
||||||
if (fault) found.push(line)
|
if (fault) {
|
||||||
|
found.push(line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,9 @@ export const test = base.extend<HarnessFixtures, HarnessWorkerFixtures>({
|
||||||
let scenario: Maybe<Scenario>
|
let scenario: Maybe<Scenario>
|
||||||
|
|
||||||
const requireScenario = () => {
|
const requireScenario = () => {
|
||||||
if (scenario) return scenario
|
if (scenario) {
|
||||||
|
return scenario
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error("Seed a scenario before opening a page for a user")
|
throw new Error("Seed a scenario before opening a page for a user")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ export const makeContextStore = <T>(installer: string) => {
|
||||||
get: (context: BrowserContext) => {
|
get: (context: BrowserContext) => {
|
||||||
const value = byContext.get(context)
|
const value = byContext.get(context)
|
||||||
|
|
||||||
if (value) return value
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`${installer} was never called for this browser context`)
|
throw new Error(`${installer} was never called for this browser context`)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ export const makePublisher = ({name, enqueue, startedAt, sign}: PublisherOptions
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (value) return value
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`An event seeded into "${name}" was read before seed() published it`)
|
throw new Error(`An event seeded into "${name}" was read before seed() published it`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,9 @@ export const seed = async (
|
||||||
const getSpace = (name: SpaceName) => {
|
const getSpace = (name: SpaceName) => {
|
||||||
const space = spaces.get(name)
|
const space = spaces.get(name)
|
||||||
|
|
||||||
if (space) return space
|
if (space) {
|
||||||
|
return space
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`No space named "${name}" was seeded`)
|
throw new Error(`No space named "${name}" was seeded`)
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +125,9 @@ export const seed = async (
|
||||||
const getOpenRelay = (name: OpenRelayName) => {
|
const getOpenRelay = (name: OpenRelayName) => {
|
||||||
const relay = opened.find(candidate => candidate.name === name)
|
const relay = opened.find(candidate => candidate.name === name)
|
||||||
|
|
||||||
if (relay) return relay
|
if (relay) {
|
||||||
|
return relay
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`No open relay named "${name}" was seeded`)
|
throw new Error(`No open relay named "${name}" was seeded`)
|
||||||
}
|
}
|
||||||
|
|
@ -133,8 +137,12 @@ export const seed = async (
|
||||||
const roomList = roomLists.get(user.pubkey)
|
const roomList = roomLists.get(user.pubkey)
|
||||||
const relayList = relayLists.get(user.pubkey)
|
const relayList = relayLists.get(user.pubkey)
|
||||||
|
|
||||||
if (roomList) events.push(roomList)
|
if (roomList) {
|
||||||
if (relayList) events.push(relayList)
|
events.push(roomList)
|
||||||
|
}
|
||||||
|
if (relayList) {
|
||||||
|
events.push(relayList)
|
||||||
|
}
|
||||||
|
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ export const seedSpace = ({zooid, enqueue, startedAt, name}: SeedSpaceOptions):
|
||||||
let testRelay: Maybe<TestRelay>
|
let testRelay: Maybe<TestRelay>
|
||||||
|
|
||||||
const relay = () => {
|
const relay = () => {
|
||||||
if (testRelay) return testRelay
|
if (testRelay) {
|
||||||
|
return testRelay
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Space "${name}" has not been seeded yet, await seed() first`)
|
throw new Error(`Space "${name}" has not been seeded yet, await seed() first`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,9 @@ const describeClockDrift = (drift: number) => {
|
||||||
const getTestUser = (pubkey: string) => {
|
const getTestUser = (pubkey: string) => {
|
||||||
const user = testUsersByPubkey.get(pubkey)
|
const user = testUsersByPubkey.get(pubkey)
|
||||||
|
|
||||||
if (user) return user
|
if (user) {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot publish as ${pubkey}: zooid authenticates every write, so seeded events must be ` +
|
`Cannot publish as ${pubkey}: zooid authenticates every write, so seeded events must be ` +
|
||||||
|
|
@ -190,7 +192,9 @@ export class Zooid {
|
||||||
// Verifies docker rather than bringing the container up, which `reset` does. Repeat calls are
|
// Verifies docker rather than bringing the container up, which `reset` does. Repeat calls are
|
||||||
// free, so the fixture can call this per test.
|
// free, so the fixture can call this per test.
|
||||||
start = async () => {
|
start = async () => {
|
||||||
if (this.started) return
|
if (this.started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const problem = await describeDockerProblem()
|
const problem = await describeDockerProblem()
|
||||||
|
|
||||||
|
|
@ -381,7 +385,9 @@ export class Zooid {
|
||||||
const key = `${host} ${user.pubkey}`
|
const key = `${host} ${user.pubkey}`
|
||||||
const session = this.sessions.get(key)
|
const session = this.sessions.get(key)
|
||||||
|
|
||||||
if (session) return session
|
if (session) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
const connection = connectToZooid(host)
|
const connection = connectToZooid(host)
|
||||||
const [, challenge] = await connection.wait(isRelayAuth)
|
const [, challenge] = await connection.wait(isRelayAuth)
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,21 @@ export const makeTestRelay = ({name, url, publish}: TestRelayOptions): TestRelay
|
||||||
|
|
||||||
const tags = [["h", h]]
|
const tags = [["h", h]]
|
||||||
|
|
||||||
if (meta.name) tags.push(["name", meta.name])
|
if (meta.name) {
|
||||||
if (meta.about) tags.push(["about", meta.about])
|
tags.push(["name", meta.name])
|
||||||
if (meta.picture) tags.push(["picture", meta.picture])
|
}
|
||||||
if (meta.closed) tags.push(["closed"])
|
if (meta.about) {
|
||||||
if (meta.private) tags.push(["private"])
|
tags.push(["about", meta.about])
|
||||||
|
}
|
||||||
|
if (meta.picture) {
|
||||||
|
tags.push(["picture", meta.picture])
|
||||||
|
}
|
||||||
|
if (meta.closed) {
|
||||||
|
tags.push(["closed"])
|
||||||
|
}
|
||||||
|
if (meta.private) {
|
||||||
|
tags.push(["private"])
|
||||||
|
}
|
||||||
|
|
||||||
await event(users.admin, makeEvent(ROOM_EDIT_META, {tags, created_at: createdAt}))
|
await event(users.admin, makeEvent(ROOM_EDIT_META, {tags, created_at: createdAt}))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export default [
|
||||||
"no-useless-escape": "off",
|
"no-useless-escape": "off",
|
||||||
"no-extra-semi": "off",
|
"no-extra-semi": "off",
|
||||||
"no-async-promise-executor": "off",
|
"no-async-promise-executor": "off",
|
||||||
|
curly: ["error", "all"],
|
||||||
"prefer-const": ["error", {destructuring: "all"}],
|
"prefer-const": ["error", {destructuring: "all"}],
|
||||||
"no-eq-null": "error",
|
"no-eq-null": "error",
|
||||||
"no-restricted-syntax": [
|
"no-restricted-syntax": [
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ export const argon2id = ({
|
||||||
hashLength,
|
hashLength,
|
||||||
outputType,
|
outputType,
|
||||||
}) => {
|
}) => {
|
||||||
if (outputType !== "binary") throw new Error("Expected Pomade's binary Argon2id output")
|
if (outputType !== "binary") {
|
||||||
|
throw new Error("Expected Pomade's binary Argon2id output")
|
||||||
|
}
|
||||||
return argon2idAsync(password, salt, {
|
return argon2idAsync(password, salt, {
|
||||||
t: iterations,
|
t: iterations,
|
||||||
m: memorySize,
|
m: memorySize,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import {verifyEvent} from "nostr-tools/pure"
|
||||||
// Welshman's optional accelerator expects verification to throw on invalid events.
|
// Welshman's optional accelerator expects verification to throw on invalid events.
|
||||||
export const initNostrWasm = async () => ({
|
export const initNostrWasm = async () => ({
|
||||||
verifyEvent(event) {
|
verifyEvent(event) {
|
||||||
if (!verifyEvent(event)) throw new Error("Invalid Nostr event")
|
if (!verifyEvent(event)) {
|
||||||
|
throw new Error("Invalid Nostr event")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@ const run = (command, args, options = {}) =>
|
||||||
const child = spawn(command, args, {cwd: root, env, stdio: "inherit", ...options})
|
const child = spawn(command, args, {cwd: root, env, stdio: "inherit", ...options})
|
||||||
child.on("error", reject)
|
child.on("error", reject)
|
||||||
child.on("exit", (code, signal) => {
|
child.on("exit", (code, signal) => {
|
||||||
if (code === 0) resolve()
|
if (code === 0) {
|
||||||
else reject(new Error(`${command} failed (${signal || code})`))
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error(`${command} failed (${signal || code})`))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,9 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||||
const thunk = await publishJoinRequest(url, claim)
|
const thunk = await publishJoinRequest(url, claim)
|
||||||
const error = await thunk.waitForError()
|
const error = await thunk.waitForError()
|
||||||
|
|
||||||
if (shouldIgnoreError(error)) return
|
if (shouldIgnoreError(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (error.includes("invite code")) {
|
if (error.includes("invite code")) {
|
||||||
return "join request rejected"
|
return "join request rejected"
|
||||||
|
|
@ -282,11 +284,21 @@ export class Access {
|
||||||
this.claimFailed,
|
this.claimFailed,
|
||||||
],
|
],
|
||||||
([$loading, $isGeneric, $isExplicit, $roomInviteError, $roomCode, $claimFailed]) => {
|
([$loading, $isGeneric, $isExplicit, $roomInviteError, $roomCode, $claimFailed]) => {
|
||||||
if ($loading) return "loading" as const
|
if ($loading) {
|
||||||
if ($isGeneric) return "network" as const
|
return "loading" as const
|
||||||
if ($isExplicit || $roomInviteError) return "auth" as const
|
}
|
||||||
if (requireCode && !$roomCode) return "failed" as const
|
if ($isGeneric) {
|
||||||
if ($claimFailed) return "noclaim" as const
|
return "network" as const
|
||||||
|
}
|
||||||
|
if ($isExplicit || $roomInviteError) {
|
||||||
|
return "auth" as const
|
||||||
|
}
|
||||||
|
if (requireCode && !$roomCode) {
|
||||||
|
return "failed" as const
|
||||||
|
}
|
||||||
|
if ($claimFailed) {
|
||||||
|
return "noclaim" as const
|
||||||
|
}
|
||||||
|
|
||||||
return "ready" as const
|
return "ready" as const
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,9 @@ const compareScores = (a: LayoutScore, b: LayoutScore): number => {
|
||||||
"aspectDeviation",
|
"aspectDeviation",
|
||||||
]
|
]
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (a[field] !== b[field]) return a[field] - b[field]
|
if (a[field] !== b[field]) {
|
||||||
|
return a[field] - b[field]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +123,9 @@ const buildCandidate = (
|
||||||
|
|
||||||
const availHeight = (containerHeight - (totalRowCount - 1) * gap) / totalRowCount
|
const availHeight = (containerHeight - (totalRowCount - 1) * gap) / totalRowCount
|
||||||
const availWidth = (containerWidth - (columnCount - 1) * gap) / columnCount
|
const availWidth = (containerWidth - (columnCount - 1) * gap) / columnCount
|
||||||
if (availWidth <= 0 || availHeight <= 0) return undefined
|
if (availWidth <= 0 || availHeight <= 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const {tileWidth, tileHeight, aspectRatio} = fitTile(availWidth, availHeight, minAspect)
|
const {tileWidth, tileHeight, aspectRatio} = fitTile(availWidth, availHeight, minAspect)
|
||||||
|
|
||||||
|
|
@ -171,7 +175,9 @@ export const computeAdaptiveGrid = (
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
containerHeight: number,
|
containerHeight: number,
|
||||||
): AdaptiveTileGrid | undefined => {
|
): AdaptiveTileGrid | undefined => {
|
||||||
if (tileCount <= 0 || containerWidth <= 0 || containerHeight <= 0) return undefined
|
if (tileCount <= 0 || containerWidth <= 0 || containerHeight <= 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const minAspect = containerWidth / containerHeight >= 4 / 3 ? 1 : TILE_ASPECT_PORTRAIT
|
const minAspect = containerWidth / containerHeight >= 4 / 3 ? 1 : TILE_ASPECT_PORTRAIT
|
||||||
|
|
||||||
|
|
@ -186,7 +192,9 @@ export const computeAdaptiveGrid = (
|
||||||
containerHeight,
|
containerHeight,
|
||||||
minAspect,
|
minAspect,
|
||||||
)
|
)
|
||||||
if (!candidate) continue
|
if (!candidate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const {tileWidth, tileHeight, aspectRatio} = candidate.rows[0]
|
const {tileWidth, tileHeight, aspectRatio} = candidate.rows[0]
|
||||||
const verticalOverflow = Math.max(0, candidate.totalHeight - containerHeight)
|
const verticalOverflow = Math.max(0, candidate.totalHeight - containerHeight)
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,18 @@ export const joinVoiceRoom = async (
|
||||||
whenTimeout(15_000, {message: "Leaving previous call timed out.", signal: settle.signal}),
|
whenTimeout(15_000, {message: "Leaving previous call timed out.", signal: settle.signal}),
|
||||||
whenAborted(signal),
|
whenAborted(signal),
|
||||||
]).catch(e => {
|
]).catch(e => {
|
||||||
if (e instanceof AbortError) throw e
|
if (e instanceof AbortError) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// leaveVoiceRoom flips callState to Disconnected; re-assert Joining.
|
// leaveVoiceRoom flips callState to Disconnected; re-assert Joining.
|
||||||
callState.set(CallState.Joining)
|
callState.set(CallState.Joining)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signal.aborted) throw new AbortError()
|
if (signal.aborted) {
|
||||||
|
throw new AbortError()
|
||||||
|
}
|
||||||
|
|
||||||
const {server_url, participant_token} = await Promise.race([
|
const {server_url, participant_token} = await Promise.race([
|
||||||
fetchLivekitToken(url, h, signal),
|
fetchLivekitToken(url, h, signal),
|
||||||
|
|
@ -89,7 +93,9 @@ export const joinVoiceRoom = async (
|
||||||
whenAborted(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
|
activeRoom = liveKitRoom
|
||||||
|
|
@ -157,7 +163,9 @@ export const joinVoiceRoom = async (
|
||||||
clearReconnectSchedule()
|
clearReconnectSchedule()
|
||||||
playJoinSound()
|
playJoinSound()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isActive()) callState.set(CallState.Disconnected)
|
if (isActive()) {
|
||||||
|
callState.set(CallState.Disconnected)
|
||||||
|
}
|
||||||
if (e instanceof AbortError) {
|
if (e instanceof AbortError) {
|
||||||
clearReconnectSchedule()
|
clearReconnectSchedule()
|
||||||
return
|
return
|
||||||
|
|
@ -165,14 +173,18 @@ export const joinVoiceRoom = async (
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
settle.abort()
|
settle.abort()
|
||||||
if (isActive()) joinAbortController = undefined
|
if (isActive()) {
|
||||||
|
joinAbortController = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const leaveVoiceRoom = async () => {
|
export const leaveVoiceRoom = async () => {
|
||||||
clearReconnectSchedule()
|
clearReconnectSchedule()
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const audio = new Audio("/leave-voice-room.mp3")
|
const audio = new Audio("/leave-voice-room.mp3")
|
||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
|
|
@ -220,7 +232,9 @@ export const cancelJoinVoiceRoom = () => {
|
||||||
|
|
||||||
export const toggleMute = async () => {
|
export const toggleMute = async () => {
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
callMicMuted.update(not)
|
callMicMuted.update(not)
|
||||||
|
|
||||||
|
|
@ -238,7 +252,9 @@ export const toggleMute = async () => {
|
||||||
|
|
||||||
export const toggleCamera = async () => {
|
export const toggleCamera = async () => {
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const cameraOn = !session.cameraOn
|
const cameraOn = !session.cameraOn
|
||||||
try {
|
try {
|
||||||
|
|
@ -254,7 +270,9 @@ export const toggleCamera = async () => {
|
||||||
|
|
||||||
export const toggleScreenShare = async () => {
|
export const toggleScreenShare = async () => {
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const screenShareOn = !session.screenShareOn
|
const screenShareOn = !session.screenShareOn
|
||||||
try {
|
try {
|
||||||
|
|
@ -273,7 +291,9 @@ export const switchCallActiveDevice = async (
|
||||||
targetDeviceId: string,
|
targetDeviceId: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const id = targetDeviceId === "" ? LIVEKIT_DEFAULT_DEVICE_ID : targetDeviceId
|
const id = targetDeviceId === "" ? LIVEKIT_DEFAULT_DEVICE_ID : targetDeviceId
|
||||||
try {
|
try {
|
||||||
await session.livekit.switchActiveDevice(kind, id)
|
await session.livekit.switchActiveDevice(kind, id)
|
||||||
|
|
@ -321,11 +341,15 @@ currentCallSession.subscribe(session => {
|
||||||
if (session) {
|
if (session) {
|
||||||
hadCallSession = true
|
hadCallSession = true
|
||||||
audioResumeListener ??= App.addListener("appStateChange", ({isActive}) => {
|
audioResumeListener ??= App.addListener("appStateChange", ({isActive}) => {
|
||||||
if (isActive) void reacquireMicrophoneIfNeeded()
|
if (isActive) {
|
||||||
|
void reacquireMicrophoneIfNeeded()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!hadCallSession) return
|
if (!hadCallSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
hadCallSession = false
|
hadCallSession = false
|
||||||
void audioResumeListener?.then(l => l.remove())
|
void audioResumeListener?.then(l => l.remove())
|
||||||
audioResumeListener = undefined
|
audioResumeListener = undefined
|
||||||
|
|
@ -334,7 +358,9 @@ currentCallSession.subscribe(session => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const teardownRoom = (livekit: LiveKitRoom) => {
|
const teardownRoom = (livekit: LiveKitRoom) => {
|
||||||
if (activeRoom === livekit) activeRoom = undefined
|
if (activeRoom === livekit) {
|
||||||
|
activeRoom = undefined
|
||||||
|
}
|
||||||
|
|
||||||
// Dropping the listeners keeps TrackUnsubscribed from removing the hidden
|
// Dropping the listeners keeps TrackUnsubscribed from removing the hidden
|
||||||
// audio elements onTrackSubscribed appended, so detach them here instead.
|
// audio elements onTrackSubscribed appended, so detach them here instead.
|
||||||
|
|
@ -361,7 +387,9 @@ const syncParticipantMedia = (participant: Participant) => {
|
||||||
const state = participantMediaFrom(participant)
|
const state = participantMediaFrom(participant)
|
||||||
participantMediaState.update(m => {
|
participantMediaState.update(m => {
|
||||||
const prev = m.get(participant.identity)
|
const prev = m.get(participant.identity)
|
||||||
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) return m
|
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) {
|
||||||
|
return m
|
||||||
|
}
|
||||||
const next = new Map(m)
|
const next = new Map(m)
|
||||||
next.set(participant.identity, state)
|
next.set(participant.identity, state)
|
||||||
return next
|
return next
|
||||||
|
|
@ -370,7 +398,9 @@ const syncParticipantMedia = (participant: Participant) => {
|
||||||
|
|
||||||
// LiveKit does not emit ParticipantConnected/Disconnected during reconnect.
|
// LiveKit does not emit ParticipantConnected/Disconnected during reconnect.
|
||||||
const resyncAfterReconnect = (livekit: LiveKitRoom) => {
|
const resyncAfterReconnect = (livekit: LiveKitRoom) => {
|
||||||
if (livekit !== activeRoom) return
|
if (livekit !== activeRoom) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const next = new Map<string, ParticipantMediaState>()
|
const next = new Map<string, ParticipantMediaState>()
|
||||||
for (const p of [livekit.localParticipant, ...livekit.remoteParticipants.values()]) {
|
for (const p of [livekit.localParticipant, ...livekit.remoteParticipants.values()]) {
|
||||||
|
|
@ -379,7 +409,9 @@ const resyncAfterReconnect = (livekit: LiveKitRoom) => {
|
||||||
participantMediaState.set(next)
|
participantMediaState.set(next)
|
||||||
|
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session) return
|
if (!session) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const {localParticipant} = livekit
|
const {localParticipant} = livekit
|
||||||
callMicMuted.set(!localParticipant.isMicrophoneEnabled)
|
callMicMuted.set(!localParticipant.isMicrophoneEnabled)
|
||||||
|
|
@ -398,7 +430,9 @@ const fetchLivekitToken = async (
|
||||||
): Promise<{server_url: string; participant_token: string}> => {
|
): Promise<{server_url: string; participant_token: string}> => {
|
||||||
const endpoint = getLivekitEndpoint(url, roomId)
|
const endpoint = getLivekitEndpoint(url, roomId)
|
||||||
|
|
||||||
if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
|
if (signal?.aborted) {
|
||||||
|
throw new DOMException("Aborted", "AbortError")
|
||||||
|
}
|
||||||
|
|
||||||
const template = await makeHttpAuth(endpoint, "GET")
|
const template = await makeHttpAuth(endpoint, "GET")
|
||||||
const signedEvent = await user.get().signer.sign(template)
|
const signedEvent = await user.get().signer.sign(template)
|
||||||
|
|
@ -443,7 +477,9 @@ const setUpMicrophone = async (
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Timeout or microphone rejection: join muted, the call is still usable. A
|
// Timeout or microphone rejection: join muted, the call is still usable. A
|
||||||
// genuine abort is surfaced to the caller so it can tear down the room.
|
// genuine abort is surfaced to the caller so it can tear down the room.
|
||||||
if (e instanceof AbortError) throw e
|
if (e instanceof AbortError) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
if (!(e instanceof TimeoutError)) {
|
if (!(e instanceof TimeoutError)) {
|
||||||
pushToast({theme: "error", message: "Could not access microphone"})
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
}
|
}
|
||||||
|
|
@ -453,12 +489,16 @@ const setUpMicrophone = async (
|
||||||
|
|
||||||
const reacquireMicrophoneIfNeeded = async () => {
|
const reacquireMicrophoneIfNeeded = async () => {
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session || get(callMicMuted)) return
|
if (!session || get(callMicMuted)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const track = session.livekit.localParticipant.getTrackPublication(
|
const track = session.livekit.localParticipant.getTrackPublication(
|
||||||
Track.Source.Microphone,
|
Track.Source.Microphone,
|
||||||
)?.audioTrack
|
)?.audioTrack
|
||||||
if (!track || track.isMuted || track.isUserProvided) return
|
if (!track || track.isMuted || track.isUserProvided) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Mirrors LiveKit's own (mobile-only, video-track-only) reacquisition
|
// Mirrors LiveKit's own (mobile-only, video-track-only) reacquisition
|
||||||
// check: a capture device that died silently still reports readyState
|
// check: a capture device that died silently still reports readyState
|
||||||
|
|
@ -468,7 +508,9 @@ const reacquireMicrophoneIfNeeded = async () => {
|
||||||
const {mediaStreamTrack} = track
|
const {mediaStreamTrack} = track
|
||||||
const needsReacquisition =
|
const needsReacquisition =
|
||||||
mediaStreamTrack.readyState !== "live" || mediaStreamTrack.muted || !mediaStreamTrack.enabled
|
mediaStreamTrack.readyState !== "live" || mediaStreamTrack.muted || !mediaStreamTrack.enabled
|
||||||
if (!needsReacquisition) return
|
if (!needsReacquisition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await track.restartTrack()
|
await track.restartTrack()
|
||||||
|
|
@ -488,7 +530,9 @@ const clearReconnectSchedule = () => {
|
||||||
|
|
||||||
const attemptReconnect = async () => {
|
const attemptReconnect = async () => {
|
||||||
const target = get(callTargetRoom)
|
const target = get(callTargetRoom)
|
||||||
if (!target) return
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await joinVoiceRoom(target.url, target.h, reconnectMicMuted, reconnectMicDeviceId)
|
await joinVoiceRoom(target.url, target.h, reconnectMicMuted, reconnectMicDeviceId)
|
||||||
|
|
@ -503,8 +547,12 @@ const attemptReconnect = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduleReconnect = () => {
|
const scheduleReconnect = () => {
|
||||||
if (reconnectTimeout !== undefined) return
|
if (reconnectTimeout !== undefined) {
|
||||||
if (!get(callTargetRoom)) return
|
return
|
||||||
|
}
|
||||||
|
if (!get(callTargetRoom)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (reconnectAttempt >= RECONNECT_DELAYS.length) {
|
if (reconnectAttempt >= RECONNECT_DELAYS.length) {
|
||||||
pushToast({theme: "error", message: "Voice connection lost."})
|
pushToast({theme: "error", message: "Voice connection lost."})
|
||||||
return
|
return
|
||||||
|
|
@ -519,13 +567,17 @@ const scheduleReconnect = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeOnRoomReconnected = (livekit: LiveKitRoom) => () => {
|
const makeOnRoomReconnected = (livekit: LiveKitRoom) => () => {
|
||||||
if (livekit !== activeRoom) return
|
if (livekit !== activeRoom) {
|
||||||
|
return
|
||||||
|
}
|
||||||
resyncAfterReconnect(livekit)
|
resyncAfterReconnect(livekit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeOnRoomDisconnected = (livekit: LiveKitRoom) => (reason?: DisconnectReason) => {
|
const makeOnRoomDisconnected = (livekit: LiveKitRoom) => (reason?: DisconnectReason) => {
|
||||||
// Ignore disconnects from rooms that are no longer the active session.
|
// Ignore disconnects from rooms that are no longer the active session.
|
||||||
if (livekit !== activeRoom) return
|
if (livekit !== activeRoom) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Livekit unsubscribes remote tracks before emitting Disconnected, so
|
// Livekit unsubscribes remote tracks before emitting Disconnected, so
|
||||||
// onTrackUnsubscribed has already removed their audio elements by now.
|
// onTrackUnsubscribed has already removed their audio elements by now.
|
||||||
|
|
@ -599,10 +651,16 @@ const onLocalTrackUnpublished = (
|
||||||
publication: LocalTrackPublication,
|
publication: LocalTrackPublication,
|
||||||
participant: LocalParticipant,
|
participant: LocalParticipant,
|
||||||
) => {
|
) => {
|
||||||
if (publication.source !== Track.Source.ScreenShare) return
|
if (publication.source !== Track.Source.ScreenShare) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const session = get(currentCallSession)
|
const session = get(currentCallSession)
|
||||||
if (!session || participant.identity !== session.livekit.localParticipant.identity) return
|
if (!session || participant.identity !== session.livekit.localParticipant.identity) {
|
||||||
if (!session.screenShareOn) return
|
return
|
||||||
|
}
|
||||||
|
if (!session.screenShareOn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
currentCallSession.set({...session, screenShareOn: false})
|
currentCallSession.set({...session, screenShareOn: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,9 @@ export const matchesTopic = (event: TrustedEvent, topic: string) =>
|
||||||
export const matchesQuery = (event: TrustedEvent, query: string) => {
|
export const matchesQuery = (event: TrustedEvent, query: string) => {
|
||||||
const q = query.trim().toLowerCase()
|
const q = query.trim().toLowerCase()
|
||||||
|
|
||||||
if (!q) return true
|
if (!q) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const classified = reader(Classified)(event)
|
const classified = reader(Classified)(event)
|
||||||
const haystack = [classified.title(), classified.summary(), event.content]
|
const haystack = [classified.title(), classified.summary(), event.content]
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@
|
||||||
// offsetTop is relative to the nearest positioned ancestor, which the scroll container itself
|
// offsetTop is relative to the nearest positioned ancestor, which the scroll container itself
|
||||||
// isn't — comparing bounding rects instead keeps this correct regardless of that
|
// isn't — comparing bounding rects instead keeps this correct regardless of that
|
||||||
const recenter = (target: HTMLElement) => {
|
const recenter = (target: HTMLElement) => {
|
||||||
if (!element) return
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const elementRect = element.getBoundingClientRect()
|
const elementRect = element.getBoundingClientRect()
|
||||||
const targetRect = target.getBoundingClientRect()
|
const targetRect = target.getBoundingClientRect()
|
||||||
|
|
@ -70,7 +72,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!element || !contentEl) return
|
if (!element || !contentEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const unpin = () => {
|
const unpin = () => {
|
||||||
pinnedTarget = undefined
|
pinnedTarget = undefined
|
||||||
|
|
@ -82,7 +86,9 @@
|
||||||
// every one of those (unlike a MutationObserver, which misses an image's own load-driven
|
// every one of those (unlike a MutationObserver, which misses an image's own load-driven
|
||||||
// reflow) and keeps the pin centered until the visitor scrolls themselves.
|
// reflow) and keeps the pin centered until the visitor scrolls themselves.
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
if (pinnedTarget) recenter(pinnedTarget)
|
if (pinnedTarget) {
|
||||||
|
recenter(pinnedTarget)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
observer.observe(contentEl)
|
observer.observe(contentEl)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@
|
||||||
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || loading) return
|
if ($uploading || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@
|
||||||
const {url, event, rsvps, onShowPeople}: Props = $props()
|
const {url, event, rsvps, onShowPeople}: Props = $props()
|
||||||
|
|
||||||
const setStatus = async (status: RsvpStatus) => {
|
const setStatus = async (status: RsvpStatus) => {
|
||||||
if (loading) return
|
if (loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@
|
||||||
const spaceName = $derived($callTargetRoom ? displayRelayUrl($callTargetRoom.url) : "")
|
const spaceName = $derived($callTargetRoom ? displayRelayUrl($callTargetRoom.url) : "")
|
||||||
|
|
||||||
const goToRoom = () => {
|
const goToRoom = () => {
|
||||||
if (!$callTargetRoom) return
|
if (!$callTargetRoom) {
|
||||||
|
return
|
||||||
|
}
|
||||||
void navigate(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
void navigate(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || disabled) return
|
if ($uploading || disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = ed.storage.nostr.getEditorTags()
|
const tags = ed.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
if (!content) return
|
if (!content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onSubmit({content, tags})
|
await onSubmit({content, tags})
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,12 @@
|
||||||
const activeArg = $derived(draft?.args[draft.activeIndex])
|
const activeArg = $derived(draft?.args[draft.activeIndex])
|
||||||
|
|
||||||
const choices = $derived.by(() => {
|
const choices = $derived.by(() => {
|
||||||
if (activeArg?.type === "enum") return activeArg.choices
|
if (activeArg?.type === "enum") {
|
||||||
if (activeArg?.type === "bool") return ["true", "false"]
|
return activeArg.choices
|
||||||
|
}
|
||||||
|
if (activeArg?.type === "bool") {
|
||||||
|
return ["true", "false"]
|
||||||
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || loading) return
|
if ($uploading || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@
|
||||||
const isBlock = (i: number) => {
|
const isBlock = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed) return false
|
if (!parsed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
||||||
return true
|
return true
|
||||||
|
|
@ -98,8 +100,12 @@
|
||||||
const isBoundary = (i: number) => {
|
const isBoundary = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed || isNewline(parsed)) return true
|
if (!parsed || isNewline(parsed)) {
|
||||||
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
|
return true
|
||||||
|
}
|
||||||
|
if (isText(parsed)) {
|
||||||
|
return Boolean(parsed.value.match(/^\s+$/))
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
|
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) {
|
||||||
|
return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
}
|
||||||
|
|
||||||
return [url, true]
|
return [url, true]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,15 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
|
if (roomReference) {
|
||||||
if (relayReference) return [makeSpacePath(relayReference), false]
|
return [makeRoomPath(roomReference.url, roomReference.h), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
}
|
||||||
|
if (relayReference) {
|
||||||
|
return [makeSpacePath(relayReference), false]
|
||||||
|
}
|
||||||
|
if (url.startsWith(PLATFORM_URL)) {
|
||||||
|
return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
}
|
||||||
|
|
||||||
return [url, true]
|
return [url, true]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,12 @@
|
||||||
const pubkeyFromEntity = (entity: string) => {
|
const pubkeyFromEntity = (entity: string) => {
|
||||||
const {type, data} = nip19.decode(entity)
|
const {type, data} = nip19.decode(entity)
|
||||||
|
|
||||||
if (type === "npub") return data
|
if (type === "npub") {
|
||||||
if (type === "nprofile") return data.pubkey
|
return data
|
||||||
|
}
|
||||||
|
if (type === "nprofile") {
|
||||||
|
return data.pubkey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everyone the content mentions, so their names are asked for and awaited rather than read
|
// Everyone the content mentions, so their names are asked for and awaited rather than read
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,12 @@
|
||||||
const isBoundary = (i: number) => {
|
const isBoundary = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed || isNewline(parsed)) return true
|
if (!parsed || isNewline(parsed)) {
|
||||||
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
|
return true
|
||||||
|
}
|
||||||
|
if (isText(parsed)) {
|
||||||
|
return Boolean(parsed.value.match(/^\s+$/))
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
let content = ed.getText({blockSeparator: "\n"}).trim()
|
let content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || loading) return
|
if ($uploading || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@
|
||||||
const {loading, bunker} = controller
|
const {loading, bunker} = controller
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if ($loading) return
|
if ($loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
|
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
const t = track
|
const t = track
|
||||||
level = 0
|
level = 0
|
||||||
|
|
||||||
if (!t || error) return
|
if (!t || error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let frame: number | undefined
|
let frame: number | undefined
|
||||||
const context = new AudioContext()
|
const context = new AudioContext()
|
||||||
|
|
@ -50,14 +52,18 @@
|
||||||
const NOISE_FLOOR = 0.03
|
const NOISE_FLOOR = 0.03
|
||||||
const gated = rms < NOISE_FLOOR ? 0 : (rms - NOISE_FLOOR) / (1 - NOISE_FLOOR)
|
const gated = rms < NOISE_FLOOR ? 0 : (rms - NOISE_FLOOR) / (1 - NOISE_FLOOR)
|
||||||
smoothed += (Math.min(1, gated * 4) - smoothed) * 0.5
|
smoothed += (Math.min(1, gated * 4) - smoothed) * 0.5
|
||||||
if (smoothed < 0.005) smoothed = 0
|
if (smoothed < 0.005) {
|
||||||
|
smoothed = 0
|
||||||
|
}
|
||||||
level = smoothed
|
level = smoothed
|
||||||
frame = requestAnimationFrame(tick)
|
frame = requestAnimationFrame(tick)
|
||||||
}
|
}
|
||||||
tick()
|
tick()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (frame !== undefined) cancelAnimationFrame(frame)
|
if (frame !== undefined) {
|
||||||
|
cancelAnimationFrame(frame)
|
||||||
|
}
|
||||||
context.close().catch(() => {})
|
context.close().catch(() => {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (Capacitor.isNativePlatform()) return
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
audioElement.load()
|
audioElement.load()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (loading) return
|
if (loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@
|
||||||
const openChat = () => goToChat([target])
|
const openChat = () => goToChat([target])
|
||||||
|
|
||||||
const toggleFollow = async () => {
|
const toggleFollow = async () => {
|
||||||
if (isSelf) return
|
if (isSelf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isFollowing) {
|
if (isFollowing) {
|
||||||
await $followLists.unfollow(target).then(publish)
|
await $followLists.unfollow(target).then(publish)
|
||||||
|
|
@ -72,7 +74,9 @@
|
||||||
|
|
||||||
input.value = ""
|
input.value = ""
|
||||||
|
|
||||||
if (!file || !$profile) return
|
if (!file || !$profile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
bannerLoading = true
|
bannerLoading = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@
|
||||||
const copyCode = () => clip(code)
|
const copyCode = () => clip(code)
|
||||||
|
|
||||||
const shareProfile = async () => {
|
const shareProfile = async () => {
|
||||||
if (!canShare) return
|
if (!canShare) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Share.share({url: code})
|
await Share.share({url: code})
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
let progress = $state(0)
|
let progress = $state(0)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!element) return
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const target = element
|
const target = element
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,18 @@
|
||||||
$relaySearch
|
$relaySearch
|
||||||
.searchValues(term)
|
.searchValues(term)
|
||||||
.filter((url: string) => {
|
.filter((url: string) => {
|
||||||
if (matchRelay?.(url) === false) return false
|
if (matchRelay?.(url) === false) {
|
||||||
if ($relays.includes(url)) return false
|
return false
|
||||||
if (isIPAddress(url)) return false
|
}
|
||||||
if ($blockedRelayUrls.includes(url)) return false
|
if ($relays.includes(url)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isIPAddress(url)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ($blockedRelayUrls.includes(url)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,9 @@
|
||||||
const hidePopover = () => tippy?.hide()
|
const hidePopover = () => tippy?.hide()
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@
|
||||||
const copyInvite = () => clip(invite)
|
const copyInvite = () => clip(invite)
|
||||||
|
|
||||||
const shareInvite = async () => {
|
const shareInvite = async () => {
|
||||||
if (!canShare) return
|
if (!canShare) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Share.share({url: invite})
|
await Share.share({url: invite})
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,9 @@
|
||||||
const togglePin = async () => {
|
const togglePin = async () => {
|
||||||
onClick()
|
onClick()
|
||||||
|
|
||||||
if (!h) return
|
if (!h) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The optimistic write flips isPinned while the publish is in flight, so what this did is read
|
// The optimistic write flips isPinned while the publish is in flight, so what this did is read
|
||||||
// before it goes out rather than after.
|
// before it goes out rather than after.
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePin = async () => {
|
const togglePin = async () => {
|
||||||
if (!h) return
|
if (!h) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,26 +61,34 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const prev = () => {
|
const prev = () => {
|
||||||
if (total === 0) return
|
if (total === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex = (currentIndex - 1 + total) % total
|
currentIndex = (currentIndex - 1 + total) % total
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (total === 0) return
|
if (total === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex = (currentIndex + 1) % total
|
currentIndex = (currentIndex + 1) % total
|
||||||
}
|
}
|
||||||
|
|
||||||
const jumpToMessage = () => {
|
const jumpToMessage = () => {
|
||||||
if (!currentEvent) return
|
if (!currentEvent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
collapse()
|
collapse()
|
||||||
goToEvent(currentEvent)
|
goToEvent(currentEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDocumentPointerDown = (event: PointerEvent) => {
|
const onDocumentPointerDown = (event: PointerEvent) => {
|
||||||
if (!expanded || !root) return
|
if (!expanded || !root) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.contains(event.target as Node)) {
|
if (!root.contains(event.target as Node)) {
|
||||||
collapse()
|
collapse()
|
||||||
|
|
@ -98,7 +106,9 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!expanded) return
|
if (!expanded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("pointerdown", onDocumentPointerDown)
|
document.addEventListener("pointerdown", onDocumentPointerDown)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toastId || timeout) return
|
if (toastId || timeout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// deriveSocketStatus is throttled, so a socket that opens immediately still reports
|
// deriveSocketStatus is throttled, so a socket that opens immediately still reports
|
||||||
// "Connecting" for the length of the throttle. Wait that out instead of flashing.
|
// "Connecting" for the length of the throttle. Wait that out instead of flashing.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@
|
||||||
const copyInvite = () => clip(invite)
|
const copyInvite = () => clip(invite)
|
||||||
|
|
||||||
const shareInvite = async () => {
|
const shareInvite = async () => {
|
||||||
if (!canShare) return
|
if (!canShare) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Share.share({url: invite})
|
await Share.share({url: invite})
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || loading) return
|
if ($uploading || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!containerEl) return
|
if (!containerEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
containerEl.addEventListener("touchmove", onTouchMove, {passive: false})
|
containerEl.addEventListener("touchmove", onTouchMove, {passive: false})
|
||||||
return () => containerEl?.removeEventListener("touchmove", onTouchMove)
|
return () => containerEl?.removeEventListener("touchmove", onTouchMove)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -127,13 +127,17 @@
|
||||||
|
|
||||||
const primaryTile = $derived.by(() => {
|
const primaryTile = $derived.by(() => {
|
||||||
const k = $videoPrimaryTileKey
|
const k = $videoPrimaryTileKey
|
||||||
if (k === undefined) return undefined
|
if (k === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
return videoTiles.find(t => tileKey(t) === k)
|
return videoTiles.find(t => tileKey(t) === k)
|
||||||
})
|
})
|
||||||
|
|
||||||
const secondaryTiles = $derived.by(() => {
|
const secondaryTiles = $derived.by(() => {
|
||||||
const p = primaryTile
|
const p = primaryTile
|
||||||
if (p === undefined) return videoTiles
|
if (p === undefined) {
|
||||||
|
return videoTiles
|
||||||
|
}
|
||||||
const pk = tileKey(p)
|
const pk = tileKey(p)
|
||||||
return videoTiles.filter(t => tileKey(t) !== pk)
|
return videoTiles.filter(t => tileKey(t) !== pk)
|
||||||
})
|
})
|
||||||
|
|
@ -150,7 +154,9 @@
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const k = $videoPrimaryTileKey
|
const k = $videoPrimaryTileKey
|
||||||
if (k === undefined) return
|
if (k === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!videoTiles.some(t => tileKey(t) === k)) {
|
if (!videoTiles.some(t => tileKey(t) === k)) {
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const element = videoElement
|
const element = videoElement
|
||||||
const activeTrack = track
|
const activeTrack = track
|
||||||
if (!element) return
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
activeTrack.attach(element)
|
activeTrack.attach(element)
|
||||||
return () => {
|
return () => {
|
||||||
activeTrack.detach(element)
|
activeTrack.detach(element)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@
|
||||||
?.mediaStreamTrack
|
?.mediaStreamTrack
|
||||||
|
|
||||||
const loadDevices = async () => {
|
const loadDevices = async () => {
|
||||||
if (!navigator.mediaDevices?.enumerateDevices) return
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
audioInputs = devices.filter(spec({kind: "audioinput"}))
|
audioInputs = devices.filter(spec({kind: "audioinput"}))
|
||||||
|
|
@ -85,7 +87,9 @@
|
||||||
// place with a new MediaStreamTrack rather than emitting a publish event,
|
// place with a new MediaStreamTrack rather than emitting a publish event,
|
||||||
// so the meter needs an explicit re-read here.
|
// so the meter needs an explicit re-read here.
|
||||||
const session = $currentCallSession
|
const session = $currentCallSession
|
||||||
if (session) micTrack = readMicTrack(session)
|
if (session) {
|
||||||
|
micTrack = readMicTrack(session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOutputChange = () => {
|
const onOutputChange = () => {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,9 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = async (e: MouseEvent) => {
|
const handleClick = async (e: MouseEvent) => {
|
||||||
if (isActive) return
|
if (isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isJoining) {
|
if (isJoining) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -67,7 +69,9 @@
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
for (const p of $participants) {
|
for (const p of $participants) {
|
||||||
if (p.pubkey) $profiles.load(p.pubkey, [url])
|
if (p.pubkey) {
|
||||||
|
$profiles.load(p.pubkey, [url])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@
|
||||||
let micError = $state(false)
|
let micError = $state(false)
|
||||||
|
|
||||||
const loadDevices = async () => {
|
const loadDevices = async () => {
|
||||||
if (!navigator.mediaDevices?.enumerateDevices) return
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
audioInputs = devices.filter(spec({kind: "audioinput"}))
|
audioInputs = devices.filter(spec({kind: "audioinput"}))
|
||||||
|
|
@ -75,7 +77,9 @@
|
||||||
stopMicPreview()
|
stopMicPreview()
|
||||||
micError = false
|
micError = false
|
||||||
|
|
||||||
if (withoutMic || !navigator.mediaDevices?.getUserMedia) return
|
if (withoutMic || !navigator.mediaDevices?.getUserMedia) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
const deviceId = selectedDeviceId
|
const deviceId = selectedDeviceId
|
||||||
|
|
@ -92,7 +96,9 @@
|
||||||
previewStream = stream
|
previewStream = stream
|
||||||
previewTrack = stream.getAudioTracks()[0]
|
previewTrack = stream.getAudioTracks()[0]
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) micError = true
|
if (!cancelled) {
|
||||||
|
micError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
@ -108,12 +114,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleJoinError = (e: unknown) => {
|
const handleJoinError = (e: unknown) => {
|
||||||
if (e instanceof AbortError) return
|
if (e instanceof AbortError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
console.error("Failed to join voice room", e)
|
console.error("Failed to join voice room", e)
|
||||||
let message = "Failed to join voice room"
|
let message = "Failed to join voice room"
|
||||||
if (e instanceof TimeoutError)
|
if (e instanceof TimeoutError) {
|
||||||
message = "Connection timed out. Please check your network and try again."
|
message = "Connection timed out. Please check your network and try again."
|
||||||
else if (e instanceof Error) message = e.message
|
} else if (e instanceof Error) {
|
||||||
|
message = e.message
|
||||||
|
}
|
||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,9 @@
|
||||||
const updateMethod = () => openSetup(tenant?.nwc_error ? "nwc" : "card")
|
const updateMethod = () => openSetup(tenant?.nwc_error ? "nwc" : "card")
|
||||||
|
|
||||||
const payOpenInvoice = () => {
|
const payOpenInvoice = () => {
|
||||||
if (openInvoice) openPayInvoice(openInvoice)
|
if (openInvoice) {
|
||||||
|
openPayInvoice(openInvoice)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismissAutopay = () => {
|
const dismissAutopay = () => {
|
||||||
|
|
@ -60,7 +62,9 @@
|
||||||
let handledInvoiceId: string | undefined
|
let handledInvoiceId: string | undefined
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const id = $page.url.searchParams.get("invoice") ?? undefined
|
const id = $page.url.searchParams.get("invoice") ?? undefined
|
||||||
if (!id || id === handledInvoiceId) return
|
if (!id || id === handledInvoiceId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handledInvoiceId = id
|
handledInvoiceId = id
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,15 @@
|
||||||
|
|
||||||
// The backend models the invoice lifecycle as timestamps, not a status field.
|
// The backend models the invoice lifecycle as timestamps, not a status field.
|
||||||
const status = $derived.by(() => {
|
const status = $derived.by(() => {
|
||||||
if (isDraft) return "draft"
|
if (isDraft) {
|
||||||
if (invoice.paid_at) return "paid"
|
return "draft"
|
||||||
if (invoice.voided_at) return "void"
|
}
|
||||||
|
if (invoice.paid_at) {
|
||||||
|
return "paid"
|
||||||
|
}
|
||||||
|
if (invoice.voided_at) {
|
||||||
|
return "void"
|
||||||
|
}
|
||||||
return "open"
|
return "open"
|
||||||
})
|
})
|
||||||
const isOpen = $derived(status === "open")
|
const isOpen = $derived(status === "open")
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const markPaid = () => {
|
const markPaid = () => {
|
||||||
if (paid) return
|
if (paid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
paid = true
|
paid = true
|
||||||
stopPolling()
|
stopPolling()
|
||||||
|
|
@ -121,10 +123,14 @@
|
||||||
|
|
||||||
// Silent poll until paid or the modal closes.
|
// Silent poll until paid or the modal closes.
|
||||||
const pollOnce = async () => {
|
const pollOnce = async () => {
|
||||||
if (destroyed || paid) return
|
if (destroyed || paid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await reconcile()) return
|
if (await reconcile()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// keep polling
|
// keep polling
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +141,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkPayment = async () => {
|
const checkPayment = async () => {
|
||||||
if (checking) return
|
if (checking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checking = true
|
checking = true
|
||||||
|
|
||||||
|
|
@ -161,7 +169,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const payWithCard = async () => {
|
const payWithCard = async () => {
|
||||||
if (redirecting) return
|
if (redirecting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
redirecting = true
|
redirecting = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@
|
||||||
// The tenant's autopay wallet (a backend NWC, write-only), distinct from the
|
// The tenant's autopay wallet (a backend NWC, write-only), distinct from the
|
||||||
// app's own wallet.
|
// app's own wallet.
|
||||||
const persistNwc = async (url?: string) => {
|
const persistNwc = async (url?: string) => {
|
||||||
if (!url || saving) return
|
if (!url || saving) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
|
|
@ -79,7 +81,9 @@
|
||||||
const useSpendingWallet = () => persistNwc(spendingWalletUrl)
|
const useSpendingWallet = () => persistNwc(spendingWalletUrl)
|
||||||
|
|
||||||
const disconnectNwc = async () => {
|
const disconnectNwc = async () => {
|
||||||
if (saving) return
|
if (saving) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!canSave) return
|
if (!canSave) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,11 @@ export const displayContentCount = (kind: number, count: number) => {
|
||||||
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
||||||
|
|
||||||
export const displayReaction = (content: string) => {
|
export const displayReaction = (content: string) => {
|
||||||
if (!content || content === "+") return "❤️"
|
if (!content || content === "+") {
|
||||||
if (content === "-") return "👎"
|
return "❤️"
|
||||||
|
}
|
||||||
|
if (content === "-") {
|
||||||
|
return "👎"
|
||||||
|
}
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,12 @@ const mergeSorted = <T>(left: T[], right: T[], compare: (a: T, b: T) => number)
|
||||||
merged.push(compare(left[i], right[j]) <= 0 ? left[i++] : right[j++])
|
merged.push(compare(left[i], right[j]) <= 0 ? left[i++] : right[j++])
|
||||||
}
|
}
|
||||||
|
|
||||||
while (i < left.length) merged.push(left[i++])
|
while (i < left.length) {
|
||||||
while (j < right.length) merged.push(right[j++])
|
merged.push(left[i++])
|
||||||
|
}
|
||||||
|
while (j < right.length) {
|
||||||
|
merged.push(right[j++])
|
||||||
|
}
|
||||||
|
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +123,9 @@ export const makeFeedContext = ({
|
||||||
if (deletedChecks.size > 0) {
|
if (deletedChecks.size > 0) {
|
||||||
if (removed.size > 0) {
|
if (removed.size > 0) {
|
||||||
for (const checks of deletedChecks.values()) {
|
for (const checks of deletedChecks.values()) {
|
||||||
for (const check of checks) check()
|
for (const check of checks) {
|
||||||
|
check()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const event of added) {
|
for (const event of added) {
|
||||||
|
|
@ -403,7 +409,9 @@ const makeFeedLoader = (load: () => Promise<FeedSpan>) => {
|
||||||
let running = false
|
let running = false
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (running || get(state).status === "exhausted") return
|
if (running || get(state).status === "exhausted") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
running = true
|
running = true
|
||||||
|
|
||||||
|
|
@ -425,7 +433,9 @@ const makeFeedLoader = (load: () => Promise<FeedSpan>) => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found > 0) break
|
if (found > 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
running = false
|
running = false
|
||||||
|
|
@ -608,7 +618,9 @@ export const makeFeed = ({
|
||||||
// The window only moves once the relays have answered: a request the socket dropped looks
|
// The window only moves once the relays have answered: a request the socket dropped looks
|
||||||
// exactly like an empty span, and walking past it would leave a hole nothing goes back for.
|
// exactly like an empty span, and walking past it would leave a hole nothing goes back for.
|
||||||
const loadOlder = async (): Promise<FeedSpan> => {
|
const loadOlder = async (): Promise<FeedSpan> => {
|
||||||
if (oldest < now() - int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
|
if (oldest < now() - int(2, YEAR)) {
|
||||||
|
return {found: 0, complete: true, exhausted: true}
|
||||||
|
}
|
||||||
|
|
||||||
const until = oldest
|
const until = oldest
|
||||||
const since = until - olderInterval
|
const since = until - olderInterval
|
||||||
|
|
@ -641,7 +653,9 @@ export const makeFeed = ({
|
||||||
// A limit is answered with the newest events matching it, which reaches away from an anchor in
|
// A limit is answered with the newest events matching it, which reaches away from an anchor in
|
||||||
// the past rather than toward it, so this direction walks spans as it always has
|
// the past rather than toward it, so this direction walks spans as it always has
|
||||||
const loadNewer = async (): Promise<FeedSpan> => {
|
const loadNewer = async (): Promise<FeedSpan> => {
|
||||||
if (newest >= now()) return {found: 0, complete: true, exhausted: true}
|
if (newest >= now()) {
|
||||||
|
return {found: 0, complete: true, exhausted: true}
|
||||||
|
}
|
||||||
|
|
||||||
const since = newest
|
const since = newest
|
||||||
const until = Math.min(now(), since + newerInterval)
|
const until = Math.min(now(), since + newerInterval)
|
||||||
|
|
@ -714,7 +728,9 @@ export const makeCalendarFeed = ({
|
||||||
const insertEvents = (newEvents: TrustedEvent[]) => {
|
const insertEvents = (newEvents: TrustedEvent[]) => {
|
||||||
const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)) && !seen.has(e.id))
|
const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)) && !seen.has(e.id))
|
||||||
|
|
||||||
if (valid.length === 0) return
|
if (valid.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for (const event of valid) {
|
for (const event of valid) {
|
||||||
seen.add(event.id)
|
seen.add(event.id)
|
||||||
|
|
@ -771,7 +787,9 @@ export const makeCalendarFeed = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadOlder = async (): Promise<FeedSpan> => {
|
const loadOlder = async (): Promise<FeedSpan> => {
|
||||||
if (oldest < now() - int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
|
if (oldest < now() - int(2, YEAR)) {
|
||||||
|
return {found: 0, complete: true, exhausted: true}
|
||||||
|
}
|
||||||
|
|
||||||
const until = oldest
|
const until = oldest
|
||||||
const since = until - interval
|
const since = until - interval
|
||||||
|
|
@ -785,7 +803,9 @@ export const makeCalendarFeed = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNewer = async (): Promise<FeedSpan> => {
|
const loadNewer = async (): Promise<FeedSpan> => {
|
||||||
if (newest > now() + int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
|
if (newest > now() + int(2, YEAR)) {
|
||||||
|
return {found: 0, complete: true, exhausted: true}
|
||||||
|
}
|
||||||
|
|
||||||
const since = newest
|
const since = newest
|
||||||
const until = since + interval
|
const until = since + interval
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,9 @@ const getAuthHeader = (): Promise<string> => {
|
||||||
|
|
||||||
// A declined signature shouldn't be cached, or the user can't retry.
|
// A declined signature shouldn't be cached, or the user can't retry.
|
||||||
header.catch(() => {
|
header.catch(() => {
|
||||||
if (cachedAuth?.header === header) cachedAuth = undefined
|
if (cachedAuth?.header === header) {
|
||||||
|
cachedAuth = undefined
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
cachedAuth = {pubkey, expiresAt: Date.now() + AUTH_TTL, header}
|
cachedAuth = {pubkey, expiresAt: Date.now() + AUTH_TTL, header}
|
||||||
|
|
@ -232,7 +234,9 @@ const hostingRequest = async (method: string, path: string, init: RequestInit =
|
||||||
const hostingJson = async <T>(response: Response): Promise<T> => {
|
const hostingJson = async <T>(response: Response): Promise<T> => {
|
||||||
const payload: Maybe<HostingResponse<T>> = await response.json().catch(() => undefined)
|
const payload: Maybe<HostingResponse<T>> = await response.json().catch(() => undefined)
|
||||||
|
|
||||||
if (response.ok && payload) return payload.data
|
if (response.ok && payload) {
|
||||||
|
return payload.data
|
||||||
|
}
|
||||||
|
|
||||||
throw new HostingError(payload?.error || `Request failed (${response.status})`, response.status)
|
throw new HostingError(payload?.error || `Request failed (${response.status})`, response.status)
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +303,9 @@ export const listRelayActivity = (id: string) =>
|
||||||
export const exportRelayData = async (id: string) => {
|
export const exportRelayData = async (id: string) => {
|
||||||
const response = await hostingRequest("GET", `/relays/${id}/export`)
|
const response = await hostingRequest("GET", `/relays/${id}/export`)
|
||||||
|
|
||||||
if (response.ok) return response.text()
|
if (response.ok) {
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
|
|
||||||
// Only the failure path is a json envelope; success is the dump itself.
|
// Only the failure path is a json envelope; success is the dump itself.
|
||||||
return hostingJson<string>(response)
|
return hostingJson<string>(response)
|
||||||
|
|
@ -336,8 +342,12 @@ export const reconcileInvoice = (invoiceId: string) =>
|
||||||
// API utils
|
// API utils
|
||||||
|
|
||||||
export const flagToBool = (value: number | undefined, fallback: boolean): boolean => {
|
export const flagToBool = (value: number | undefined, fallback: boolean): boolean => {
|
||||||
if (value === 0) return false
|
if (value === 0) {
|
||||||
if (value === 1) return true
|
return false
|
||||||
|
}
|
||||||
|
if (value === 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +401,9 @@ export const ensureSessionTenant = async () => {
|
||||||
const promise = createTenant(hostingReturnUrl())
|
const promise = createTenant(hostingReturnUrl())
|
||||||
|
|
||||||
promise.catch(() => {
|
promise.catch(() => {
|
||||||
if (tenantPromise?.promise === promise) tenantPromise = undefined
|
if (tenantPromise?.promise === promise) {
|
||||||
|
tenantPromise = undefined
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
tenantPromise = {pubkey: $pubkey, promise}
|
tenantPromise = {pubkey: $pubkey, promise}
|
||||||
|
|
@ -423,7 +435,9 @@ export const deriveRelayActivity = (id: Readable<Maybe<string>>) =>
|
||||||
derived<Readable<Maybe<string>>, RelayActivityState>(
|
derived<Readable<Maybe<string>>, RelayActivityState>(
|
||||||
id,
|
id,
|
||||||
($id, set) => {
|
($id, set) => {
|
||||||
if (!$id) return set({loading: false, activity: []})
|
if (!$id) {
|
||||||
|
return set({loading: false, activity: []})
|
||||||
|
}
|
||||||
|
|
||||||
set({loading: true, activity: []})
|
set({loading: true, activity: []})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import {Keyboard} from "@capacitor/keyboard"
|
||||||
import {noop} from "@welshman/lib"
|
import {noop} from "@welshman/lib"
|
||||||
|
|
||||||
export const syncKeyboard = () => {
|
export const syncKeyboard = () => {
|
||||||
if (!Capacitor.isPluginAvailable("Keyboard")) return noop
|
if (!Capacitor.isPluginAvailable("Keyboard")) {
|
||||||
|
return noop
|
||||||
|
}
|
||||||
|
|
||||||
const showListener = Keyboard.addListener("keyboardWillShow", () => {
|
const showListener = Keyboard.addListener("keyboardWillShow", () => {
|
||||||
document.body.classList.add("keyboard-open")
|
document.body.classList.add("keyboard-open")
|
||||||
|
|
@ -17,7 +19,9 @@ export const syncKeyboard = () => {
|
||||||
// On Android, system-dismissing the IME during pause doesn't fire keyboardWillHide,
|
// On Android, system-dismissing the IME during pause doesn't fire keyboardWillHide,
|
||||||
// so on resume we force a hide to re-sync native insets and clear our CSS state.
|
// so on resume we force a hide to re-sync native insets and clear our CSS state.
|
||||||
const resumeListener = App.addListener("appStateChange", ({isActive}) => {
|
const resumeListener = App.addListener("appStateChange", ({isActive}) => {
|
||||||
if (isActive) Keyboard.hide()
|
if (isActive) {
|
||||||
|
Keyboard.hide()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,14 @@ export const payInvoice = async (invoice: string, msats?: number) => {
|
||||||
|
|
||||||
if (isNWCWallet($wallet)) {
|
if (isNWCWallet($wallet)) {
|
||||||
const params: {invoice: string; amount?: number} = {invoice}
|
const params: {invoice: string; amount?: number} = {invoice}
|
||||||
if (msats) params.amount = msats
|
if (msats) {
|
||||||
|
params.amount = msats
|
||||||
|
}
|
||||||
return getNwcClient().payInvoice(params)
|
return getNwcClient().payInvoice(params)
|
||||||
} else if (isWebLNWallet($wallet)) {
|
} else if (isWebLNWallet($wallet)) {
|
||||||
if (msats) throw new Error("Unable to pay zero invoices with webln")
|
if (msats) {
|
||||||
|
throw new Error("Unable to pay zero invoices with webln")
|
||||||
|
}
|
||||||
return getWebLn()
|
return getWebLn()
|
||||||
.enable()
|
.enable()
|
||||||
.then(() => getWebLn().sendPayment(invoice))
|
.then(() => getWebLn().sendPayment(invoice))
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ import {logger, wraps, writer} from "@app/core"
|
||||||
import {PLATFORM_LOGEE} from "@app/env"
|
import {PLATFORM_LOGEE} from "@app/env"
|
||||||
|
|
||||||
const formatValue = (value: unknown) => {
|
const formatValue = (value: unknown) => {
|
||||||
if (value instanceof Error) return value.stack || value.message
|
if (value instanceof Error) {
|
||||||
if (typeof value === "string") return value
|
return value.stack || value.message
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
return tryCatch(() => JSON.stringify(value)) || String(value)
|
return tryCatch(() => JSON.stringify(value)) || String(value)
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +76,9 @@ export const sendLogs = async () => {
|
||||||
|
|
||||||
size += line.length
|
size += line.length
|
||||||
|
|
||||||
if (size > 32_000) break
|
if (size > 32_000) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
lines.unshift(line)
|
lines.unshift(line)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ export const syncChecked = () => {
|
||||||
|
|
||||||
checked.update($checked => {
|
checked.update($checked => {
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
if (defer && path === defer) continue
|
if (defer && path === defer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
$checked[path] = now()
|
$checked[path] = now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +214,9 @@ const latestContentActivity = (url: string, events: TrustedEvent[]) => {
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const target = getContentTarget(event)
|
const target = getContentTarget(event)
|
||||||
|
|
||||||
if (!target) continue
|
if (!target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const path = makeContentPath(url, target.kind, target.idOrAddress)
|
const path = makeContentPath(url, target.kind, target.idOrAddress)
|
||||||
|
|
||||||
|
|
@ -329,8 +333,12 @@ export const notifications = derived(
|
||||||
([$page, $allNotifications, $deferredRoomPath]) =>
|
([$page, $allNotifications, $deferredRoomPath]) =>
|
||||||
new Set(
|
new Set(
|
||||||
[...$allNotifications].filter(p => {
|
[...$allNotifications].filter(p => {
|
||||||
if (!$page.url.pathname.startsWith(p)) return true
|
if (!$page.url.pathname.startsWith(p)) {
|
||||||
if ($deferredRoomPath && p === $deferredRoomPath) return true
|
return true
|
||||||
|
}
|
||||||
|
if ($deferredRoomPath && p === $deferredRoomPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,12 @@ export const ingestPolicy: AppPolicy = app =>
|
||||||
const event = message[2]
|
const event = message[2]
|
||||||
const trusted = getSetting("trusted_relays").includes(socket.url)
|
const trusted = getSetting("trusted_relays").includes(socket.url)
|
||||||
|
|
||||||
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) return
|
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) {
|
||||||
if (!trusted && !verifyEvent(event)) return
|
return
|
||||||
|
}
|
||||||
|
if (!trusted && !verifyEvent(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
app.tracker.track(event.id, socket.url)
|
app.tracker.track(event.id, socket.url)
|
||||||
app.repository.publish(event)
|
app.repository.publish(event)
|
||||||
|
|
@ -62,13 +66,27 @@ export const ingestPolicy: AppPolicy = app =>
|
||||||
export const authPolicy = makeAppPolicyAuth((socket, $app) => {
|
export const authPolicy = makeAppPolicyAuth((socket, $app) => {
|
||||||
const $pubkey = app.get().user?.pubkey
|
const $pubkey = app.get().user?.pubkey
|
||||||
|
|
||||||
if (!$pubkey) return false
|
if (!$pubkey) {
|
||||||
if ($app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) return false
|
return false
|
||||||
if (getSetting("relay_auth") === RelayAuthMode.Aggressive) return true
|
}
|
||||||
if ($app.use(RoomLists).urls($pubkey).get().includes(socket.url)) return true
|
if ($app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||||
if ($app.use(RelayLists).urls($pubkey).get().includes(socket.url)) return true
|
return false
|
||||||
if (get($app.use(Thunks).history).some(t => t.options.relays.includes(socket.url))) return true
|
}
|
||||||
if ($app.use(MessagingRelayLists).urls($pubkey).get().includes(socket.url)) return true
|
if (getSetting("relay_auth") === RelayAuthMode.Aggressive) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ($app.use(RoomLists).urls($pubkey).get().includes(socket.url)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ($app.use(RelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (get($app.use(Thunks).history).some(t => t.options.relays.includes(socket.url))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ($app.use(MessagingRelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
@ -79,8 +97,12 @@ const makeBlockPolicy = ($app: IApp) => (socket: Socket) => {
|
||||||
socket.open = () => {
|
socket.open = () => {
|
||||||
const $pubkey = $app.user?.pubkey
|
const $pubkey = $app.user?.pubkey
|
||||||
|
|
||||||
if (BLOCKED_RELAYS.includes(socket.url)) return
|
if (BLOCKED_RELAYS.includes(socket.url)) {
|
||||||
if ($pubkey && $app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) return
|
return
|
||||||
|
}
|
||||||
|
if ($pubkey && $app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
previousOpen()
|
previousOpen()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ export const loadPomadeSessions = async () => {
|
||||||
const result = await client.listSessions()
|
const result = await client.listSessions()
|
||||||
|
|
||||||
for (const message of result.messages) {
|
for (const message of result.messages) {
|
||||||
if (!message.res?.items) continue
|
if (!message.res?.items) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of message.res.items) {
|
for (const item of message.res.items) {
|
||||||
const existing = sessionMap.get(item.client)
|
const existing = sessionMap.get(item.client)
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,9 @@ export const groupRoomMessages = ({
|
||||||
let previousCreatedAt = 0
|
let previousCreatedAt = 0
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (seenIds.has(event.id)) continue
|
if (seenIds.has(event.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
seenIds.add(event.id)
|
seenIds.add(event.id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,12 @@ export const deriveIsMuted = (url: string, h: string) =>
|
||||||
export const getNotifyPreference = ({alerts}: SettingsValues, url: string, h?: string) => {
|
export const getNotifyPreference = ({alerts}: SettingsValues, url: string, h?: string) => {
|
||||||
const pref = alerts.find(spec({url}))
|
const pref = alerts.find(spec({url}))
|
||||||
|
|
||||||
if (!pref) return true
|
if (!pref) {
|
||||||
if (!h) return pref.notify
|
return true
|
||||||
|
}
|
||||||
|
if (!h) {
|
||||||
|
return pref.notify
|
||||||
|
}
|
||||||
|
|
||||||
return pref.notify ? !pref.exceptions.includes(h) : pref.exceptions.includes(h)
|
return pref.notify ? !pref.exceptions.includes(h) : pref.exceptions.includes(h)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ export const signerRequests = derived(
|
||||||
const requestsById = new Map<string, SignerRequest>()
|
const requestsById = new Map<string, SignerRequest>()
|
||||||
|
|
||||||
for (const message of $messages) {
|
for (const message of $messages) {
|
||||||
if (message.source !== "signer") continue
|
if (message.source !== "signer") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (message.status === "pending") {
|
if (message.status === "pending") {
|
||||||
requestsById.set(message.id, {
|
requestsById.set(message.id, {
|
||||||
|
|
|
||||||
|
|
@ -118,19 +118,37 @@ export const isEventMuted = withGetter(
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (e: TrustedEvent) => {
|
return (e: TrustedEvent) => {
|
||||||
if (!$muteList) return false
|
if (!$muteList) {
|
||||||
if ($user.pubkey === e.pubkey) return false
|
return false
|
||||||
if (mutedPubkeys.has(e.pubkey)) return true
|
}
|
||||||
if (mutedEvents.has(e.id)) return true
|
if ($user.pubkey === e.pubkey) {
|
||||||
if (mutedAddresses.has(getAddress(e))) return true
|
return false
|
||||||
if (getParents(e).some(v => mutedEvents.has(v) || mutedAddresses.has(v))) return true
|
}
|
||||||
if (tagValues(topicTags("t"), e.tags).some(t => mutedTopics.has(t))) return true
|
if (mutedPubkeys.has(e.pubkey)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mutedEvents.has(e.id)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mutedAddresses.has(getAddress(e))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (getParents(e).some(v => mutedEvents.has(v) || mutedAddresses.has(v))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (tagValues(topicTags("t"), e.tags).some(t => mutedTopics.has(t))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (regex) {
|
if (regex) {
|
||||||
const profile = profiles.get().get(e.pubkey)
|
const profile = profiles.get().get(e.pubkey)
|
||||||
|
|
||||||
if (profile?.display().toLowerCase().match(regex)) return true
|
if (profile?.display().toLowerCase().match(regex)) {
|
||||||
if (profile?.nip05()?.match(regex)) return true
|
return true
|
||||||
|
}
|
||||||
|
if (profile?.nip05()?.match(regex)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ export const kv = call(() => {
|
||||||
|
|
||||||
const get = async <T>(key: string): Promise<T | undefined> => {
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
||||||
const result = await Preferences.get({key})
|
const result = await Preferences.get({key})
|
||||||
if (!result.value) return undefined
|
if (!result.value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(result.value)
|
return JSON.parse(result.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -91,7 +93,9 @@ export const ss = call(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) return undefined
|
if (!value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(value)
|
return JSON.parse(value)
|
||||||
|
|
@ -402,7 +406,9 @@ class Storage {
|
||||||
)
|
)
|
||||||
|
|
||||||
return this.app.use(Relays).onItem((_url, relay) => {
|
return this.app.use(Relays).onItem((_url, relay) => {
|
||||||
if (relay) enqueue(relay)
|
if (relay) {
|
||||||
|
enqueue(relay)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,7 +422,9 @@ class Storage {
|
||||||
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
||||||
|
|
||||||
return this.app.use(RelayStats).onItem((_url, stats) => {
|
return this.app.use(RelayStats).onItem((_url, stats) => {
|
||||||
if (stats) enqueue(stats)
|
if (stats) {
|
||||||
|
enqueue(stats)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,7 +438,9 @@ class Storage {
|
||||||
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
||||||
|
|
||||||
return this.app.use(Handles).onItem((_nip05, handle) => {
|
return this.app.use(Handles).onItem((_nip05, handle) => {
|
||||||
if (handle) enqueue(handle)
|
if (handle) {
|
||||||
|
enqueue(handle)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,7 +461,9 @@ class Storage {
|
||||||
)
|
)
|
||||||
|
|
||||||
return this.app.use(Zappers).onItem((_lnurl, zapper) => {
|
return this.app.use(Zappers).onItem((_lnurl, zapper) => {
|
||||||
if (zapper) enqueue(zapper)
|
if (zapper) {
|
||||||
|
enqueue(zapper)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,7 +477,9 @@ class Storage {
|
||||||
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
||||||
|
|
||||||
return this.app.use(Plaintext).onItem((key, value) => {
|
return this.app.use(Plaintext).onItem((key, value) => {
|
||||||
if (value) enqueue({key, value})
|
if (value) {
|
||||||
|
enqueue({key, value})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@ type SyncOpts = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullAndListen = ({url, signal, filters}: SyncOpts) => {
|
const pullAndListen = ({url, signal, filters}: SyncOpts) => {
|
||||||
if (signal.aborted) return
|
if (signal.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
app
|
app
|
||||||
.get()
|
.get()
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@
|
||||||
// When no date is provided the library initializes its time input from the
|
// When no date is provided the library initializes its time input from the
|
||||||
// epoch (a locale-dependent afternoon); reset it to something friendlier
|
// epoch (a locale-dependent afternoon); reset it to something friendlier
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!startDate) startDateTime = "12:00"
|
if (!startDate) {
|
||||||
|
startDateTime = "12:00"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,12 @@
|
||||||
// When no date is provided the library initializes its time inputs from the
|
// When no date is provided the library initializes its time inputs from the
|
||||||
// epoch (a locale-dependent afternoon); reset them for the create flow
|
// epoch (a locale-dependent afternoon); reset them for the create flow
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!startDate) startDateTime = "12:00"
|
if (!startDate) {
|
||||||
if (!endDate) endDateTime = "13:00"
|
startDateTime = "12:00"
|
||||||
|
}
|
||||||
|
if (!endDate) {
|
||||||
|
endDateTime = "13:00"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
|
|
||||||
if (isWebkit && isRemoteSvg(url)) {
|
if (isWebkit && isRemoteSvg(url)) {
|
||||||
fetchSvg(url).then(blobUrl => {
|
fetchSvg(url).then(blobUrl => {
|
||||||
if (!blobUrl) return
|
if (!blobUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
URL.revokeObjectURL(blobUrl)
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@
|
||||||
const id = randomId()
|
const id = randomId()
|
||||||
|
|
||||||
const getImageUrl = (item: string | File): string => {
|
const getImageUrl = (item: string | File): string => {
|
||||||
if (typeof item === "string") return item
|
if (typeof item === "string") {
|
||||||
|
return item
|
||||||
|
}
|
||||||
return URL.createObjectURL(item)
|
return URL.createObjectURL(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ export {preventDefault, stopPropagation} from "svelte/legacy"
|
||||||
export const documentActive = readable(
|
export const documentActive = readable(
|
||||||
typeof document === "undefined" ? true : !document.hidden && document.hasFocus(),
|
typeof document === "undefined" ? true : !document.hidden && document.hasFocus(),
|
||||||
set => {
|
set => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const update = () => set(!document.hidden && document.hasFocus())
|
const update = () => set(!document.hidden && document.hasFocus())
|
||||||
|
|
||||||
|
|
@ -36,7 +38,9 @@ export const anchorDatepicker = (wrapper: HTMLElement) => {
|
||||||
const anchor = wrapper.querySelector("label")
|
const anchor = wrapper.querySelector("label")
|
||||||
const container = wrapper.querySelector(".calendars-container") as HTMLElement | null
|
const container = wrapper.querySelector(".calendars-container") as HTMLElement | null
|
||||||
|
|
||||||
if (!anchor || !container) return
|
if (!anchor || !container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const margin = 8
|
const margin = 8
|
||||||
const rect = anchor.getBoundingClientRect()
|
const rect = anchor.getBoundingClientRect()
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,9 @@ export class IDB {
|
||||||
getAll = async <T>(table: string): Promise<T[]> => {
|
getAll = async <T>(table: string): Promise<T[]> => {
|
||||||
const connection = await this.live()
|
const connection = await this.live()
|
||||||
|
|
||||||
if (!connection) return []
|
if (!connection) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const tx = connection.transaction(table, "readonly")
|
const tx = connection.transaction(table, "readonly")
|
||||||
const store = tx.objectStore(table)
|
const store = tx.objectStore(table)
|
||||||
|
|
@ -100,7 +102,9 @@ export class IDB {
|
||||||
bulkPut = async <T>(table: string, data: Iterable<T>) => {
|
bulkPut = async <T>(table: string, data: Iterable<T>) => {
|
||||||
const connection = await this.live()
|
const connection = await this.live()
|
||||||
|
|
||||||
if (!connection) return
|
if (!connection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const tx = connection.transaction(table, "readwrite")
|
const tx = connection.transaction(table, "readwrite")
|
||||||
const store = tx.objectStore(table)
|
const store = tx.objectStore(table)
|
||||||
|
|
@ -121,7 +125,9 @@ export class IDB {
|
||||||
bulkDelete = async (table: string, ids: Iterable<string>) => {
|
bulkDelete = async (table: string, ids: Iterable<string>) => {
|
||||||
const connection = await this.live()
|
const connection = await this.live()
|
||||||
|
|
||||||
if (!connection) return
|
if (!connection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const tx = connection.transaction(table, "readwrite")
|
const tx = connection.transaction(table, "readwrite")
|
||||||
const store = tx.objectStore(table)
|
const store = tx.objectStore(table)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ export const nsecEncode = (secret: string) => nip19.nsecEncode(hexToBytes(secret
|
||||||
export const nsecDecode = (nsec: string) => {
|
export const nsecDecode = (nsec: string) => {
|
||||||
const {type, data} = nip19.decode(nsec)
|
const {type, data} = nip19.decode(nsec)
|
||||||
|
|
||||||
if (type !== "nsec") throw new Error(`Invalid nsec: ${nsec}`)
|
if (type !== "nsec") {
|
||||||
|
throw new Error(`Invalid nsec: ${nsec}`)
|
||||||
|
}
|
||||||
|
|
||||||
return bytesToHex(data)
|
return bytesToHex(data)
|
||||||
}
|
}
|
||||||
|
|
@ -58,12 +60,17 @@ export class TimeoutError extends Error {
|
||||||
|
|
||||||
/** Returns a promise that rejects with AbortError when signal aborts. Use with Promise.race. */
|
/** Returns a promise that rejects with AbortError when signal aborts. Use with Promise.race. */
|
||||||
export const whenAborted = (signal?: AbortSignal) => {
|
export const whenAborted = (signal?: AbortSignal) => {
|
||||||
if (!signal) return new Promise<never>(() => {})
|
if (!signal) {
|
||||||
|
return new Promise<never>(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise<never>((_, reject) => {
|
return new Promise<never>((_, reject) => {
|
||||||
const onAborted = () => reject(new AbortError())
|
const onAborted = () => reject(new AbortError())
|
||||||
if (signal.aborted) onAborted()
|
if (signal.aborted) {
|
||||||
else signal.addEventListener("abort", onAborted, {once: true})
|
onAborted()
|
||||||
|
} else {
|
||||||
|
signal.addEventListener("abort", onAborted, {once: true})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,9 @@
|
||||||
// Listen for signer errors, report to user via toast
|
// Listen for signer errors, report to user via toast
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
throttled(10_000, signerRequests).subscribe($requests => {
|
throttled(10_000, signerRequests).subscribe($requests => {
|
||||||
if ($toast) return
|
if ($toast) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const longCutoff = Date.now() - 30_000
|
const longCutoff = Date.now() - 30_000
|
||||||
const shortCutoff = Date.now() - 10_000
|
const shortCutoff = Date.now() - 10_000
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,15 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = $derived.by(() => {
|
const status = $derived.by(() => {
|
||||||
if (!tenant) return "inactive"
|
if (!tenant) {
|
||||||
if (tenant.churned_at) return "delinquent"
|
return "inactive"
|
||||||
if (hasPaidSubscription || openInvoice || autopayConfigured(tenant)) return "active"
|
}
|
||||||
|
if (tenant.churned_at) {
|
||||||
|
return "delinquent"
|
||||||
|
}
|
||||||
|
if (hasPaidSubscription || openInvoice || autopayConfigured(tenant)) {
|
||||||
|
return "active"
|
||||||
|
}
|
||||||
return "inactive"
|
return "inactive"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -77,15 +83,23 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
const nwc = $derived.by<PaymentMethodState>(() => {
|
const nwc = $derived.by<PaymentMethodState>(() => {
|
||||||
if (!tenant?.nwc_is_set) return {kind: "not_set_up"}
|
if (!tenant?.nwc_is_set) {
|
||||||
if (tenant.nwc_error) return {kind: "error", message: tenant.nwc_error}
|
return {kind: "not_set_up"}
|
||||||
|
}
|
||||||
|
if (tenant.nwc_error) {
|
||||||
|
return {kind: "error", message: tenant.nwc_error}
|
||||||
|
}
|
||||||
return {kind: "ok"}
|
return {kind: "ok"}
|
||||||
})
|
})
|
||||||
|
|
||||||
const card = $derived.by<PaymentMethodState>(() => {
|
const card = $derived.by<PaymentMethodState>(() => {
|
||||||
if (!tenant?.stripe_payment_method_id) return {kind: "not_set_up"}
|
if (!tenant?.stripe_payment_method_id) {
|
||||||
|
return {kind: "not_set_up"}
|
||||||
|
}
|
||||||
// Don't surface Stripe's raw decline text to the tenant.
|
// Don't surface Stripe's raw decline text to the tenant.
|
||||||
if (tenant.stripe_error) return {kind: "error", message: "Payment failed"}
|
if (tenant.stripe_error) {
|
||||||
|
return {kind: "error", message: "Payment failed"}
|
||||||
|
}
|
||||||
return {kind: "ok"}
|
return {kind: "ok"}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -95,7 +109,9 @@
|
||||||
|
|
||||||
const refetch = async () => {
|
const refetch = async () => {
|
||||||
const pk = user.get().pubkey
|
const pk = user.get().pubkey
|
||||||
if (!pk) return
|
if (!pk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
getTenant(pk),
|
getTenant(pk),
|
||||||
|
|
@ -106,10 +122,18 @@
|
||||||
|
|
||||||
const [t, inv, rel, draft] = results
|
const [t, inv, rel, draft] = results
|
||||||
|
|
||||||
if (t.status === "fulfilled") tenant = t.value
|
if (t.status === "fulfilled") {
|
||||||
if (inv.status === "fulfilled") invoices = sortBy(invoice => -invoice.created_at, inv.value)
|
tenant = t.value
|
||||||
if (rel.status === "fulfilled") relays = rel.value
|
}
|
||||||
if (draft.status === "fulfilled") draftInvoice = draft.value
|
if (inv.status === "fulfilled") {
|
||||||
|
invoices = sortBy(invoice => -invoice.created_at, inv.value)
|
||||||
|
}
|
||||||
|
if (rel.status === "fulfilled") {
|
||||||
|
relays = rel.value
|
||||||
|
}
|
||||||
|
if (draft.status === "fulfilled") {
|
||||||
|
draftInvoice = draft.value
|
||||||
|
}
|
||||||
|
|
||||||
if (results.some(spec({status: "rejected"}))) {
|
if (results.some(spec({status: "rejected"}))) {
|
||||||
pushToast({theme: "error", message: "Failed to refresh billing data"})
|
pushToast({theme: "error", message: "Failed to refresh billing data"})
|
||||||
|
|
@ -127,7 +151,9 @@
|
||||||
// invoices is left to the backend's dunning poll, which collects all of them.
|
// invoices is left to the backend's dunning poll, which collects all of them.
|
||||||
const reconcile = async () => {
|
const reconcile = async () => {
|
||||||
const pk = user.get().pubkey
|
const pk = user.get().pubkey
|
||||||
if (!pk || reconciling) return
|
if (!pk || reconciling) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
reconciling = true
|
reconciling = true
|
||||||
error = ""
|
error = ""
|
||||||
|
|
@ -178,7 +204,9 @@
|
||||||
// Re-run on foreground return (native browser round-trip) to pick up a
|
// Re-run on foreground return (native browser round-trip) to pick up a
|
||||||
// completed checkout or a portal-added card.
|
// completed checkout or a portal-added card.
|
||||||
const resumeListener = App.addListener("appStateChange", ({isActive}) => {
|
const resumeListener = App.addListener("appStateChange", ({isActive}) => {
|
||||||
if (isActive) void reconcile()
|
if (isActive) {
|
||||||
|
void reconcile()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,9 @@
|
||||||
return createSearch(options, {
|
return createSearch(options, {
|
||||||
getValue: (relay: Relay) => relay.url,
|
getValue: (relay: Relay) => relay.url,
|
||||||
sortFn: ({score, item}) => {
|
sortFn: ({score, item}) => {
|
||||||
if (score && score > 0.1) return -score!
|
if (score && score > 0.1) {
|
||||||
|
return -score!
|
||||||
|
}
|
||||||
|
|
||||||
const wotScore = $spacePubkeysByUrl.get(item.url)?.size || 0
|
const wotScore = $spacePubkeysByUrl.get(item.url)?.size || 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@
|
||||||
// Watch for relay errors and notify the user
|
// Watch for relay errors and notify the user
|
||||||
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (getModal()) return
|
if (getModal()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (redirectUrl && redirectUrl !== url && !redirectPrompted.has(url)) {
|
if (redirectUrl && redirectUrl !== url && !redirectPrompted.has(url)) {
|
||||||
redirectPrompted.add(url)
|
redirectPrompted.add(url)
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading || uploadingCover || loading) return
|
if ($uploading || uploadingCover || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,15 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "ArrowLeft") showPrevious()
|
if (event.key === "ArrowLeft") {
|
||||||
if (event.key === "ArrowRight") showNext()
|
showPrevious()
|
||||||
if (event.key === "t") showToday()
|
}
|
||||||
|
if (event.key === "ArrowRight") {
|
||||||
|
showNext()
|
||||||
|
}
|
||||||
|
if (event.key === "t") {
|
||||||
|
showToday()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewClass = (target: CalendarView) =>
|
const viewClass = (target: CalendarView) =>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,9 @@
|
||||||
const items = $derived.by(() => {
|
const items = $derived.by(() => {
|
||||||
const matches = scoped.filter(event => tab === "all" || getStatus(event) === tab)
|
const matches = scoped.filter(event => tab === "all" || getStatus(event) === tab)
|
||||||
|
|
||||||
if (sort === "new") return sortBy(event => -event.created_at, matches)
|
if (sort === "new") {
|
||||||
|
return sortBy(event => -event.created_at, matches)
|
||||||
|
}
|
||||||
|
|
||||||
if (sort === "price-asc") {
|
if (sort === "price-asc") {
|
||||||
return sortBy(event => reader(Classified)(event).price()?.amount ?? Infinity, matches)
|
return sortBy(event => reader(Classified)(event).price()?.amount ?? Infinity, matches)
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,9 @@
|
||||||
const visibleMembers = $derived.by(() => {
|
const visibleMembers = $derived.by(() => {
|
||||||
const t = term.trim().toLowerCase()
|
const t = term.trim().toLowerCase()
|
||||||
|
|
||||||
if (!t) return $memberList
|
if (!t) {
|
||||||
|
return $memberList
|
||||||
|
}
|
||||||
|
|
||||||
return $memberList.filter(
|
return $memberList.filter(
|
||||||
({pubkey, roleList}) =>
|
({pubkey, roleList}) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue