consolidate more tables
This commit is contained in:
@@ -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
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user