Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e7d9188
feat: WebRTC voice/video channels with `^` prefix
ValwareIRC May 3, 2026
2f10087
android: sign release APK so it installs on devices
ValwareIRC May 3, 2026
5e1d0c2
voice: chunked SDP, mid-based track attribution, spotlight UI, reacti…
ValwareIRC May 5, 2026
7d7134b
voice: REQ obsidianirc/voice cap, wire-budget SDP chunking, drop self…
ValwareIRC May 5, 2026
d9a8e37
compose: switch web container to traefik-fronted production layout
ValwareIRC May 5, 2026
3ecee75
android: register ARC++ VIEW action for deep links on ChromeOS
ValwareIRC May 5, 2026
ac1570d
voice-channels: $ stream channels + 401 fixes
ValwareIRC May 8, 2026
5da6d4a
voice-channels: per-peer volume slider for stream rooms
ValwareIRC May 8, 2026
ecb3c04
voice-channels: render the regular text ChatArea below the voice grid
ValwareIRC May 10, 2026
99d861c
Merge branch 'main' into feat/voice-channels
ValwareIRC May 10, 2026
bcbc1d3
voice-channels: show per-peer volume slider AND mute toggle in ^ rooms
ValwareIRC May 10, 2026
480be92
voice-channels: gate sections on cap, prefix-aware icons, & local cha…
ValwareIRC May 11, 2026
980c57c
voice-channels: drop the duplicate header inside the voice grid
ValwareIRC May 11, 2026
456c4b8
voice-channels: keep the channel ChatHeader above the voice grid
ValwareIRC May 11, 2026
d5336a3
voice-channels: dynamic grid sizing so tiles fill the panel
ValwareIRC May 11, 2026
4d5d42b
voice-channels: shrink voice section to its content, give chat the rest
ValwareIRC May 11, 2026
c5bec43
Revert compose.yaml to main; do not change deploy layout on this branch
ValwareIRC May 11, 2026
24d57bb
Merge remote-tracking branch 'origin/main' into feat/voice-channels
ValwareIRC May 11, 2026
c7be554
Merge remote-tracking branch 'origin/main' into feat/voice-channels
ValwareIRC May 11, 2026
7f25024
Merge remote-tracking branch 'origin/main' into feat/voice-channels
ValwareIRC May 11, 2026
d6aef73
Merge remote-tracking branch 'origin/main' into feat/voice-channels
ValwareIRC May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 13 additions & 103 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,107 +2,17 @@ services:
obsidian:
build:
context: .
dockerfile: Dockerfile
container_name: obsidian-irc
ports:
- '3000:80'
dockerfile: ./Dockerfile
args:
VITE_DEFAULT_IRC_SERVER: wss://${IRC_FQDN}
VITE_DEFAULT_IRC_SERVER_NAME: ${NETWORK_NAME}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if Dockerfile declares and uses the build args

rg -n 'ARG (VITE_DEFAULT_IRC_SERVER|VITE_DEFAULT_IRC_SERVER_NAME)' Dockerfile

Repository: ObsidianIRC/ObsidianIRC

Length of output: 132


🏁 Script executed:

# Check for existing environment variable documentation
find . -maxdepth 2 -type f \( -name ".env.example" -o -name ".env" -o -name "README*" \) | head -20

Repository: ObsidianIRC/ObsidianIRC

Length of output: 94


🏁 Script executed:

# Search for references to IRC_FQDN and NETWORK_NAME in documentation/config files
rg -i "(IRC_FQDN|NETWORK_NAME|WEB_FQDN)" --type markdown --type txt --type yaml . | grep -v "compose.yaml" | head -20

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Read .env.example to see what's documented
cat -n ./.env.example

Repository: ObsidianIRC/ObsidianIRC

Length of output: 619


🏁 Script executed:

# Search README for environment variable section
head -100 README.md

Repository: ObsidianIRC/ObsidianIRC

Length of output: 5066


🏁 Script executed:

# Search for docker or deployment documentation
find . -maxdepth 2 -type f \( -name "docker*" -o -name "DOCKER*" -o -name "deploy*" \) | grep -v ".git"

Repository: ObsidianIRC/ObsidianIRC

Length of output: 49


🏁 Script executed:

# Check if there's a docker-compose override or additional config
ls -la | grep -E "compose|docker"

Repository: ObsidianIRC/ObsidianIRC

Length of output: 260


🏁 Script executed:

# Check what's in the docker directory
ls -la docker/

Repository: ObsidianIRC/ObsidianIRC

Length of output: 368


🏁 Script executed:

# Check if there's a .env file (not .env.example)
ls -la | grep "\.env"

Repository: ObsidianIRC/ObsidianIRC

Length of output: 133


Document required environment variables for docker-compose deployment.

The build args in compose.yaml (lines 6-8) reference ${IRC_FQDN}, ${NETWORK_NAME}, and ${WEB_FQDN} (line 16), but these infrastructure-level variables are undocumented. The existing .env.example documents application variables (VITE_*), not the environment variables required for docker-compose deployment. Without documentation, users deploying via docker-compose will not know these required variables must be set.

Add a .env file or deployment documentation clarifying the required variables:

📝 Recommendation

Create a .env file at the repository root documenting infrastructure deployment variables:

# Required for docker-compose deployment
IRC_FQDN=irc.example.com
NETWORK_NAME="My IRC Network"
WEB_FQDN=web.example.com

Or add a deployment section to README.md explaining these variables and how to set them before running docker-compose up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compose.yaml` around lines 6 - 8, The docker-compose build args reference
infra variables (IRC_FQDN, NETWORK_NAME, WEB_FQDN) that are not documented; add
a deployment-level .env (or extend .env.example) and/or a README "Deployment
(docker-compose)" section that lists and explains IRC_FQDN, NETWORK_NAME, and
WEB_FQDN, shows example values and instructs users to export or create the .env
at repo root before running docker-compose up so the VITE_* build args in
compose.yaml resolve correctly.

image: obsidianirc:local

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this imagdoesn't exist publicy @ValwareIRC

DONT TOUCH THIS IN THIS PR

container_name: obsidianirc
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost/']
interval: 30s
timeout: 10s
retries: 3

gen-certs:
image: alpine:3.20
volumes:
- ${MKCERT_CAROOT:?Run ./scripts/setup-dev-certs.sh first}:/caroot:ro
- ./certs:/certs
command:
- sh
- -c
- |
openssl x509 -noout -in /certs/server.pem 2>/dev/null && exit 0
apk add --no-cache openssl -q
openssl genrsa -out /certs/server-key.pem 2048
openssl req -new -key /certs/server-key.pem -out /tmp/csr.pem -subj "/CN=localhost"
printf '[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1\n' > /tmp/ext.cnf
openssl x509 -req -in /tmp/csr.pem \
-CA /caroot/rootCA.pem -CAkey /caroot/rootCA-key.pem -set_serial 1 \
-out /certs/server.pem -days 825 -sha256 -extfile /tmp/ext.cnf -extensions SAN
profiles:
- testing

# TODO - Add IRC daemon + backend etc
ircd:
init: true
# TODO: Use our unrealircd custom image instead
image: ghcr.io/ergochat/ergo:master
container_name: ergo
ports:
- '6667:6667' # plain IRC (bots)
- '6697:6697' # IRC over TLS — ircs://localhost:6697 (Tauri native TCP)
- '8097:8097' # WebSocket over TLS — wss://localhost:8097 (browser / WebView)
restart: unless-stopped
depends_on:
gen-certs:
condition: service_completed_successfully
volumes:
- ./docker/ergo.yaml:/ircd/ircd.yaml
- ./certs:/ircd/certs:ro
profiles:
- testing

echo-bot:
image: python:3.12-alpine
container_name: echo-bot
volumes:
- ./docker/echobot.py:/app/echobot.py:ro
working_dir: /app
depends_on:
- ircd
environment:
HOST: ircd
PORT: 6667
CHANNEL: '#test'
NICK: EchoBot
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing

helper-bot:
image: python:3.12-alpine
container_name: helper-bot
volumes:
- ./docker/echobot.py:/app/echobot.py:ro
working_dir: /app
depends_on:
- ircd
environment:
HOST: ircd
PORT: 6667
CHANNEL: '#test'
NICK: HelperBot1
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing

chat-bot:
image: python:3.12-alpine
container_name: chat-bot
volumes:
- ./docker/echobot.py:/app/echobot.py:ro
working_dir: /app
depends_on:
- ircd
environment:
HOST: ircd
PORT: 6667
CHANNEL: '#test'
NICK: HelperBot2
command: ['python', '-u', 'echobot.py']
restart: unless-stopped
profiles:
- testing
expose:
- "80"
labels:
- traefik.enable=true
- traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
- traefik.http.routers.obsidian.tls=true
- traefik.http.services.obsidian.loadbalancer.server.port=80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing Traefik network configuration.

The service has been changed from published ports to expose, meaning it's only accessible via the Docker internal network. The Traefik labels route traffic to this service, but there's no networks: section connecting the obsidian service to Traefik's network.

Without a shared network, Traefik cannot discover or route to this container, making the service unreachable.

🔧 Proposed fix

Define and attach a network that Traefik uses:

     labels:
       - traefik.enable=true
       - traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
       - traefik.http.routers.obsidian.tls=true
       - traefik.http.services.obsidian.loadbalancer.server.port=80
+    networks:
+      - traefik-network
+
+networks:
+  traefik-network:
+    external: true

Alternatively, if Traefik is defined in this same compose file, use that network name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compose.yaml` around lines 12 - 18, The obsidian service uses expose and
Traefik labels but is not attached to Traefik's network; update the obsidian
service (the service that includes expose: "80" and labels like
traefik.http.routers.obsidian.*) to include a networks: entry referencing the
Traefik network name, and add a matching top-level networks: definition (or use
the existing Traefik network name if Traefik is in this compose). Ensure the
network name matches the one Traefik is listening on so Traefik can discover and
route to the obsidian container.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete Traefik TLS configuration.

The labels enable TLS (traefik.http.routers.obsidian.tls=true) but don't specify:

  1. Which Traefik entrypoint to use (typically websecure for HTTPS on port 443)
  2. How to obtain TLS certificates (cert resolver for Let's Encrypt, etc.)

Without these, HTTPS routing may not work as expected.

🔒 Proposed additions
     labels:
       - traefik.enable=true
       - traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
+      - traefik.http.routers.obsidian.entrypoints=websecure
       - traefik.http.routers.obsidian.tls=true
+      - traefik.http.routers.obsidian.tls.certresolver=letsencrypt
       - traefik.http.services.obsidian.loadbalancer.server.port=80

Adjust websecure and letsencrypt to match your Traefik configuration. If using a different cert resolver or entrypoint name, update accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
labels:
- traefik.enable=true
- traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
- traefik.http.routers.obsidian.tls=true
- traefik.http.services.obsidian.loadbalancer.server.port=80
labels:
- traefik.enable=true
- traefik.http.routers.obsidian.rule=Host(`${WEB_FQDN}`)
- traefik.http.routers.obsidian.entrypoints=websecure
- traefik.http.routers.obsidian.tls=true
- traefik.http.routers.obsidian.tls.certresolver=letsencrypt
- traefik.http.services.obsidian.loadbalancer.server.port=80
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compose.yaml` around lines 14 - 18, The Traefik TLS labels are incomplete:
update the Docker service labels for the obsidian router by adding an entrypoint
label (traefik.http.routers.obsidian.entrypoints, e.g. set to "websecure") and a
cert resolver label (traefik.http.routers.obsidian.tls.certresolver, e.g.
"letsencrypt") so Traefik knows which entrypoint to use and how to obtain
certificates; ensure you replace "websecure" and "letsencrypt" with the actual
entrypoint and certResolver names used in your Traefik configuration.

24 changes: 24 additions & 0 deletions src-tauri/gen/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import java.io.File
import java.util.Properties

plugins {
Expand All @@ -24,6 +25,28 @@ android {
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val keystorePropsFile = rootProject.file("keystore.properties")
if (keystorePropsFile.exists()) {
val keystoreProps = Properties().apply {
keystorePropsFile.inputStream().use { load(it) }
}
storeFile = file(keystoreProps.getProperty("storeFile"))
storePassword = keystoreProps.getProperty("storePassword")
keyAlias = keystoreProps.getProperty("keyAlias")
keyPassword = keystoreProps.getProperty("keyPassword")
} else {
val debugKeystore = File(System.getProperty("user.home"), ".android/debug.keystore")
if (debugKeystore.exists()) {
storeFile = debugKeystore
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
}
}
Comment on lines +28 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this might break CI builds

buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
Expand All @@ -38,6 +61,7 @@ android {
}
getByName("release") {
isMinifyEnabled = true
signingConfig = signingConfigs.getByName("release")
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/gen/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
<intent-filter >
<action android:name="android.intent.action.VIEW" />
<!-- ChromeOS ARC++ uses a different action for deep links -->
<action android:name="org.chromium.arc.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
Expand All @@ -56,6 +58,8 @@
</intent-filter>
<intent-filter >
<action android:name="android.intent.action.VIEW" />
<!-- ChromeOS ARC++ uses a different action for deep links -->
<action android:name="org.chromium.arc.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
Expand Down
57 changes: 44 additions & 13 deletions src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChatArea } from "./ChatArea";
import { MemberList } from "./MemberList";
import { ResizableSidebar } from "./ResizableSidebar";
import { ServerList } from "./ServerList";
import { VoiceChannelView } from "./VoiceChannelView";

const PAGE_ORDER: layoutColumn[] = ["serverList", "chatView", "memberList"];
const getPageIndex = (column: layoutColumn): number =>
Expand Down Expand Up @@ -41,7 +42,19 @@ export const AppLayout: React.FC = () => {
selectedChannelId: null,
selectedPrivateChatId: null,
};
const { selectedPrivateChatId } = currentSelection;
const { selectedChannelId, selectedPrivateChatId } = currentSelection;

const selectedChannel = useStore((s) => {
if (!selectedServerId || !selectedChannelId) return null;
const srv = s.servers.find((x) => x.id === selectedServerId);
return srv?.channels.find((c) => c.id === selectedChannelId) ?? null;
});
// Both ^ (voice) and $ (stream) channels render through VoiceChannelView.
// The view itself reads the room mode from VoiceState.mode and adapts
// its layout (publishing controls vs viewer chat).
const isVoiceChannel =
!!selectedChannel?.name.startsWith("^") ||
!!selectedChannel?.name.startsWith("$");
const {
isDarkMode,
isMobileMenuOpen,
Expand All @@ -50,8 +63,9 @@ export const AppLayout: React.FC = () => {
mobileViewActiveColumn,
} = ui;

// Hide member list for private chats
const shouldShowMemberList = isMemberListVisible && !selectedPrivateChatId;
// Hide member list for private chats and voice channels (voice has its own grid)
const shouldShowMemberList =
isMemberListVisible && !selectedPrivateChatId && !isVoiceChannel;
Comment on lines +66 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hide the mobile member page for voice/stream channels too.

This only suppresses the desktop/sidebar member list. On narrow view, the mobile pager still treats the channel as having a member page, so voice channels can still swipe into the old MemberList screen even though the voice grid is supposed to replace it. Reuse the same !isVoiceChannel condition in the mobile-page calculation as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/AppLayout.tsx` around lines 66 - 68, The mobile pager
still exposes the MemberList for voice/stream channels because the mobile-page
visibility logic doesn't reuse the voice-channel guard; update the mobile-page
calculation to include the same !isVoiceChannel condition used in
shouldShowMemberList (which currently is: const shouldShowMemberList =
isMemberListVisible && !selectedPrivateChatId && !isVoiceChannel) so that the
mobile member page is suppressed for voice/stream channels as well; locate the
mobile pager/mobile-page visibility variable and add the !isVoiceChannel check
alongside isMemberListVisible and !selectedPrivateChatId.


const handleChannelListWidthChange = useCallback((width: number) => {
setChannelListWidth(width);
Expand Down Expand Up @@ -202,16 +216,33 @@ export const AppLayout: React.FC = () => {
<div
className={`${isNarrowView ? "w-full" : "flex-grow"} h-full bg-discord-dark-200 flex flex-col min-w-0 z-10`}
>
<ChatArea
isChanListVisible={isChannelListVisible}
onToggleChanList={() => {
if (isNarrowView) {
setMobileViewActiveColumn("serverList");
} else {
toggleChannelList(!isChannelListVisible);
}
}}
/>
{(() => {
// Both `^` (voice) and `$` (stream) channels render through
// ChatArea with the VoiceChannelView injected as a topSlot,
// so the channel's ChatHeader stays at the very top of the
// panel and the voice grid + chat live beneath it as one
// continuous column.
const voiceTopSlot =
isVoiceChannel && selectedServerId && selectedChannel ? (
<VoiceChannelView
serverId={selectedServerId}
channelName={selectedChannel.name}
/>
) : undefined;
return (
<ChatArea
isChanListVisible={isChannelListVisible}
onToggleChanList={() => {
if (isNarrowView) {
setMobileViewActiveColumn("serverList");
} else {
toggleChannelList(!isChannelListVisible);
}
}}
topSlot={voiceTopSlot}
/>
);
})()}
</div>
);
case "memberList":
Expand Down
Loading
Loading