Merge branch 'dev' into feature/59-space-search
This commit is contained in:
+1
-1
@@ -9,4 +9,4 @@ build
|
||||
|
||||
# Env files (keep .env for build; exclude local overrides)
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*.local
|
||||
|
||||
@@ -5,7 +5,6 @@ VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
# Can be a local path (e.g. static/logo.png) or a full https URL
|
||||
VITE_PLATFORM_LOGO=static/logo.png
|
||||
VITE_PLATFORM_RELAYS=
|
||||
VITE_PLATFORM_ACCENT="#7161FF"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
## Project Overview
|
||||
|
||||
Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-29 (relay-based groups) to create Discord-like spaces (servers) and rooms (channels).
|
||||
|
||||
Please visit our [issue tracker](https://gitea.coracle.social/coracle/flotilla/issues) to contribute. Any new issues should be opened without a milestone, label, or project and the project owners will triage them.
|
||||
|
||||
### Milestones
|
||||
|
||||
Milestones indicate how soon a given task should be tackled.
|
||||
|
||||
- [Current](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=1) issues are immediately actionable.
|
||||
- [Next](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=2) means an issue is blocked.
|
||||
- [Future](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=3) means we're deferring work until a later date.
|
||||
|
||||
### Labels
|
||||
|
||||
- [Design](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=15) issues need design work before being implemented. This might take the form of a high-quality mockup, wireframes, user flows, or just a couple notes about where things go, depending on the nature of the task.
|
||||
- [Dev](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=16) issues are ready to be implemented. Most of the work will be related to architecting and writing code.
|
||||
- [Easy](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=14) issues have no dependencies, and are scoped quite narrowly.
|
||||
- [Priority](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=6) issues include bugs and urgent feature requests. These should get attention first if possible, although sometimes long-standing performance issues or subtle bugs might end up here for a while.
|
||||
- [Ideas](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=13) are for things that aren't scoped out yet, or which need protocol work before getting designed or implemented.
|
||||
|
||||
### Projects
|
||||
|
||||
Issues may or may not have a project. Projects are used to group issues thematically just for organization.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
There are a few conventions that are helpful to know right out of the gate.
|
||||
|
||||
- Most nostr protocol functionality is implemented via the [welshman library](https://welshman.coracle.social/)
|
||||
- Use Svelte 4 **stores** rather than runes for all state outside UI components
|
||||
- Most global state flows through Welshman's `repository` (unidirectional)
|
||||
- Query state using `deriveEventsMapped` or `deriveProfile` etc
|
||||
- Events are published via `publishThunk`, which allows for optimistic UI updates during signing/pow generation.
|
||||
- Components should have minimal props - e.g. instead of passing a whole `relay` through, pass its `url`.
|
||||
- Use `AbortController` when possible instead of request ids
|
||||
- Use `undefined` or optional properties instead of `null`
|
||||
- Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect.
|
||||
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||
- When dynamically building classes, use `cx` from `classnames`.
|
||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates.
|
||||
|
||||
## Contributing Workflow
|
||||
|
||||
To contribute, do the following:
|
||||
|
||||
- Find or create an issue and assign yourself (comment instead if you're not able to self-assign)
|
||||
- If the issue is a design task, attach or link out to any mockups/wireframes/flowcharts
|
||||
- Once a design task is completed, a maintainer will remove the `design` label and add the `dev` label
|
||||
- If the issue is a development task, fork the repository and create a branch prefixed by the issue number, e.g. `105-deep-links`
|
||||
- Before requesting a review, be sure to review any agent-generated code, run the pre-commit hooks, and test the changes.
|
||||
- Open a PR and request a review. A maintainer will get back to you with requested changes, or will merge the PR.
|
||||
- Keep your PR up-to-date by rebasing frequently on `dev`. Avoid force-pushing to `dev`.
|
||||
- PRs are rebased, squashed, and merged to keep commit history simple.
|
||||
- An issue may have multiple PRs. Once complete, it can be closed.
|
||||
@@ -16,11 +16,13 @@ You can also optionally create an `.env.local` file and populate it with the fol
|
||||
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
|
||||
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
|
||||
|
||||
These values **won't** be used for a built version. Instead, env variables should be provided to `build.sh` directly or to the built container.
|
||||
|
||||
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](AGENTS.md).
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
temp_env=$(declare -p -x)
|
||||
|
||||
if [ -f .env.template ]; then
|
||||
source .env.template
|
||||
fi
|
||||
if [ -f .env.local ]; then
|
||||
source .env.local
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Avoid overwriting env vars provided directly
|
||||
|
||||
@@ -2,7 +2,7 @@ import dotenv from "dotenv"
|
||||
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
||||
|
||||
dotenv.config({path: ".env.local"})
|
||||
dotenv.config({path: ".env.template"})
|
||||
dotenv.config({path: ".env"})
|
||||
|
||||
export default defineConfig({
|
||||
preset,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {goToChat} from "@app/util/routes"
|
||||
import {goToChat, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
@@ -26,7 +26,9 @@
|
||||
|
||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||
|
||||
const anySpaceNotifications = $derived($userSpaceUrls.some(p => $notifications.has(p)))
|
||||
const anySpaceNotifications = $derived(
|
||||
$userSpaceUrls.some(p => $notifications.has(makeSpacePath(p))),
|
||||
)
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="ellipsize whitespace-nowrap text-xl">
|
||||
<RelayName {url} />
|
||||
</h2>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {tick} from "svelte"
|
||||
import {createSearch} from "@welshman/app"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {request} from "@welshman/net"
|
||||
import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {MESSAGE} from "@welshman/util"
|
||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||
import {sortEventsDesc} from "@welshman/util"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {deriveEventsForUrl} from "@app/core/state"
|
||||
import {CONTENT_KINDS} from "@app/core/state"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -19,14 +20,16 @@
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const spaceMessages = deriveEventsForUrl(
|
||||
url,
|
||||
h ? [{kinds: [MESSAGE], "#h": [h]}] : [{kinds: [MESSAGE]}],
|
||||
)
|
||||
|
||||
let term = $state("")
|
||||
let show = $state(false)
|
||||
let results = $state<TrustedEvent[]>([])
|
||||
let loading = $state(false)
|
||||
let input: HTMLInputElement | undefined = $state()
|
||||
let controller: AbortController | undefined
|
||||
|
||||
const relayStatus = $derived(
|
||||
h ? `Searching this room on relay: ${url}.` : `Searching this space on relay: ${url}.`,
|
||||
)
|
||||
|
||||
const open = () => {
|
||||
show = true
|
||||
@@ -40,21 +43,53 @@
|
||||
const clear = () => {
|
||||
term = ""
|
||||
show = false
|
||||
loading = false
|
||||
results = []
|
||||
controller?.abort()
|
||||
controller = undefined
|
||||
}
|
||||
|
||||
const getRelayUrls = () => [url]
|
||||
|
||||
const getFilter = (searchTerm: string): Filter =>
|
||||
h
|
||||
? {kinds: CONTENT_KINDS, "#h": [h], search: searchTerm}
|
||||
: {kinds: CONTENT_KINDS, search: searchTerm}
|
||||
|
||||
const search = debounce(300, async (searchTerm: string) => {
|
||||
controller?.abort()
|
||||
|
||||
if (!searchTerm.trim()) {
|
||||
loading = false
|
||||
results = []
|
||||
return
|
||||
}
|
||||
|
||||
controller = new AbortController()
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const events = await request({
|
||||
relays: getRelayUrls(),
|
||||
autoClose: true,
|
||||
signal: controller.signal,
|
||||
filters: [getFilter(searchTerm.trim())],
|
||||
})
|
||||
|
||||
results = sortEventsDesc(events)
|
||||
} catch (error) {
|
||||
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||
results = []
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
})
|
||||
|
||||
const onInput = () => {
|
||||
show = true
|
||||
void search(term)
|
||||
}
|
||||
|
||||
const searchIndex = $derived.by(() =>
|
||||
createSearch($spaceMessages, {
|
||||
getValue: event => event.id,
|
||||
fuseOptions: {keys: ["content"]},
|
||||
}),
|
||||
)
|
||||
|
||||
const results = $derived(term ? searchIndex.searchOptions(term) : [])
|
||||
|
||||
const eventsByAge = $derived(groupBy(e => getAgeSection(e.created_at), results))
|
||||
|
||||
const getAgeSection = (createdAt: number) => {
|
||||
@@ -122,10 +157,13 @@
|
||||
oninput={onInput} />
|
||||
</label>
|
||||
<div class="max-h-[65vh] overflow-y-auto">
|
||||
<p class="mb-2 text-xs opacity-70">{relayStatus}</p>
|
||||
{#if !term}
|
||||
<p class="text-sm opacity-70">
|
||||
{h ? "Search for messages in this room." : "Search for messages across this space."}
|
||||
{h ? "Search for content in this room." : "Search for content in this space."}
|
||||
</p>
|
||||
{:else if loading}
|
||||
<p class="text-sm opacity-70">Searching...</p>
|
||||
{:else if eventsByAge.size === 0}
|
||||
<p class="text-sm opacity-70">No results found.</p>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script lang="ts">
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {
|
||||
currentVoiceSession,
|
||||
DeviceKind,
|
||||
supportsAudioOutputSelection,
|
||||
switchVoiceActiveDevice,
|
||||
type VoiceSession,
|
||||
} from "@app/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
|
||||
const selectValueForActiveDevice = (session: VoiceSession, kind: DeviceKind): string => {
|
||||
const livekitDeviceId = session.room.getActiveDevice(kind)
|
||||
if (livekitDeviceId === undefined || livekitDeviceId === "" || livekitDeviceId === "default") {
|
||||
return ""
|
||||
}
|
||||
return livekitDeviceId
|
||||
}
|
||||
|
||||
let audioInputs = $state<MediaDeviceInfo[]>([])
|
||||
let audioOutputs = $state<MediaDeviceInfo[]>([])
|
||||
let selectedInput = $state("")
|
||||
let selectedOutput = $state("")
|
||||
|
||||
const loadDevices = async () => {
|
||||
if (!navigator.mediaDevices?.enumerateDevices) return
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
audioInputs = devices.filter(d => d.kind === "audioinput")
|
||||
audioOutputs = devices.filter(d => d.kind === "audiooutput")
|
||||
} catch {
|
||||
audioInputs = []
|
||||
audioOutputs = []
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadDevices()
|
||||
navigator.mediaDevices?.addEventListener?.("devicechange", loadDevices)
|
||||
return () => navigator.mediaDevices?.removeEventListener?.("devicechange", loadDevices)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const session = $currentVoiceSession
|
||||
if (!session) {
|
||||
popModal()
|
||||
return
|
||||
}
|
||||
selectedInput = selectValueForActiveDevice(session, DeviceKind.AudioInput)
|
||||
selectedOutput = selectValueForActiveDevice(session, DeviceKind.AudioOutput)
|
||||
})
|
||||
|
||||
const onInputChange = () => {
|
||||
void switchVoiceActiveDevice(DeviceKind.AudioInput, selectedInput)
|
||||
}
|
||||
|
||||
const onOutputChange = () => {
|
||||
void switchVoiceActiveDevice(DeviceKind.AudioOutput, selectedOutput)
|
||||
}
|
||||
|
||||
const onDone = () => {
|
||||
popModal()
|
||||
}
|
||||
|
||||
// Output not support in Safari
|
||||
const canPickOutput = supportsAudioOutputSelection()
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Audio settings</ModalTitle>
|
||||
<ModalSubtitle>Choose microphone and speaker for this call.</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-4 pt-2">
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Microphone</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
bind:value={selectedInput}
|
||||
onchange={onInputChange}
|
||||
aria-label="Microphone">
|
||||
<option value="">Default microphone</option>
|
||||
{#each audioInputs as d (d.deviceId)}
|
||||
<option value={d.deviceId}>
|
||||
{d.label || `Microphone ${d.deviceId.slice(0, 8)}…`}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{#if canPickOutput}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Speaker</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
bind:value={selectedOutput}
|
||||
onchange={onOutputChange}
|
||||
aria-label="Speaker">
|
||||
<option value="">Default speaker</option>
|
||||
{#each audioOutputs as d (d.deviceId)}
|
||||
<option value={d.deviceId}>
|
||||
{d.label || `Speaker ${d.deviceId.slice(0, 8)}…`}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-primary w-full" onclick={onDone}>Done</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
@@ -12,9 +12,11 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {AbortError, TimeoutError} from "$lib/util"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
import {joinVoiceRoom} from "@app/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -45,6 +47,16 @@
|
||||
|
||||
const goBack = () => history.back()
|
||||
|
||||
const handleJoinError = (e: unknown) => {
|
||||
if (e instanceof AbortError) return
|
||||
console.error("Failed to join voice room", e)
|
||||
let message = "Failed to join voice room"
|
||||
if (e instanceof TimeoutError)
|
||||
message = "Connection timed out. Please check your network and try again."
|
||||
else if (e instanceof Error) message = e.message
|
||||
pushToast({theme: "error", message})
|
||||
}
|
||||
|
||||
const joinVoice = async () => {
|
||||
popModal()
|
||||
await joinVoiceRoom(
|
||||
@@ -52,7 +64,7 @@
|
||||
h,
|
||||
startWithoutMic,
|
||||
startWithoutMic ? undefined : selectedDeviceId || undefined,
|
||||
)
|
||||
).catch(handleJoinError)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
||||
import PhoneCallingRounded from "@assets/icons/phone-calling-rounded.svg?dataurl"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import VoiceCallAudioSettingsDialog from "@app/components/VoiceCallAudioSettingsDialog.svelte"
|
||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||
import {
|
||||
decodeRelay,
|
||||
@@ -63,6 +65,10 @@
|
||||
await goto(makeRoomPath(targetRoom.url, targetRoom.h))
|
||||
pushModal(VoiceRoomJoinDialog, {url: targetRoom.url, h: targetRoom.h})
|
||||
}
|
||||
|
||||
const openAudioSettings = () => {
|
||||
pushModal(VoiceCallAudioSettingsDialog)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if targetRoom}
|
||||
@@ -100,6 +106,12 @@
|
||||
onclick={toggleMute}>
|
||||
<Icon icon={$currentVoiceSession.muted ? MicrophoneOff : Microphone} size={4} />
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Audio settings"
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
|
||||
onclick={openAudioSettings}>
|
||||
<Icon icon={Settings} size={4} />
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Leave room"
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-error"
|
||||
|
||||
@@ -193,7 +193,7 @@ export const PLATFORM_PRIVACY = import.meta.env.VITE_PLATFORM_PRIVACY
|
||||
|
||||
export const PLATFORM_LOGO = import.meta.env.PROD
|
||||
? PLATFORM_URL + "/logo.png"
|
||||
: import.meta.env.VITE_PLATFORM_LOGO || PLATFORM_URL + "/logo.png"
|
||||
: import.meta.env.VITE_PLATFORM_LOGO.replace(/^static/, "") || PLATFORM_URL + "/logo.png"
|
||||
|
||||
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME
|
||||
|
||||
|
||||
@@ -298,10 +298,7 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: relayKinds},
|
||||
{kinds: roomMetaKinds},
|
||||
{kinds: roomMemberKinds},
|
||||
{kinds: MESSAGE_KINDS, since},
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...MESSAGE_KINDS]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||
],
|
||||
onEvent: event => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Room as LiveKitRoom,
|
||||
RoomEvent,
|
||||
Track,
|
||||
supportsAudioOutputSelection,
|
||||
type AudioCaptureOptions,
|
||||
type LocalParticipant,
|
||||
} from "livekit-client"
|
||||
@@ -24,6 +25,8 @@ export const LIVEKIT_PARTICIPANTS = 39004
|
||||
|
||||
export {checkRelayHasLivekit} from "$lib/livekit"
|
||||
|
||||
export {supportsAudioOutputSelection}
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
@@ -43,6 +46,36 @@ export enum VoiceState {
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
const LIVEKIT_DEFAULT_DEVICE_ID = "default"
|
||||
|
||||
export enum DeviceKind {
|
||||
AudioInput = "audioinput",
|
||||
AudioOutput = "audiooutput",
|
||||
}
|
||||
|
||||
export const switchVoiceActiveDevice = async (
|
||||
kind: DeviceKind,
|
||||
targetDeviceId: string,
|
||||
): Promise<void> => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
const id = targetDeviceId === "" ? LIVEKIT_DEFAULT_DEVICE_ID : targetDeviceId
|
||||
try {
|
||||
await session.room.switchActiveDevice(kind, id)
|
||||
} catch {
|
||||
let label: string
|
||||
switch (kind) {
|
||||
case DeviceKind.AudioInput:
|
||||
label = "microphone"
|
||||
break
|
||||
case DeviceKind.AudioOutput:
|
||||
label = "speaker"
|
||||
break
|
||||
}
|
||||
pushToast({theme: "error", message: `Error changing ${label}`})
|
||||
}
|
||||
}
|
||||
|
||||
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||
|
||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
} from "@app/core/state"
|
||||
import {setSpaceMembershipOrder} from "@app/core/commands"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {goToSpace} from "@app/util/routes"
|
||||
import {goToSpace, makeSpacePath} from "@app/util/routes"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
|
||||
@@ -254,9 +255,12 @@
|
||||
ondrop={e => onDrop(e, url)}
|
||||
ondragend={onDragEnd}>
|
||||
<Button
|
||||
class="card2 bg-alt shadow-md transition-all hover:shadow-lg hover:dark:brightness-[1.1] w-full"
|
||||
class="card2 bg-alt shadow-md transition-all hover:shadow-lg hover:dark:brightness-[1.1] w-full relative"
|
||||
onclick={() => openSpace(url)}>
|
||||
<RelaySummary hideFavorites {url} />
|
||||
{#if $notifications.has(makeSpacePath(url))}
|
||||
<div class="absolute right-3 top-3 h-2 w-2 rounded-full bg-primary"></div>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import daisyui from "daisyui"
|
||||
import themes from "daisyui/src/theming/themes"
|
||||
|
||||
config({path: ".env.local"})
|
||||
config({path: ".env.template"})
|
||||
config({path: ".env"})
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {sveltekit} from "@sveltejs/kit/vite"
|
||||
import svg from "@poppanator/sveltekit-svg"
|
||||
|
||||
config({path: ".env.local"})
|
||||
config({path: ".env.template"})
|
||||
config({path: ".env"})
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user