consolidate more tables

This commit is contained in:
2026-04-10 18:09:33 -04:00
parent 8b80f2a600
commit 2bb5d31777
18 changed files with 236 additions and 236 deletions
+1 -5
View File
@@ -135,15 +135,11 @@ pub fn on_disconnect(ctx: &ReducerContext) {
sync_server_member_info(&ctx.db, ctx.sender());
if let Some(vs) = ctx.db.voice_state().identity().find(ctx.sender()) {
ctx.db.voice_state().delete(vs);
}
if let Some(ta) = ctx.db.typing_activity().identity().find(ctx.sender()) {
ctx.db.typing_activity().delete(ta);
}
clear_signaling_for_user(&ctx.db, ctx.sender());
clear_user_presence(&ctx.db, ctx.sender());
}
#[spacetimedb::reducer]
+41 -69
View File
@@ -8,13 +8,13 @@ pub fn set_typing(ctx: &ReducerContext, channel_id: u64, typing: bool) {
ctx.db.typing_activity().identity().update(TypingActivity {
identity: ctx.sender(),
channel_id,
is_typing: typing,
typing: typing,
});
} else {
ctx.db.typing_activity().insert(TypingActivity {
identity: ctx.sender(),
channel_id,
is_typing: typing,
typing: typing,
});
}
}
@@ -465,23 +465,6 @@ pub fn extend_subscription(ctx: &ReducerContext, channel_id: u64, earliest_seq_i
}
}
#[spacetimedb::reducer]
pub fn set_talking(ctx: &ReducerContext, talking: bool, channel_id: u64) {
if let Some(_) = ctx.db.voice_activity().identity().find(ctx.sender()) {
ctx.db.voice_activity().identity().update(VoiceActivity {
identity: ctx.sender(),
channel_id,
is_talking: talking,
});
} else {
ctx.db.voice_activity().insert(VoiceActivity {
identity: ctx.sender(),
channel_id,
is_talking: talking,
});
}
}
#[spacetimedb::reducer]
pub fn create_server(ctx: &ReducerContext, name: String) {
validate_name(&name).expect("Invalid name");
@@ -660,44 +643,42 @@ pub fn join_voice(ctx: &ReducerContext, channel_id: u64) {
panic!("Invalid voice channel");
}
if let Some(existing) = ctx.db.voice_state().identity().find(ctx.sender()) {
if existing.channel_id != channel_id {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
if state.channel_id != channel_id {
clear_signaling_for_user(&ctx.db, ctx.sender());
ctx.db.voice_state().identity().update(VoiceState {
identity: ctx.sender(),
channel_id,
is_sharing_screen: false,
is_muted: false,
is_deafened: false,
});
state.channel_id = channel_id;
state.is_sharing_screen = false;
state.is_talking = false;
state.watching = None;
ctx.db.user_state().identity().update(state);
}
} else {
ctx.db.voice_state().insert(VoiceState {
ctx.db.user_state().insert(UserState {
identity: ctx.sender(),
channel_id,
is_sharing_screen: false,
is_muted: false,
is_deafened: false,
is_talking: false,
watching: None,
});
}
}
#[spacetimedb::reducer]
pub fn set_sharing_screen(ctx: &ReducerContext, sharing: bool) {
if let Some(mut state) = ctx.db.voice_state().identity().find(ctx.sender()) {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.is_sharing_screen = sharing;
ctx.db.voice_state().identity().update(state);
ctx.db.user_state().identity().update(state);
if !sharing {
let watching: Vec<_> = ctx
.db
.watching()
.watchee()
.filter(ctx.sender())
.map(|w| w.id)
// Clean up watching state for people watching ME
let watchers: Vec<_> = ctx.db.user_state().iter()
.filter(|s| s.watching == Some(ctx.sender()))
.collect();
for id in watching {
ctx.db.watching().id().delete(id);
for mut w in watchers {
w.watching = None;
ctx.db.user_state().identity().update(w);
}
let signals: Vec<_> = ctx
@@ -717,60 +698,51 @@ pub fn set_sharing_screen(ctx: &ReducerContext, sharing: bool) {
#[spacetimedb::reducer]
pub fn set_mute(ctx: &ReducerContext, muted: bool) {
if let Some(mut state) = ctx.db.voice_state().identity().find(ctx.sender()) {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.is_muted = muted;
ctx.db.voice_state().identity().update(state);
ctx.db.user_state().identity().update(state);
}
}
#[spacetimedb::reducer]
pub fn set_deafen(ctx: &ReducerContext, deafened: bool) {
if let Some(mut state) = ctx.db.voice_state().identity().find(ctx.sender()) {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.is_deafened = deafened;
ctx.db.voice_state().identity().update(state);
ctx.db.user_state().identity().update(state);
}
}
#[spacetimedb::reducer]
pub fn start_watching(ctx: &ReducerContext, watchee: Identity, channel_id: u64) {
pub fn set_talking(ctx: &ReducerContext, talking: bool) {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.is_talking = talking;
ctx.db.user_state().identity().update(state);
}
}
#[spacetimedb::reducer]
pub fn start_watching(ctx: &ReducerContext, watchee: Identity) {
if ctx.sender() == watchee {
return;
}
for w in ctx.db.watching().watcher().filter(ctx.sender()) {
if w.watchee == watchee {
return;
}
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.watching = Some(watchee);
ctx.db.user_state().identity().update(state);
}
ctx.db.watching().insert(Watching {
id: 0,
watcher: ctx.sender(),
watchee,
channel_id,
});
}
#[spacetimedb::reducer]
pub fn stop_watching(ctx: &ReducerContext, watchee: Identity) {
let to_delete: Vec<_> = ctx
.db
.watching()
.watcher()
.filter(ctx.sender())
.filter(|w| w.watchee == watchee)
.map(|w| w.id)
.collect();
for id in to_delete {
ctx.db.watching().id().delete(id);
pub fn stop_watching(ctx: &ReducerContext) {
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
state.watching = None;
ctx.db.user_state().identity().update(state);
}
}
#[spacetimedb::reducer]
pub fn leave_voice(ctx: &ReducerContext) {
if let Some(_) = ctx.db.voice_state().identity().find(ctx.sender()) {
ctx.db.voice_state().identity().delete(ctx.sender());
}
clear_signaling_for_user(&ctx.db, ctx.sender());
clear_user_presence(&ctx.db, ctx.sender());
}
#[spacetimedb::reducer]
+9 -25
View File
@@ -87,9 +87,9 @@ pub struct DirectMessage {
#[index(btree)]
pub is_open_recipient: bool,
}
#[spacetimedb::table(accessor = voice_state, public)]
pub struct VoiceState {
#[spacetimedb::table(accessor = user_state)]
#[derive(Clone)]
pub struct UserState {
#[primary_key]
pub identity: Identity,
#[index(btree)]
@@ -97,28 +97,8 @@ pub struct VoiceState {
pub is_sharing_screen: bool,
pub is_muted: bool,
pub is_deafened: bool,
}
#[spacetimedb::table(accessor = voice_activity, public)]
pub struct VoiceActivity {
#[primary_key]
pub identity: Identity,
#[index(btree)]
pub channel_id: u64,
pub is_talking: bool,
}
#[spacetimedb::table(accessor = watching, public)]
pub struct Watching {
#[primary_key]
#[auto_inc]
pub id: u64,
#[index(btree)]
pub watcher: Identity,
#[index(btree)]
pub watchee: Identity,
#[index(btree)]
pub channel_id: u64,
pub watching: Option<Identity>,
}
#[derive(spacetimedb::SpacetimeType, Clone, Copy, Debug, PartialEq)]
@@ -128,6 +108,7 @@ pub enum SignalKind {
IceCandidate,
}
#[derive(spacetimedb::SpacetimeType, Clone, Copy, Debug, PartialEq)]
pub enum MediaType {
Voice,
@@ -226,14 +207,17 @@ pub struct Image {
}
#[spacetimedb::table(accessor = typing_activity, public)]
#[derive(Clone)]
pub struct TypingActivity {
#[primary_key]
#[index(btree)]
pub identity: Identity,
#[index(btree)]
pub channel_id: u64,
pub is_typing: bool,
pub typing: bool,
}
#[spacetimedb::table(accessor = recent_message)]
pub struct RecentMessage {
#[primary_key]
+7 -14
View File
@@ -265,20 +265,6 @@ pub fn get_visible_image_ids_read_only(db: &LocalReadOnly, identity: Identity) -
}
pub fn clear_signaling_for_user(db: &Local, identity: Identity) {
if let Some(va) = db.voice_activity().identity().find(identity) {
db.voice_activity().delete(va);
}
let watchers: Vec<_> = db.watching().watcher().filter(identity).collect();
for row in watchers {
db.watching().delete(row);
}
let watchees: Vec<_> = db.watching().watchee().filter(identity).collect();
for row in watchees {
db.watching().delete(row);
}
for row in db
.webrtc_signal()
.sender()
@@ -297,6 +283,13 @@ pub fn clear_signaling_for_user(db: &Local, identity: Identity) {
}
}
pub fn clear_user_presence(db: &Local, identity: Identity) {
if let Some(state) = db.user_state().identity().find(identity) {
db.user_state().delete(state);
}
clear_signaling_for_user(db, identity);
}
pub fn auto_join_community_server(db: &Local, identity: Identity) {
let community_server = db.server().name().filter(&"Zep".to_string()).next();
if let Some(s) = community_server {
+71 -1
View File
@@ -1,6 +1,6 @@
use crate::tables::*;
use crate::utils::*;
use spacetimedb::{Identity, Query, Timestamp, ViewContext};
use spacetimedb::{Identity, Query, Table, Timestamp, ViewContext};
#[derive(spacetimedb::SpacetimeType)]
pub struct VisibleImageRow {
@@ -27,6 +27,40 @@ pub struct VisibleMessageRow {
pub is_encrypted: bool,
}
#[spacetimedb::view(accessor = visible_typing_activity, public)]
pub fn visible_typing_activity(ctx: &ViewContext) -> Vec<TypingActivity> {
let identity = ctx.sender();
let mut results = Vec::new();
// 1. Server channels
for member in ctx.db.server_member().identity().filter(identity) {
if let Some(s) = ctx.db.server().id().find(member.server_id) {
for chan_meta in s.channels {
for activity in ctx.db.typing_activity().channel_id().filter(chan_meta.id) {
results.push(activity);
}
}
}
}
// 2. DM channels
let mut dm_channel_ids = Vec::new();
for dm in ctx.db.direct_message().sender().filter(identity) {
dm_channel_ids.push(dm.channel_id);
}
for dm in ctx.db.direct_message().recipient().filter(identity) {
dm_channel_ids.push(dm.channel_id);
}
for channel_id in dm_channel_ids {
for activity in ctx.db.typing_activity().channel_id().filter(channel_id) {
results.push(activity);
}
}
results
}
#[derive(spacetimedb::SpacetimeType)]
pub struct MyChannelSubscriptionRow {
pub identity: Identity,
@@ -226,6 +260,42 @@ pub fn visible_images(ctx: &ViewContext) -> Vec<VisibleImageRow> {
results
}
#[spacetimedb::view(accessor = visible_user_states, public)]
pub fn visible_user_states(ctx: &ViewContext) -> Vec<UserState> {
let identity = ctx.sender();
let mut results = std::collections::HashMap::new();
// 1. My own state
if let Some(my_state) = ctx.db.user_state().identity().find(identity) {
results.insert(my_state.identity, my_state.clone());
}
// 2. States in my servers
for member in ctx.db.server_member().identity().filter(identity) {
if let Some(s) = ctx.db.server().id().find(member.server_id) {
for chan_meta in s.channels {
for state in ctx.db.user_state().channel_id().filter(chan_meta.id) {
results.insert(state.identity, state.clone());
}
}
}
}
// 3. States in my DMs
for dm in ctx.db.direct_message().sender().filter(identity) {
for state in ctx.db.user_state().channel_id().filter(dm.channel_id) {
results.insert(state.identity, state.clone());
}
}
for dm in ctx.db.direct_message().recipient().filter(identity) {
for state in ctx.db.user_state().channel_id().filter(dm.channel_id) {
results.insert(state.identity, state.clone());
}
}
results.into_values().collect()
}
#[spacetimedb::view(accessor = visible_webrtc_signals, public)]
pub fn visible_webrtc_signals(ctx: &ViewContext) -> Vec<WebRTCSignal> {
let identity = ctx.sender();
+8 -1
View File
@@ -33,7 +33,14 @@
chat.identity = $spacetime.identity;
webrtc.identity = $spacetime.identity;
}
webrtc.connectedChannelId = chat.connectedVoiceChannel?.id;
// Sync voice channel status
const voiceChan = chat.connectedVoiceChannel;
if (voiceChan) {
webrtc.connectedChannelId = voiceChan.id;
} else {
webrtc.connectedChannelId = undefined;
}
});
setContext("chat", chat);
setContext("webrtc", webrtc);
+2 -3
View File
@@ -11,12 +11,11 @@
let offlineMembers = $derived(chat.activeServerMembers.filter((m) => !m.online));
function isTalking(member: Types.ServerMember) {
return chat.voiceActivity.find((va) => va.identity.isEqual(member.identity))?.isTalking || false;
return chat.userStates.find((s) => s.identity.isEqual(member.identity))?.isTalking || false;
}
function isSharing(member: Types.ServerMember) {
const userVoiceState = chat.voiceStates.find((vs) => vs.identity.isEqual(member.identity));
return userVoiceState?.isSharingScreen || false;
return chat.userStates.find((s) => s.identity.isEqual(member.identity))?.isSharingScreen || false;
}
function isMe(member: Types.ServerMember) {
+5 -5
View File
@@ -21,7 +21,7 @@
// Voice specific states
const peerIdHex = user.identity.toHexString();
const voiceState = $derived(chat.voiceStates.find(vs => vs.identity.isEqual(user.identity)));
const voiceState = $derived(chat.userStates.find(s => s.identity.isEqual(user.identity)));
const isMe = $derived(chat.identity?.isEqual(user.identity));
const isLocalUserInVoice = $derived(!!chat.connectedVoiceChannel);
const isInSameVoiceChannel = $derived(
@@ -30,9 +30,9 @@
voiceState.channelId === chat.connectedVoiceChannel?.id
);
const isWatchingThisUser = $derived(chat.watching.some(w =>
w.watcher.isEqual(chat.identity) && w.watchee.isEqual(user.identity)
));
const isWatchingThisUser = $derived(
chat.currentVoiceState?.watching?.isEqual(user.identity) || false
);
let volume = $state(1.0);
let isMuted = $state(false);
@@ -129,7 +129,7 @@
class="menu-item"
onclick={() => {
if (isWatchingThisUser) {
webrtc.stopWatching(user.identity);
webrtc.stopWatching();
} else {
webrtc.startWatching(user.identity);
}
+36 -44
View File
@@ -11,14 +11,14 @@
let focusedIdentity = $state<Identity | null>(null);
const participants = $derived(
chat.voiceStates.filter((vs) => vs.channelId === webrtc.connectedChannelId),
chat.userStates.filter((s) => s.channelId === webrtc.connectedChannelId),
);
const localSharing = $derived(!!webrtc.localScreenStream);
const remoteSharerVs = $derived(
participants.find((vs) => {
if (vs.identity.isEqual(webrtc.identity!)) return false;
return vs.isSharingScreen;
participants.find((s) => {
if (s.identity.isEqual(webrtc.identity!)) return false;
return s.isSharingScreen;
}),
);
@@ -29,29 +29,27 @@
const primarySharerIdentity = $derived(focusedIdentity || defaultSharerIdentity);
function isWatchingPeer(peerIdHex: string) {
return webrtc.watching.some(
(w) =>
w.watcher.isEqual(webrtc.identity!) &&
w.watchee.toHexString() === peerIdHex,
);
const s = participants.find(p => p.identity.toHexString() === peerIdHex);
// I am watching the peer if my state's watching field points to them
return chat.currentVoiceState?.watching?.toHexString() === peerIdHex;
}
function toggleWatch(peerIdentity: Identity) {
if (isWatchingPeer(peerIdentity.toHexString())) {
webrtc.stopWatching(peerIdentity);
webrtc.stopWatching();
} else {
webrtc.startWatching(peerIdentity);
}
}
const heroVs = $derived(
participants.find((vs) =>
vs.identity.isEqual(primarySharerIdentity || Identity.zero()),
participants.find((s) =>
s.identity.isEqual(primarySharerIdentity || Identity.zero()),
),
);
const rowParticipants = $derived(
participants.filter(
(vs) => !vs.identity.isEqual(primarySharerIdentity || Identity.zero()),
(s) => !s.identity.isEqual(primarySharerIdentity || Identity.zero()),
),
);
</script>
@@ -74,9 +72,7 @@
? webrtc.localScreenStream || undefined
: webrtc.peers.get(heroVs.identity.toHexString())?.videoStream}
isLocal={heroVs.identity.isEqual(webrtc.identity!)}
isTalking={chat.voiceActivity.find((va) =>
va.identity.isEqual(heroVs.identity),
)?.isTalking || false}
isTalking={heroVs.isTalking}
isWatching={isWatchingPeer(heroVs.identity.toHexString())}
isSharing={heroVs.identity.isEqual(webrtc.identity!)
? localSharing
@@ -89,29 +85,27 @@
{/if}
{#if rowParticipants.length > 0}
<div class="video-participants-row">
{#each rowParticipants as vs (vs.identity.toHexString())}
{#each rowParticipants as s (s.identity.toHexString())}
<div
class="video-tile-container is-row"
onclick={() => (focusedIdentity = vs.identity)}
onclick={() => (focusedIdentity = s.identity)}
role="button"
tabindex="0"
onkeydown={(e) => e.key === "Enter" && (focusedIdentity = vs.identity)}
onkeydown={(e) => e.key === "Enter" && (focusedIdentity = s.identity)}
style="cursor: pointer;"
>
<VideoTile
identity={vs.identity}
stream={vs.identity.isEqual(webrtc.identity!)
identity={s.identity}
stream={s.identity.isEqual(webrtc.identity!)
? webrtc.localScreenStream || undefined
: webrtc.peers.get(vs.identity.toHexString())?.videoStream}
isLocal={vs.identity.isEqual(webrtc.identity!)}
isTalking={chat.voiceActivity.find((va) =>
va.identity.isEqual(vs.identity),
)?.isTalking || false}
isWatching={isWatchingPeer(vs.identity.toHexString())}
isSharing={vs.identity.isEqual(webrtc.identity!)
: webrtc.peers.get(s.identity.toHexString())?.videoStream}
isLocal={s.identity.isEqual(webrtc.identity!)}
isTalking={s.isTalking}
isWatching={isWatchingPeer(s.identity.toHexString())}
isSharing={s.identity.isEqual(webrtc.identity!)
? localSharing
: vs.isSharingScreen}
onToggleWatch={() => toggleWatch(vs.identity)}
: s.isSharingScreen}
onToggleWatch={() => toggleWatch(s.identity)}
isHero={false}
users={chat.users}
/>
@@ -120,29 +114,27 @@
</div>
{/if}
{:else}
{#each participants as vs (vs.identity.toHexString())}
{#each participants as s (s.identity.toHexString())}
<div
class="video-tile-container is-grid"
onclick={() => (focusedIdentity = vs.identity)}
onclick={() => (focusedIdentity = s.identity)}
role="button"
tabindex="0"
onkeydown={(e) => e.key === "Enter" && (focusedIdentity = vs.identity)}
onkeydown={(e) => e.key === "Enter" && (focusedIdentity = s.identity)}
style="cursor: pointer;"
>
<VideoTile
identity={vs.identity}
stream={vs.identity.isEqual(webrtc.identity!)
identity={s.identity}
stream={s.identity.isEqual(webrtc.identity!)
? webrtc.localScreenStream || undefined
: webrtc.peers.get(vs.identity.toHexString())?.videoStream}
isLocal={vs.identity.isEqual(webrtc.identity!)}
isTalking={chat.voiceActivity.find((va) =>
va.identity.isEqual(vs.identity),
)?.isTalking || false}
isWatching={isWatchingPeer(vs.identity.toHexString())}
isSharing={vs.identity.isEqual(webrtc.identity!)
: webrtc.peers.get(s.identity.toHexString())?.videoStream}
isLocal={s.identity.isEqual(webrtc.identity!)}
isTalking={s.isTalking}
isWatching={isWatchingPeer(s.identity.toHexString())}
isSharing={s.identity.isEqual(webrtc.identity!)
? localSharing
: vs.isSharingScreen}
onToggleWatch={() => toggleWatch(vs.identity)}
: s.isSharingScreen}
onToggleWatch={() => toggleWatch(s.identity)}
isHero={false}
users={chat.users}
/>
@@ -87,13 +87,13 @@
<!-- Voice Channel Members -->
<div class="voice-member-list">
{#each chat.voiceStates.filter((vs) => vs.channelId === channel.id) as vs (vs.identity.toHexString())}
{@const peerIdHex = vs.identity.toHexString()}
{@const isMe = chat.identity?.isEqual(vs.identity)}
{#each chat.userStates.filter((s) => s.channelId === channel.id) as s (s.identity.toHexString())}
{@const peerIdHex = s.identity.toHexString()}
{@const isMe = chat.identity?.isEqual(s.identity)}
{@const status = webrtc.peerStatuses.get(peerIdHex)}
{@const isTalking = chat.voiceActivity.find((va) => va.identity.isEqual(vs.identity))?.isTalking || false}
{@const isSharing = vs.isSharingScreen}
{@const isWatchingMe = chat.watching.some(w => w.watcher.isEqual(vs.identity) && w.watchee.isEqual(chat.identity))}
{@const isTalking = s.isTalking}
{@const isSharing = s.isSharingScreen}
{@const isWatchingMe = s.watching?.isEqual(chat.identity!)}
{@const amISharing = chat.currentVoiceState?.isSharingScreen}
{@const voiceStatusColor = isMe ? "green" : getStatusColor(status)}
{@const isLocalUserInThisChannel = chat.connectedVoiceChannel?.id === channel.id}
@@ -101,13 +101,13 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="voice-member-item"
oncontextmenu={(e) => handleContextMenu(e, vs.identity)}
oncontextmenu={(e) => handleContextMenu(e, s.identity)}
role="button"
tabindex="-1"
>
<Avatar user={chat.users.find(u => u.identity.isEqual(vs.identity))} size="tiny" isTalking={isTalking} />
<Avatar user={chat.users.find(u => u.identity.isEqual(s.identity))} size="tiny" isTalking={isTalking} />
<span class="voice-member-name {isTalking ? 'talking' : ''}">
{chat.getUsername(vs.identity)}
{chat.getUsername(s.identity)}
</span>
{#if amISharing && isWatchingMe}
<span class="watcher-eye" title="Watching your stream">
@@ -135,20 +135,20 @@
<span class="sharing-badge">LIVE</span>
{/if}
<div class="voice-member-indicators">
{#if vs.isDeafened}
{#if s.isDeafened}
<span class="voice-indicator-icon" title="Deafened">
<svg viewBox="0 0 512 512" class="deafen-indicator-svg" fill="currentColor">
<path d="M0 256C0 114.6 114.6 0 256 0S512 114.6 512 256V416c0 35.3-28.7 64-64 64H384c-35.3 0-64-28.7-64-64V320c0-35.3 28.7-64 64-64h64V256c0-106-86-192-192-192S64 150 64 256v64h64c35.3 0 64 28.7 64 64v96c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256z"/>
<rect x="-50" y="236" width="612" height="40" transform="rotate(-45 256 256)" fill="currentColor" rx="4" />
</svg>
</span>
{:else if vs.isMuted}
{:else if s.isMuted}
<span class="voice-indicator-icon" title="Muted"><i class="fas fa-microphone-slash"></i></span>
{#if isLocalUserInThisChannel}
<div class="status-dot {voiceStatusColor}"></div>
{/if}
{/if}
</div>
{#if isLocalUserInThisChannel}
<div class="status-dot {voiceStatusColor}"></div>
{/if}
</div>
{#if isLocalUserInThisChannel && hoveredPeer === peerIdHex && popoverPos}
@@ -159,7 +159,7 @@
style="left: {popoverPos.x}px; top: {popoverPos.y}px;"
>
<div class="popover-header">
<span class="popover-name">{chat.getUsername(vs.identity)}</span>
<span class="popover-name">{chat.getUsername(s.identity)}</span>
<span class="popover-status {voiceStatusColor}">
{isMe ? "connected" : status || "connecting"}
</span>
+2 -8
View File
@@ -340,18 +340,12 @@ export class ChatService {
get customEmojis() {
return this.#db.customEmojis;
}
get voiceStates() {
return this.#db.voiceStates;
}
get voiceActivity() {
return this.#db.voiceActivity;
get userStates() {
return this.#db.userStates;
}
get typingActivity() {
return this.#db.typingActivity;
}
get watching() {
return this.#db.watching;
}
get isUsersReady() {
return this.#db.isUsersReady;
}
+4 -10
View File
@@ -14,10 +14,8 @@ export class DatabaseService {
allThreads = $state<readonly Types.Thread[]>([]);
images = $state<readonly Types.VisibleImageRow[]>([]);
customEmojis = $state<readonly Types.CustomEmoji[]>([]);
voiceStates = $state<readonly Types.VoiceState[]>([]);
voiceActivity = $state<readonly Types.VoiceActivity[]>([]);
userStates = $state<readonly Types.UserState[]>([]);
typingActivity = $state<readonly Types.TypingActivity[]>([]);
watching = $state<readonly Types.Watching[]>([]);
systemConfiguration = $state<readonly Types.SystemConfiguration[]>([]);
uploadStatus = $state<readonly Types.UploadStatus[]>([]);
isUsersReady = $state(false);
@@ -43,14 +41,12 @@ export class DatabaseService {
const [channelsStore, channelsReadyStore] = useTable(tables.visible_channels);
const [directMessagesStore] = useTable(tables.visible_direct_messages);
const [usersStore, usersReadyStore] = useTable(tables.user);
const [voiceStatesStore] = useTable(tables.voice_state);
const [userStatesStore] = useTable(tables.visible_user_states);
const [serverMembersStore, membersReadyStore] = useTable(tables.visible_server_members);
const [threadsStore] = useTable(tables.thread);
const [imagesStore, imagesReadyStore] = useTable(tables.visible_images);
const [customEmojisStore] = useTable(tables.custom_emoji);
const [voiceActivityStore] = useTable(tables.voice_activity);
const [typingActivityStore] = useTable(tables.typing_activity);
const [watchingStore] = useTable(tables.watching);
const [typingActivityStore] = useTable(tables.visible_typing_activity);
const [systemConfigStore] = useTable(tables.system_configuration);
const [uploadStatusStore] = useTable(tables.upload_status);
@@ -61,7 +57,7 @@ export class DatabaseService {
directMessagesStore.subscribe((v) => (this.directMessages = v));
usersStore.subscribe((v) => (this.users = v));
usersReadyStore.subscribe((v) => (this.isUsersReady = v));
voiceStatesStore.subscribe((v) => (this.voiceStates = v));
userStatesStore.subscribe((v) => (this.userStates = v));
serverMembersStore.subscribe((v) => (this.serverMembers = v));
membersReadyStore.subscribe((v) => (this.isMembersReady = v));
threadsStore.subscribe((v) => (this.allThreads = v));
@@ -77,9 +73,7 @@ export class DatabaseService {
});
imagesReadyStore.subscribe((v) => (this.isImagesReady = v));
customEmojisStore.subscribe((v) => (this.customEmojis = v));
voiceActivityStore.subscribe((v) => (this.voiceActivity = v));
typingActivityStore.subscribe((v) => (this.typingActivity = v));
watchingStore.subscribe((v) => (this.watching = v));
systemConfigStore.subscribe((v) => (this.systemConfiguration = v));
uploadStatusStore.subscribe((v) => (this.uploadStatus = v));
}
+2 -2
View File
@@ -162,8 +162,8 @@ export class MessagingService {
this.#subscribeToChannelReducer({ channelId });
queries.push(`SELECT * FROM visible_scrollback_messages`);
queries.push(`SELECT * FROM thread WHERE channel_id = ${channelId}`);
queries.push(`SELECT * FROM voice_state WHERE channel_id = ${channelId}`);
queries.push(`SELECT * FROM typing_activity WHERE channel_id = ${channelId}`);
queries.push(`SELECT * FROM visible_user_states`);
queries.push(`SELECT * FROM visible_typing_activity WHERE channel_id = ${channelId}`);
}
console.log(`[MessagingService] Updating subscriptions: ${queries.length} queries`);
+1 -1
View File
@@ -22,7 +22,7 @@ export class VoiceService {
}
get currentVoiceState() {
return this.#db.voiceStates.find((vs) =>
return this.#db.userStates.find((vs) =>
vs.identity?.isEqual(this.#identity() || Identity.zero()),
);
}
@@ -5,7 +5,7 @@ import { PeerManagerService } from "./peer-manager.svelte";
import * as Types from "../../../module_bindings/types";
export class ChannelAudioWebRTCService {
voiceStates = $state<readonly Types.VoiceState[]>([]);
userStates = $state<readonly Types.UserState[]>([]);
signals = $state<readonly Types.WebRtcSignal[]>([]);
#sendSignal = useReducer(reducers.sendWebrtcSignal);
@@ -36,8 +36,8 @@ export class ChannelAudioWebRTCService {
this.localStream = localStream;
this.isDeafened = isDeafened;
const [vsStore] = useTable(tables.voice_state);
vsStore.subscribe((v) => (this.voiceStates = v));
const [usStore] = useTable(tables.visible_user_states);
usStore.subscribe((v) => (this.userStates = v));
const [signalsStore] = useTable(tables.visible_webrtc_signals);
signalsStore.subscribe((v) => (this.signals = v));
@@ -101,13 +101,13 @@ export class ChannelAudioWebRTCService {
}
const peersToConnect = new Set(
this.voiceStates
this.userStates
.filter(
(vs) =>
vs.channelId === this.connectedChannelId &&
!vs.identity.isEqual(this.identity!),
(s) =>
s.channelId === this.connectedChannelId &&
!s.identity.isEqual(this.identity!),
)
.map((vs) => vs.identity.toHexString()),
.map((s) => s.identity.toHexString()),
);
peersToConnect.forEach((id) => {
@@ -74,7 +74,6 @@ export class LocalMediaService {
if (this.connectedChannelId !== undefined) {
this.#setTalking({
talking: false,
channelId: this.connectedChannelId,
});
}
this.isTalking = false;
@@ -153,7 +152,6 @@ export class LocalMediaService {
if (this.connectedChannelId !== undefined) {
this.#setTalking({
talking: false,
channelId: this.connectedChannelId,
});
}
this.isTalking = false;
@@ -167,7 +165,6 @@ export class LocalMediaService {
if (this.connectedChannelId !== undefined) {
this.#setTalking({
talking: true,
channelId: this.connectedChannelId,
});
}
this.isTalking = true;
@@ -178,7 +175,6 @@ export class LocalMediaService {
if (this.connectedChannelId !== undefined) {
this.#setTalking({
talking: false,
channelId: this.connectedChannelId,
});
}
this.isTalking = false;
@@ -5,7 +5,7 @@ import { PeerManagerService } from "./peer-manager.svelte";
import * as Types from "../../../module_bindings/types";
export class ScreenSharingWebRTCService {
watching = $state<readonly Types.Watching[]>([]);
userStates = $state<readonly Types.UserState[]>([]);
signals = $state<readonly Types.WebRtcSignal[]>([]);
#sendSignal = useReducer(reducers.sendWebrtcSignal);
@@ -33,8 +33,8 @@ export class ScreenSharingWebRTCService {
this.connectedChannelId = connectedChannelId;
this.localScreenStream = localScreenStream;
const [wStore] = useTable(tables.watching);
wStore.subscribe((v) => (this.watching = v));
const [usStore] = useTable(tables.visible_user_states);
usStore.subscribe((v) => (this.userStates = v));
const [signalsStore] = useTable(tables.visible_webrtc_signals);
signalsStore.subscribe((v) => (this.signals = v));
@@ -96,12 +96,16 @@ export class ScreenSharingWebRTCService {
}
const screenPeersToConnect = new Set<string>();
this.watching.forEach((w) => {
if (w.channelId === this.connectedChannelId) {
if (w.watcher.isEqual(this.identity!))
screenPeersToConnect.add(w.watchee.toHexString());
else if (w.watchee.isEqual(this.identity!))
screenPeersToConnect.add(w.watcher.toHexString());
this.userStates.forEach((s) => {
if (s.channelId === this.connectedChannelId) {
// If I am watching this user
if (s.identity.isEqual(this.identity!)) {
if (s.watching) screenPeersToConnect.add(s.watching.toHexString());
}
// If this user is watching me
if (s.watching?.isEqual(this.identity!)) {
screenPeersToConnect.add(s.identity.toHexString());
}
}
});
+11 -12
View File
@@ -11,7 +11,7 @@ import * as Types from "../../../module_bindings/types";
export class WebRTCService {
identity = $state<Identity | null>(null);
connectedChannelId = $state<bigint | undefined>();
watching = $state<readonly Types.Watching[]>([]);
userStates = $state<readonly Types.UserState[]>([]);
#startWatchingReducer = useReducer(reducers.startWatching);
#stopWatchingReducer = useReducer(reducers.stopWatching);
@@ -29,16 +29,16 @@ export class WebRTCService {
this.identity = identity;
this.connectedChannelId = connectedChannelId;
const [wStore] = useTable(tables.watching);
wStore.subscribe((v) => (this.watching = v));
const [usStore] = useTable(tables.visible_user_states);
usStore.subscribe((v) => (this.userStates = v));
// Sound for new/leaving watchers
let lastWatchers = new Set<string>();
$effect(() => {
const currentWatchers = new Set(
this.watching
.filter((w) => this.identity && w.watchee.isEqual(this.identity))
.map((w) => w.watcher.toHexString()),
this.userStates
.filter((s) => this.identity && s.watching?.isEqual(this.identity))
.map((s) => s.identity.toHexString()),
);
if (this.identity) {
@@ -85,9 +85,9 @@ export class WebRTCService {
let lastWatched = new Set<string>();
$effect(() => {
const currentWatched = new Set(
this.watching
.filter((w) => this.identity && w.watcher.isEqual(this.identity))
.map((w) => w.watchee.toHexString()),
this.userStates
.filter((s) => this.identity && s.identity.isEqual(this.identity) && s.watching)
.map((s) => s.watching!.toHexString()),
);
if (this.identity) {
@@ -165,13 +165,12 @@ export class WebRTCService {
if (this.connectedChannelId !== undefined) {
this.#startWatchingReducer({
watchee: peerIdentity,
channelId: this.connectedChannelId,
});
}
};
stopWatching = (peerIdentity: Identity) => {
this.#stopWatchingReducer({ watchee: peerIdentity });
stopWatching = () => {
this.#stopWatchingReducer();
};
// Facade getters