1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use dioxus::prelude::*;
use crate::components::avatar::Avatar;
use crate::state::app_state::AppState;
/// Video room component.
/// Video rooms are persistent rooms where a call is always active.
/// They use Element Call (or Jitsi) widget embedded in the room.
#[component]
pub fn VideoRoom(room_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut is_joined = use_signal(|| false);
let participants = use_signal(Vec::<VideoParticipant>::new);
let room_name = state
.read()
.rooms
.values()
.find(|r| r.room_id.to_string() == room_id)
.map(|r| r.display_name.clone())
.unwrap_or_else(|| "Video Room".to_string());
rsx! {
div {
class: "video-room",
// Video room header
div {
class: "video-room__header",
h3 { "{room_name}" }
span {
class: "video-room__status",
if *is_joined.read() { "In call" } else { "Not joined" }
}
}
if !*is_joined.read() {
// Join prompt
div {
class: "video-room__join-prompt",
div {
class: "video-room__preview",
Avatar {
name: room_name.clone(),
url: None::<String>,
size: 80,
}
h4 { "{room_name}" }
p { "This is a video room. Join to participate in the ongoing call." }
}
div {
class: "video-room__join-actions",
button {
class: "btn btn--primary",
onclick: move |_| is_joined.set(true),
"Join with video"
}
button {
class: "btn btn--secondary",
onclick: move |_| is_joined.set(true),
"Join without video"
}
}
}
} else {
// Video grid
div {
class: "video-room__grid",
// Self video tile
div {
class: "video-room__tile video-room__tile--self",
div {
class: "video-room__tile-video",
// Camera placeholder
Avatar {
name: "You".to_string(),
url: None::<String>,
size: 64,
}
}
span { class: "video-room__tile-name", "You" }
}
// Participant tiles
for p in participants.read().iter() {
{
let name = p.display_name.clone();
let avatar = p.avatar_url.clone();
rsx! {
div {
class: "video-room__tile",
div {
class: "video-room__tile-video",
Avatar {
name: name.clone(),
url: avatar,
size: 64,
}
}
span { class: "video-room__tile-name", "{name}" }
}
}
}
}
}
// Call controls
div {
class: "video-room__controls",
button {
class: "call-view__btn",
title: "Toggle microphone",
"🎤"
}
button {
class: "call-view__btn",
title: "Toggle camera",
"📹"
}
button {
class: "call-view__btn",
title: "Share screen",
"🖥"
}
button {
class: "call-view__btn call-view__btn--hangup",
onclick: move |_| is_joined.set(false),
"Leave"
}
}
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct VideoParticipant {
user_id: String,
display_name: String,
avatar_url: Option<String>,
has_video: bool,
is_muted: bool,
}