Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c737b26c0 | |||
| 6dbe9c0ebb | |||
| 45df132dc6 | |||
| c42a285f0b | |||
| 1e3211ae74 | |||
| ec507b05d6 | |||
| 339bb1afac | |||
| c441012e02 | |||
| 0d61278c56 |
@@ -1,8 +1,13 @@
|
|||||||
name: Docker
|
name: Container Image Build and Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea.coracle.social
|
REGISTRY: gitea.coracle.social
|
||||||
@@ -32,6 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
type=sha
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -45,6 +51,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
target: production
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -27,13 +27,10 @@ android/app/src/main/assets/public/
|
|||||||
node_modules/
|
node_modules/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
build/
|
build/
|
||||||
|
build-server/
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
.next/
|
.next/
|
||||||
|
|
||||||
# Rust/Tauri
|
|
||||||
*target/
|
|
||||||
src-tauri/binaries/
|
|
||||||
|
|
||||||
# iOS
|
# iOS
|
||||||
ios/App/App/public
|
ios/App/App/public
|
||||||
ios/DerivedData
|
ios/DerivedData
|
||||||
|
|||||||
@@ -6,22 +6,26 @@
|
|||||||
# Pass --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) to stamp the build.
|
# Pass --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) to stamp the build.
|
||||||
# A .env in the build context is picked up by build.sh for branding config.
|
# A .env in the build context is picked up by build.sh for branding config.
|
||||||
|
|
||||||
FROM node:22-bookworm
|
# https://pnpm.io/docker#example-3-build-on-cicd
|
||||||
|
FROM node:24-slim AS builder
|
||||||
RUN npm install -g pnpm@10.33.0
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
|
|
||||||
RUN pnpm i --frozen-lockfile
|
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG VITE_BUILD_HASH
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
RUN pnpm run build:server
|
||||||
|
|
||||||
|
FROM node:24-slim AS production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/build /app/build
|
||||||
|
COPY --from=builder /app/build-server/server.js /app/server.js
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
USER node
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ include ':capawesome-capacitor-badge'
|
|||||||
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
||||||
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
|
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
|
||||||
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Flotilla Chat' do
|
target 'Flotilla Chat' do
|
||||||
|
|||||||
@@ -5,12 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
|
"build:server": "vite build --config vite.config.server.ts",
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"tauri:dev": "tauri dev",
|
|
||||||
"tauri:build": "tauri build",
|
|
||||||
"tauri:info": "tauri info",
|
|
||||||
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
|
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
@@ -21,10 +18,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@sveltejs/kit": "^2.50.1",
|
"@sveltejs/kit": "^2.61.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -32,15 +28,15 @@
|
|||||||
"eslint-config-prettier": "^9.1.2",
|
"eslint-config-prettier": "^9.1.2",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.15",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
"svelte": "^5.48.0",
|
"svelte": "^5.55.9",
|
||||||
"svelte-check": "^4.3.5",
|
"svelte-check": "^4.3.5",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.53.1",
|
"typescript-eslint": "^8.53.1",
|
||||||
"vite": "^5.4.21"
|
"vite": "^6.4.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -64,14 +60,14 @@
|
|||||||
"@hono/node-server": "^2.0.0",
|
"@hono/node-server": "^2.0.0",
|
||||||
"@noble/curves": "^1.9.7",
|
"@noble/curves": "^1.9.7",
|
||||||
"@pomade/core": "^0.2.3",
|
"@pomade/core": "^0.2.3",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^7.0.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
"@tiptap/pm": "^2.27.2",
|
"@tiptap/pm": "^2.27.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vite-pwa/sveltekit": "^0.6.8",
|
"@vite-pwa/sveltekit": "^1.1.0",
|
||||||
"@welshman/app": "^0.8.15",
|
"@welshman/app": "^0.8.15",
|
||||||
"@welshman/content": "^0.8.15",
|
"@welshman/content": "^0.8.15",
|
||||||
"@welshman/editor": "^0.8.15",
|
"@welshman/editor": "^0.8.15",
|
||||||
@@ -90,7 +86,7 @@
|
|||||||
"emoji-picker-element": "^1.28.1",
|
"emoji-picker-element": "^1.28.1",
|
||||||
"emoji-picker-element-data": "^1.8.0",
|
"emoji-picker-element-data": "^1.8.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"hono": "^4.12.15",
|
"hono": "^4.12.23",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"livekit-client": "^2.17.2",
|
"livekit-client": "^2.17.2",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "1.92.0"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "flotilla"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "flotilla_lib"
|
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2.5.3", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tauri = { version = "2.9.5", features = [] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["custom-protocol"]
|
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
|
||||||
"identifier": "default",
|
|
||||||
"description": "Default desktop capability for the main window",
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": ["core:default"]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#fff</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 926 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
@@ -1,2 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "1.92.0"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
||||||
pub fn run() {
|
|
||||||
tauri::Builder::default()
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
flotilla_lib::run();
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
|
||||||
"productName": "Flotilla",
|
|
||||||
"mainBinaryName": "flotilla",
|
|
||||||
"identifier": "social.flotilla.app",
|
|
||||||
"build": {
|
|
||||||
"beforeDevCommand": "pnpm dev",
|
|
||||||
"beforeBuildCommand": "pnpm build",
|
|
||||||
"devUrl": "http://localhost:1847",
|
|
||||||
"frontendDist": "../build"
|
|
||||||
},
|
|
||||||
"app": {
|
|
||||||
"security": {
|
|
||||||
"capabilities": ["default"]
|
|
||||||
},
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"label": "main",
|
|
||||||
"title": "Flotilla",
|
|
||||||
"width": 1240,
|
|
||||||
"height": 775,
|
|
||||||
"resizable": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": false,
|
|
||||||
"targets": "all",
|
|
||||||
"icon": [
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,8 +29,6 @@ export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
|||||||
|
|
||||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||||
|
|
||||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
|
||||||
|
|
||||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||||
|
|
||||||
@@ -43,6 +41,21 @@ export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
|||||||
|
|
||||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||||
|
|
||||||
|
export const participantMediaState = writable(
|
||||||
|
new Map<string, {muted: boolean; cameraOn: boolean}>(),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mediaStateByIdentity = derived(
|
||||||
|
[participantMediaState, currentVoiceSession, voiceMicMuted],
|
||||||
|
([$media, $session, $micMuted]) =>
|
||||||
|
(identity: string) => {
|
||||||
|
if ($session?.room.localParticipant.identity === identity) {
|
||||||
|
return {muted: $micMuted, cameraOn: $session.cameraOn}
|
||||||
|
}
|
||||||
|
return $media.get(identity) ?? {muted: true, cameraOn: false}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const isParticipantSpeaking = derived(
|
export const isParticipantSpeaking = derived(
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
$participants => (p: VoiceParticipant) =>
|
$participants => (p: VoiceParticipant) =>
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import {
|
|||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
LocalTrackPublication,
|
LocalTrackPublication,
|
||||||
|
Participant,
|
||||||
Room as LiveKitRoom,
|
Room as LiveKitRoom,
|
||||||
RoomEvent,
|
RoomEvent,
|
||||||
Track,
|
Track,
|
||||||
|
TrackPublication,
|
||||||
supportsAudioOutputSelection,
|
supportsAudioOutputSelection,
|
||||||
type AudioCaptureOptions,
|
type AudioCaptureOptions,
|
||||||
} from "livekit-client"
|
} from "livekit-client"
|
||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
import {map, not, removeUndefined, uniqBy} from "@welshman/lib"
|
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import {signer} from "@welshman/app"
|
||||||
@@ -25,8 +27,7 @@ import {
|
|||||||
voiceMicMuted,
|
voiceMicMuted,
|
||||||
participantFromLiveKitIdentity,
|
participantFromLiveKitIdentity,
|
||||||
participantKey,
|
participantKey,
|
||||||
participantPubkeyMap,
|
participantMediaState,
|
||||||
pubkeyFromLiveKitIdentity,
|
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
VoiceState,
|
VoiceState,
|
||||||
type VoiceParticipant,
|
type VoiceParticipant,
|
||||||
@@ -76,20 +77,23 @@ export const switchVoiceActiveDevice = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addParticipant = (identity: string) => {
|
const deleteParticipant = (identity: string) => {
|
||||||
participantPubkeyMap.update(m => {
|
participantMediaState.update(m => new Map(reject(nthEq(0, identity), [...m])))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncParticipantMedia = (participant: Participant) => {
|
||||||
|
const state = {muted: !participant.isMicrophoneEnabled, cameraOn: participant.isCameraEnabled}
|
||||||
|
participantMediaState.update(m => {
|
||||||
|
const prev = m.get(participant.identity)
|
||||||
|
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) return m
|
||||||
const next = new Map(m)
|
const next = new Map(m)
|
||||||
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
|
next.set(participant.identity, state)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteParticipant = (identity: string) => {
|
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
|
||||||
participantPubkeyMap.update(m => {
|
syncParticipantMedia(participant)
|
||||||
const next = new Map(m)
|
|
||||||
next.delete(identity)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLivekitToken = async (
|
const fetchLivekitToken = async (
|
||||||
@@ -125,15 +129,15 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
|||||||
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
||||||
derived(
|
derived(
|
||||||
[
|
[
|
||||||
participantPubkeyMap,
|
participantMediaState,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||||
],
|
],
|
||||||
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||||
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
||||||
|
|
||||||
if (inCall) {
|
if (inCall) {
|
||||||
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
|
const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
|
||||||
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
} else {
|
} else {
|
||||||
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
||||||
@@ -186,7 +190,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
|
|||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
}
|
}
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTrackSubscribed = (track: Track) => {
|
const onTrackSubscribed = (track: Track) => {
|
||||||
@@ -216,8 +220,8 @@ const playJoinSound = () => {
|
|||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onParticipantConnected = (participant: {identity: string}) => {
|
const onParticipantConnected = (participant: Participant) => {
|
||||||
addParticipant(participant.identity)
|
syncParticipantMedia(participant)
|
||||||
playJoinSound()
|
playJoinSound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +279,11 @@ export const joinVoiceRoom = async (
|
|||||||
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||||
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||||
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackMuted, onParticipantMediaChanged)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackUnmuted, onParticipantMediaChanged)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackPublished, onParticipantMediaChanged)
|
||||||
|
liveKitRoom.on(RoomEvent.TrackUnpublished, onParticipantMediaChanged)
|
||||||
|
liveKitRoom.on(RoomEvent.LocalTrackPublished, onParticipantMediaChanged)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
@@ -289,10 +298,10 @@ export const joinVoiceRoom = async (
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
addParticipant(liveKitRoom.localParticipant.identity)
|
syncParticipantMedia(liveKitRoom.localParticipant)
|
||||||
for (const p of liveKitRoom.remoteParticipants.values()) {
|
for (const p of liveKitRoom.remoteParticipants.values()) {
|
||||||
addParticipant(p.identity)
|
syncParticipantMedia(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
@@ -346,7 +355,7 @@ export const leaveVoiceRoom = async () => {
|
|||||||
resetVideoCallLayout()
|
resetVideoCallLayout()
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rejoinVoiceRoom = async (): Promise<void> => {
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
repository.removeEvent(thunk.event.id)
|
repository.removeEvent(thunk.event.id)
|
||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
} else {
|
} else {
|
||||||
|
await removeRoomMembership(url, h)
|
||||||
goto(makeSpacePath(url))
|
goto(makeSpacePath(url))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
|
||||||
import SignUpKey from "@app/components/SignUpKey.svelte"
|
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||||
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
||||||
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
@@ -91,11 +90,9 @@
|
|||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h1 class="heading">Sign up with Nostr</h1>
|
<h1 class="heading">Join {PLATFORM_NAME}</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
{PLATFORM_NAME} is built using the
|
Censorship resistant digital spaces for communities. Meet new people, own your identity.
|
||||||
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
|
|
||||||
users control over their digital identity using <strong>cryptographic key pairs</strong>.
|
|
||||||
</p>
|
</p>
|
||||||
{#if hasPomade}
|
{#if hasPomade}
|
||||||
<Button onclick={flows.email.start} class="btn btn-primary">
|
<Button onclick={flows.email.start} class="btn btn-primary">
|
||||||
|
|||||||
@@ -7,17 +7,19 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import {decodeRelay} from "@app/core/state"
|
import {decodeRelay} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
back?: () => unknown
|
back?: () => unknown
|
||||||
|
leading?: Snippet
|
||||||
title?: Snippet
|
title?: Snippet
|
||||||
action?: Snippet
|
action?: Snippet
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
|
const {back = () => goto(makeSpacePath(url)), leading, title, action, ...props}: Props = $props()
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
</script>
|
</script>
|
||||||
@@ -30,6 +32,10 @@
|
|||||||
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
|
<RelayIcon {url} size={5} class="rounded-full md:hidden" />
|
||||||
|
<div class="hidden md:contents">
|
||||||
|
{@render leading?.()}
|
||||||
|
</div>
|
||||||
{@render title?.()}
|
{@render title?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-primary md:hidden">
|
<div class="text-xs text-primary md:hidden">
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
|
|
||||||
{#if $toast}
|
{#if $toast}
|
||||||
{@const theme = $toast.theme || "info"}
|
{@const theme = $toast.theme || "info"}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={containerEl}
|
bind:this={containerEl}
|
||||||
transition:fly={{y: -20}}
|
transition:fly={{y: -20}}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
||||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
|
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
VideoCallLayout,
|
VideoCallLayout,
|
||||||
@@ -18,7 +19,12 @@
|
|||||||
ViewportSize,
|
ViewportSize,
|
||||||
videoPrimaryTileKey,
|
videoPrimaryTileKey,
|
||||||
} from "@app/call/video"
|
} from "@app/call/video"
|
||||||
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
|
import {
|
||||||
|
currentVoiceSession,
|
||||||
|
currentVoiceRoom,
|
||||||
|
mediaStateByIdentity,
|
||||||
|
pubkeyFromLiveKitIdentity,
|
||||||
|
} from "@app/call/stores"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layout: VideoCallLayout
|
layout: VideoCallLayout
|
||||||
@@ -121,6 +127,25 @@
|
|||||||
source: Track.Source.ScreenShare,
|
source: Track.Source.ScreenShare,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (!videoTiles.some(t => t.identity === rp.identity)) {
|
||||||
|
videoTiles.push({
|
||||||
|
identity: rp.identity,
|
||||||
|
isLocal: false,
|
||||||
|
trackSid: `avatar-${rp.identity}`,
|
||||||
|
track: undefined,
|
||||||
|
source: Track.Source.Camera,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoTiles.some(t => t.identity === user.identity)) {
|
||||||
|
videoTiles.push({
|
||||||
|
identity: user.identity,
|
||||||
|
isLocal: true,
|
||||||
|
trackSid: "local-avatar",
|
||||||
|
track: undefined,
|
||||||
|
source: Track.Source.Camera,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoTiles
|
return videoTiles
|
||||||
@@ -187,6 +212,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
|
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
|
||||||
|
{@const media = $mediaStateByIdentity(tile.identity)}
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"relative isolate overflow-hidden rounded-box shadow-sm",
|
"relative isolate overflow-hidden rounded-box shadow-sm",
|
||||||
@@ -206,6 +232,15 @@
|
|||||||
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if tile.track}
|
||||||
|
<div class="pointer-events-none absolute left-1 top-1 z-10">
|
||||||
|
<VoiceParticipantMediaBadges
|
||||||
|
muted={media.muted}
|
||||||
|
cameraOn={media.cameraOn}
|
||||||
|
showCamera={tile.source === Track.Source.Camera}
|
||||||
|
size={3} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
|
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
|
||||||
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
||||||
@@ -216,8 +251,8 @@
|
|||||||
data-tip={pinned ? "Exit spotlight" : "Spotlight"}
|
data-tip={pinned ? "Exit spotlight" : "Spotlight"}
|
||||||
aria-pressed={pinned}
|
aria-pressed={pinned}
|
||||||
class={cx(
|
class={cx(
|
||||||
"absolute right-1 top-1 z-20 btn btn-xs btn-square btn-ghost",
|
"absolute right-1 top-1 z-20 btn btn-xs btn-square",
|
||||||
pinned ? "btn-active bg-primary/25 text-primary" : "bg-base-100/70",
|
pinned ? "btn-primary" : "btn-ghost bg-base-100",
|
||||||
)}
|
)}
|
||||||
onclick={spotlightHandlerFor(tileKey(tile))}>
|
onclick={spotlightHandlerFor(tileKey(tile))}>
|
||||||
<Icon icon={Pin} size={3} />
|
<Icon icon={Pin} size={3} />
|
||||||
@@ -256,8 +291,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-200/50 p-4 text-center text-sm opacity-80">
|
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-200/50 p-4 text-center text-sm opacity-80">
|
||||||
<p>No camera or screen share yet.</p>
|
<p>No one is sharing video yet.</p>
|
||||||
<p class="text-xs">Use the camera or screen share control to share video.</p>
|
<p class="text-xs">
|
||||||
|
Participants appear here when they turn on their camera or share their screen.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
|
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
||||||
|
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
muted: boolean
|
||||||
|
cameraOn: boolean
|
||||||
|
showCamera?: boolean
|
||||||
|
size?: number
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {muted, cameraOn, showCamera = true, size = 3, class: className = ""}: Props = $props()
|
||||||
|
|
||||||
|
const badgeClass =
|
||||||
|
"inline-flex size-4 shrink-0 items-center justify-center rounded bg-base-100/80 p-0.5 text-error"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if muted || (showCamera && !cameraOn)}
|
||||||
|
<div class={cx("flex items-center gap-1", className)}>
|
||||||
|
{#if muted}
|
||||||
|
<span class={badgeClass} aria-label="Muted">
|
||||||
|
<Icon icon={MicrophoneOff} {size} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if showCamera && !cameraOn}
|
||||||
|
<span class={badgeClass} aria-label="Camera off">
|
||||||
|
<Icon icon={VideocameraOff} {size} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
|
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
||||||
import {makeRoomId} from "@app/core/state"
|
import {makeRoomId} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
VoiceState,
|
VoiceState,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
isParticipantSpeaking,
|
isParticipantSpeaking,
|
||||||
|
mediaStateByIdentity,
|
||||||
participantKey,
|
participantKey,
|
||||||
voiceState,
|
voiceState,
|
||||||
type VoiceParticipant,
|
type VoiceParticipant,
|
||||||
@@ -83,9 +85,17 @@
|
|||||||
)}>
|
)}>
|
||||||
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
|
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<span class="ellipsize text-xs opacity-70">
|
<span class="ellipsize min-w-0 flex-1 text-xs opacity-70">
|
||||||
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
|
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
|
{#if isActive}
|
||||||
|
{@const media = $mediaStateByIdentity(p.identity)}
|
||||||
|
<VoiceParticipantMediaBadges
|
||||||
|
muted={media.muted}
|
||||||
|
cameraOn={media.cameraOn}
|
||||||
|
size={3}
|
||||||
|
class="shrink-0" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import Videocamera from "@assets/icons/videocamera.svg?dataurl"
|
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl"
|
||||||
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
|
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
|
||||||
import Monitor from "@assets/icons/monitor.svg?dataurl"
|
import Monitor from "@assets/icons/monitor.svg?dataurl"
|
||||||
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
voiceMicMuted,
|
voiceMicMuted,
|
||||||
voiceState,
|
voiceState,
|
||||||
isLocalSpeaking,
|
|
||||||
} from "@app/call/stores"
|
} from "@app/call/stores"
|
||||||
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
||||||
|
|
||||||
@@ -188,7 +187,6 @@
|
|||||||
class={cx(
|
class={cx(
|
||||||
mediaToggleClass,
|
mediaToggleClass,
|
||||||
"overflow-visible",
|
"overflow-visible",
|
||||||
!$voiceMicMuted && $isLocalSpeaking && "text-primary",
|
|
||||||
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||||
)}
|
)}
|
||||||
onclick={toggleMute}>
|
onclick={toggleMute}>
|
||||||
@@ -207,9 +205,17 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||||
class={cx(mediaToggleClass, $currentVoiceSession.cameraOn && "text-primary")}
|
class={cx(
|
||||||
|
mediaToggleClass,
|
||||||
|
"overflow-visible",
|
||||||
|
$currentVoiceSession.cameraOn && "text-primary",
|
||||||
|
!$currentVoiceSession.cameraOn &&
|
||||||
|
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||||
|
)}
|
||||||
onclick={toggleCamera}>
|
onclick={toggleCamera}>
|
||||||
<Icon icon={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
|
<Icon
|
||||||
|
icon={$currentVoiceSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
||||||
|
size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !Capacitor.isNativePlatform()}
|
{#if !Capacitor.isNativePlatform()}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {derived, readable, writable} from "svelte/store"
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
gt,
|
gte,
|
||||||
max,
|
max,
|
||||||
spec,
|
spec,
|
||||||
call,
|
call,
|
||||||
@@ -620,7 +620,7 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
|
|||||||
for (const event of metaEvents) {
|
for (const event of metaEvents) {
|
||||||
const meta = tryCatch(() => readRoomMeta(event))
|
const meta = tryCatch(() => readRoomMeta(event))
|
||||||
|
|
||||||
if (!meta || gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
if (!meta || gte(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +651,10 @@ export const loadRoom = call(() => {
|
|||||||
|
|
||||||
await load({
|
await load({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [{kinds: [ROOM_META], "#d": [h]}],
|
filters: [
|
||||||
|
{kinds: [ROOM_META], "#d": [h]},
|
||||||
|
{kinds: [ROOM_DELETE], "#h": [h]},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 11.5C2 8.21252 2 6.56878 2.90796 5.46243C3.07418 5.25989 3.25989 5.07418 3.46243 4.90796C4.56878 4 6.21252 4 9.5 4C12.7875 4 14.4312 4 15.5376 4.90796C15.7401 5.07418 15.9258 5.25989 16.092 5.46243C17 6.56878 17 8.21252 17 11.5V12.5C17 15.7875 17 17.4312 16.092 18.5376C15.9258 18.7401 15.7401 18.9258 15.5376 19.092C14.4312 20 12.7875 20 9.5 20C6.21252 20 4.56878 20 3.46243 19.092C3.25989 18.9258 3.07418 18.7401 2.90796 18.5376C2 17.4312 2 15.7875 2 12.5V11.5Z" stroke="#000000" stroke-width="1.5"/>
|
||||||
|
<path d="M17 9.50019L17.6584 9.17101C19.6042 8.19807 20.5772 7.7116 21.2886 8.15127C22 8.59094 22 9.67872 22 11.8543V12.1461C22 14.3217 22 15.4094 21.2886 15.8491C20.5772 16.2888 19.6042 15.8023 17.6584 14.8294L17 14.5002V9.50019Z" stroke="#000000" stroke-width="1.5"/>
|
||||||
|
<path d="M22 2L2 22" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 970 B |
@@ -9,8 +9,8 @@
|
|||||||
const {children, ...props}: Props = $props()
|
const {children, ...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div data-component="PageBar" class="relative z-nav p-2 -mb-4 {props.class}">
|
<div
|
||||||
<div class="rounded-xl bg-base-100 p-4 shadow-md h-20 md:h-12 flex flex-col justify-center">
|
data-component="PageBar"
|
||||||
{@render children?.()}
|
class="relative bg-base-100 p-4 shadow-md h-20 md:h-12 flex flex-col justify-center z-nav {props.class}">
|
||||||
</div>
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -430,8 +430,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<RoomImage {url} {h} />
|
<RoomImage {url} {h} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<RoomName {url} {h} />
|
<RoomName {url} {h} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -111,8 +111,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={CalendarMinimalistic} />
|
<Icon icon={CalendarMinimalistic} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Calendar</strong>
|
<strong>Calendar</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -307,8 +307,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={ChatRound} />
|
<Icon icon={ChatRound} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Chat</strong>
|
<strong>Chat</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -63,8 +63,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={CaseMinimalistic} />
|
<Icon icon={CaseMinimalistic} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Classifieds</strong>
|
<strong>Classifieds</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -62,8 +62,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={StarFallMinimalistic} />
|
<Icon icon={StarFallMinimalistic} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Goals</strong>
|
<strong>Goals</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -62,8 +62,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={PollIcon} />
|
<Icon icon={PollIcon} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Polls</strong>
|
<strong>Polls</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -211,8 +211,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={History} />
|
<Icon icon={History} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Recent Activity</strong>
|
<strong>Recent Activity</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -63,8 +63,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet leading()}
|
||||||
<Icon icon={NotesMinimalistic} />
|
<Icon icon={NotesMinimalistic} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<strong>Threads</strong>
|
<strong>Threads</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from "vite"
|
||||||
|
import { builtinModules } from 'module'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
ssr: {
|
||||||
|
noExternal: true, // bundle every dependency
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'node24', // match your Node.js version
|
||||||
|
outDir: 'build-server',
|
||||||
|
emptyOutDir: false, // don't wipe the frontend build output
|
||||||
|
ssr: true, // tells Vite this is a server-side build
|
||||||
|
minify: 'esbuild', // minify the output
|
||||||
|
lib: {
|
||||||
|
entry: 'server.js', // your server entry point
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => 'server.js',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
// Externalize only Node.js built-ins, bundle everything else
|
||||||
|
external: [
|
||||||
|
...builtinModules,
|
||||||
|
...builtinModules.map(m => `node:${m}`),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -54,17 +54,7 @@ export default defineConfig({
|
|||||||
svg({
|
svg({
|
||||||
svgoOptions: {
|
svgoOptions: {
|
||||||
multipass: true,
|
multipass: true,
|
||||||
plugins: [
|
plugins: ["preset-default", "removeDimensions"],
|
||||||
{
|
|
||||||
name: "preset-default",
|
|
||||||
params: {
|
|
||||||
overrides: {
|
|
||||||
removeViewBox: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"removeDimensions",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||