fenetre 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +98 -9
- data/app/assets/javascripts/fenetre/application.js +1 -14
- data/app/assets/javascripts/fenetre/controllers/index.js +1 -19
- data/app/assets/javascripts/fenetre/controllers/video_chat_controller.js +556 -516
- data/app/assets/javascripts/fenetre/vendor/stimulus.js +2567 -0
- data/app/assets/javascripts/fenetre/vendor/stimulus.min.js +5 -2588
- data/app/assets/javascripts/fenetre.js +12 -8
- data/app/assets/javascripts/stimulus/stimulus.min.js +5 -2588
- data/app/assets/stylesheets/fenetre/video_chat.css +218 -157
- data/app/channels/fenetre/video_chat_channel.rb +69 -111
- data/app/controllers/fenetre/status_controller.rb +13 -0
- data/app/helpers/fenetre/video_chat_helper.rb +54 -32
- data/app/javascript/controllers/fenetre/video_chat_controller.js +1 -662
- data/config/importmap/fenetre.rb +4 -0
- data/config/importmap.rb +7 -21
- data/config/routes.rb +6 -0
- data/lib/fenetre/engine.rb +4 -167
- data/lib/fenetre/parallel_formatter.rb +25 -0
- data/lib/fenetre/presence.rb +34 -0
- data/lib/fenetre/version.rb +1 -1
- data/lib/fenetre.rb +40 -2
- metadata +25 -42
- data/Rakefile +0 -33
- data/app/javascript/test/test_runner.js +0 -15
- data/app/javascript/test/video_chat_controller_test.js +0 -215
- data/lib/fenetre/video_chat_channel.rb +0 -36
- data/lib/tasks/javascript_test.rake +0 -69
|
@@ -1,225 +1,286 @@
|
|
|
1
|
-
/* Fenetre Video Chat
|
|
1
|
+
/* Fenetre Video Chat — stage-first layout for Zoom/Skype/Jitsi-class calls */
|
|
2
|
+
|
|
2
3
|
.fenetre-video-chat-container {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
4
|
+
--fenetre-bg: #12151a;
|
|
5
|
+
--fenetre-surface: #1a1f27;
|
|
6
|
+
--fenetre-text: #f3f6fa;
|
|
7
|
+
--fenetre-muted: #9aa3b2;
|
|
8
|
+
--fenetre-accent: #3d8bfd;
|
|
9
|
+
--fenetre-danger: #e35d6a;
|
|
10
|
+
--fenetre-ok: #3dd68c;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
border-bottom: 1px solid #23272e;
|
|
26
|
-
}
|
|
27
|
-
.fenetre-theme-dark video {
|
|
28
|
-
border: 2px solid #23272e;
|
|
29
|
-
background: #101214;
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: minmax(0, 1fr) minmax(220px, 280px);
|
|
14
|
+
grid-template-rows: auto minmax(320px, 1fr) auto;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
border-radius: 12px;
|
|
17
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
18
|
+
padding: 1rem;
|
|
19
|
+
max-width: 1200px;
|
|
20
|
+
min-height: 520px;
|
|
21
|
+
margin: 1rem auto;
|
|
22
|
+
font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
|
23
|
+
background: radial-gradient(120% 80% at 10% 0%, #1c2430 0%, var(--fenetre-bg) 55%);
|
|
24
|
+
color: var(--fenetre-text);
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
.fenetre-theme-light {
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
--fenetre-bg: #eef2f7;
|
|
29
|
+
--fenetre-surface: #ffffff;
|
|
30
|
+
--fenetre-text: #1a1f27;
|
|
31
|
+
--fenetre-muted: #5b6575;
|
|
32
|
+
--fenetre-accent: #2563eb;
|
|
33
|
+
background: linear-gradient(160deg, #f7fafd 0%, #e8eef6 100%);
|
|
34
|
+
color: var(--fenetre-text);
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
|
|
37
|
+
.fenetre-connection-status {
|
|
38
|
+
grid-column: 1 / -1;
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-self: start;
|
|
41
|
+
justify-self: start;
|
|
42
|
+
padding: 4px 10px;
|
|
43
|
+
border-radius: 999px;
|
|
44
|
+
font-size: 0.75rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
letter-spacing: 0.02em;
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
.fenetre-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
.fenetre-status-connecting { background: #ffe08a; color: #5c4800; }
|
|
50
|
+
.fenetre-status-connected { background: color-mix(in srgb, var(--fenetre-ok) 30%, transparent); color: var(--fenetre-ok); }
|
|
51
|
+
.fenetre-status-disconnected,
|
|
52
|
+
.fenetre-status-error { background: color-mix(in srgb, var(--fenetre-danger) 25%, transparent); color: var(--fenetre-danger); }
|
|
53
|
+
.fenetre-status-reconnecting {
|
|
54
|
+
background: #e2e3e5;
|
|
55
|
+
color: #383d41;
|
|
56
|
+
animation: fenetre-pulse 1.5s infinite;
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
|
|
59
|
+
.fenetre-stage {
|
|
60
|
+
grid-column: 1;
|
|
61
|
+
grid-row: 2;
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 0.75rem;
|
|
65
|
+
min-height: 0;
|
|
46
66
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
|
|
68
|
+
.fenetre-sidebar {
|
|
69
|
+
grid-column: 2;
|
|
70
|
+
grid-row: 2 / 4;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 0.5rem;
|
|
74
|
+
background: var(--fenetre-surface);
|
|
75
|
+
border-radius: 10px;
|
|
76
|
+
padding: 0.75rem;
|
|
77
|
+
min-height: 0;
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
.fenetre-video-chat-container h3 {
|
|
53
|
-
margin
|
|
54
|
-
|
|
81
|
+
margin: 0.25rem 0;
|
|
82
|
+
font-size: 0.8rem;
|
|
55
83
|
font-weight: 600;
|
|
56
|
-
|
|
84
|
+
text-transform: uppercase;
|
|
85
|
+
letter-spacing: 0.06em;
|
|
86
|
+
color: var(--fenetre-muted);
|
|
57
87
|
}
|
|
88
|
+
|
|
58
89
|
.fenetre-video-chat-container ul#fenetre-participant-list {
|
|
59
|
-
border-radius: 8px;
|
|
60
|
-
padding: 0.5em 1em;
|
|
61
|
-
margin-bottom: 1em;
|
|
62
90
|
list-style: none;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.fenetre-video-chat-container ul#fenetre-participant-list li {
|
|
66
|
-
padding: 0.2em 0;
|
|
67
|
-
}
|
|
68
|
-
.fenetre-video-chat-container .fenetre-video-section {
|
|
69
|
-
display: flex;
|
|
70
|
-
gap: 2em;
|
|
71
|
-
flex-wrap: wrap;
|
|
72
|
-
}
|
|
73
|
-
.fenetre-video-chat-container video {
|
|
91
|
+
margin: 0 0 0.5rem;
|
|
92
|
+
padding: 0.4rem 0.6rem;
|
|
74
93
|
border-radius: 8px;
|
|
75
|
-
|
|
76
|
-
.fenetre-video-chat-container #fenetre-chat-box {
|
|
77
|
-
border-radius: 8px;
|
|
78
|
-
padding: 0.5em 1em;
|
|
94
|
+
background: color-mix(in srgb, var(--fenetre-bg) 55%, var(--fenetre-surface));
|
|
79
95
|
min-height: 2em;
|
|
80
|
-
|
|
81
|
-
font-size: 1em;
|
|
82
|
-
max-height: 180px;
|
|
96
|
+
max-height: 8rem;
|
|
83
97
|
overflow-y: auto;
|
|
84
|
-
|
|
85
|
-
.fenetre-video-chat-container #fenetre-chat-box div {
|
|
86
|
-
margin-bottom: 0.3em;
|
|
98
|
+
font-size: 0.85rem;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
.fenetre-
|
|
91
|
-
display:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
font-weight: 500;
|
|
96
|
-
margin-bottom: 1rem;
|
|
101
|
+
.fenetre-local-section,
|
|
102
|
+
.fenetre-remote-section {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 0.75rem;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
min-height: 0;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
.fenetre-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
.fenetre-local-video,
|
|
110
|
+
.fenetre-remote-video-container video {
|
|
111
|
+
width: 100%;
|
|
112
|
+
height: 100%;
|
|
113
|
+
object-fit: cover;
|
|
114
|
+
background: #0a0c0f;
|
|
115
|
+
border-radius: 10px;
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
.fenetre-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
.fenetre-local-section {
|
|
119
|
+
position: relative;
|
|
120
|
+
max-width: 280px;
|
|
121
|
+
aspect-ratio: 16 / 9;
|
|
122
|
+
border-radius: 10px;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
.fenetre-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
.fenetre-remote-grid {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
130
|
+
gap: 0.75rem;
|
|
131
|
+
width: 100%;
|
|
132
|
+
flex: 1;
|
|
112
133
|
}
|
|
113
134
|
|
|
114
|
-
.fenetre-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
.fenetre-remote-video-container {
|
|
136
|
+
position: relative;
|
|
137
|
+
aspect-ratio: 16 / 9;
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
background: #0a0c0f;
|
|
141
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
118
142
|
}
|
|
119
143
|
|
|
120
|
-
.fenetre-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
.fenetre-peer-label {
|
|
145
|
+
position: absolute;
|
|
146
|
+
left: 8px;
|
|
147
|
+
bottom: 8px;
|
|
148
|
+
margin: 0;
|
|
149
|
+
padding: 2px 8px;
|
|
150
|
+
border-radius: 6px;
|
|
151
|
+
font-size: 0.75rem;
|
|
152
|
+
background: rgba(0, 0, 0, 0.55);
|
|
153
|
+
color: #fff;
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
/* Peer connection status indicators */
|
|
127
156
|
.fenetre-peer-status {
|
|
128
157
|
position: absolute;
|
|
129
|
-
top:
|
|
130
|
-
right:
|
|
131
|
-
border-radius:
|
|
132
|
-
padding: 2px
|
|
158
|
+
top: 8px;
|
|
159
|
+
right: 8px;
|
|
160
|
+
border-radius: 6px;
|
|
161
|
+
padding: 2px 8px;
|
|
133
162
|
font-size: 0.7rem;
|
|
134
|
-
background: rgba(0, 0, 0, 0.
|
|
163
|
+
background: rgba(0, 0, 0, 0.55);
|
|
135
164
|
color: white;
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
.fenetre-peer-connecting {
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
.fenetre-peer-connecting { background-color: rgba(255, 193, 7, 0.85); color: #212529; }
|
|
168
|
+
.fenetre-peer-connected { background-color: rgba(40, 167, 69, 0.85); }
|
|
169
|
+
.fenetre-peer-disconnected,
|
|
170
|
+
.fenetre-peer-failed { background-color: rgba(220, 53, 69, 0.85); }
|
|
171
|
+
|
|
172
|
+
.fenetre-controls {
|
|
173
|
+
grid-column: 1;
|
|
174
|
+
grid-row: 3;
|
|
175
|
+
display: flex;
|
|
176
|
+
gap: 10px;
|
|
177
|
+
flex-wrap: wrap;
|
|
178
|
+
align-items: center;
|
|
141
179
|
}
|
|
142
180
|
|
|
143
|
-
.fenetre-
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
.fenetre-control-button,
|
|
182
|
+
.fenetre-chat-send-button {
|
|
183
|
+
padding: 10px 16px;
|
|
184
|
+
border-radius: 999px;
|
|
185
|
+
border: none;
|
|
186
|
+
font-size: 0.9rem;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
background: var(--fenetre-surface);
|
|
190
|
+
color: var(--fenetre-text);
|
|
191
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fenetre-text) 12%, transparent);
|
|
192
|
+
transition: background 0.15s, color 0.15s, transform 0.1s;
|
|
146
193
|
}
|
|
147
194
|
|
|
148
|
-
.fenetre-
|
|
149
|
-
|
|
150
|
-
|
|
195
|
+
.fenetre-control-button:hover,
|
|
196
|
+
.fenetre-chat-send-button:hover {
|
|
197
|
+
background: color-mix(in srgb, var(--fenetre-accent) 18%, var(--fenetre-surface));
|
|
151
198
|
}
|
|
152
199
|
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
position: relative;
|
|
156
|
-
border-radius: 8px;
|
|
157
|
-
overflow: hidden;
|
|
200
|
+
.fenetre-control-button:active {
|
|
201
|
+
transform: scale(0.98);
|
|
158
202
|
}
|
|
159
203
|
|
|
160
|
-
/* Screen sharing button styles */
|
|
161
204
|
.fenetre-control-button.screen-sharing {
|
|
162
|
-
background
|
|
163
|
-
color:
|
|
205
|
+
background: var(--fenetre-accent);
|
|
206
|
+
color: #fff;
|
|
164
207
|
}
|
|
165
208
|
|
|
166
|
-
.fenetre-
|
|
167
|
-
|
|
209
|
+
.fenetre-control-button.video-off,
|
|
210
|
+
.fenetre-control-button.audio-off {
|
|
211
|
+
background: var(--fenetre-danger);
|
|
212
|
+
color: #fff;
|
|
168
213
|
}
|
|
169
214
|
|
|
170
|
-
.fenetre-
|
|
171
|
-
|
|
215
|
+
.fenetre-chat-container {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
flex: 1;
|
|
219
|
+
min-height: 0;
|
|
172
220
|
}
|
|
173
221
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
opacity: 1;
|
|
184
|
-
}
|
|
222
|
+
.fenetre-chat-messages {
|
|
223
|
+
flex: 1;
|
|
224
|
+
min-height: 120px;
|
|
225
|
+
max-height: none;
|
|
226
|
+
overflow-y: auto;
|
|
227
|
+
padding: 0.5rem;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
background: color-mix(in srgb, var(--fenetre-bg) 55%, var(--fenetre-surface));
|
|
230
|
+
font-size: 0.85rem;
|
|
185
231
|
}
|
|
186
232
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
margin-top: 1rem;
|
|
192
|
-
flex-wrap: wrap;
|
|
233
|
+
.fenetre-chat-message {
|
|
234
|
+
margin-bottom: 0.35rem;
|
|
235
|
+
line-height: 1.35;
|
|
236
|
+
word-break: break-word;
|
|
193
237
|
}
|
|
194
238
|
|
|
195
|
-
.fenetre-
|
|
196
|
-
padding: 8px 12px;
|
|
197
|
-
border-radius: 20px;
|
|
198
|
-
border: none;
|
|
199
|
-
font-size: 0.9rem;
|
|
200
|
-
cursor: pointer;
|
|
239
|
+
.fenetre-chat-input-container {
|
|
201
240
|
display: flex;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
transition: all 0.2s;
|
|
241
|
+
gap: 0.4rem;
|
|
242
|
+
margin-top: 0.5rem;
|
|
205
243
|
}
|
|
206
244
|
|
|
207
|
-
.fenetre-
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
.fenetre-chat-input-container input {
|
|
246
|
+
flex: 1;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
padding: 8px 10px;
|
|
249
|
+
border-radius: 8px;
|
|
250
|
+
border: 1px solid color-mix(in srgb, var(--fenetre-text) 15%, transparent);
|
|
251
|
+
background: var(--fenetre-bg);
|
|
252
|
+
color: var(--fenetre-text);
|
|
210
253
|
}
|
|
211
254
|
|
|
212
|
-
.fenetre-
|
|
213
|
-
|
|
214
|
-
color:
|
|
255
|
+
.fenetre-media-error {
|
|
256
|
+
grid-column: 1 / -1;
|
|
257
|
+
color: var(--fenetre-danger);
|
|
258
|
+
padding: 10px;
|
|
259
|
+
margin: 0 0 0.5rem;
|
|
260
|
+
background: color-mix(in srgb, var(--fenetre-danger) 12%, transparent);
|
|
261
|
+
border: 1px solid var(--fenetre-danger);
|
|
262
|
+
border-radius: 8px;
|
|
215
263
|
}
|
|
216
264
|
|
|
217
|
-
|
|
218
|
-
opacity:
|
|
265
|
+
@keyframes fenetre-pulse {
|
|
266
|
+
0%, 100% { opacity: 1; }
|
|
267
|
+
50% { opacity: 0.55; }
|
|
219
268
|
}
|
|
220
269
|
|
|
221
|
-
|
|
222
|
-
.fenetre-
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
@media (max-width: 860px) {
|
|
271
|
+
.fenetre-video-chat-container {
|
|
272
|
+
grid-template-columns: 1fr;
|
|
273
|
+
grid-template-rows: auto auto auto auto;
|
|
274
|
+
min-height: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.fenetre-stage,
|
|
278
|
+
.fenetre-sidebar,
|
|
279
|
+
.fenetre-controls {
|
|
280
|
+
grid-column: 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.fenetre-stage { grid-row: 2; }
|
|
284
|
+
.fenetre-controls { grid-row: 3; }
|
|
285
|
+
.fenetre-sidebar { grid-row: 4; }
|
|
225
286
|
}
|
|
@@ -2,163 +2,121 @@
|
|
|
2
2
|
|
|
3
3
|
module Fenetre
|
|
4
4
|
class VideoChatChannel < ActionCable::Channel::Base
|
|
5
|
-
|
|
6
|
-
@@participants = Hash.new { |h, k| h[k] = [] }
|
|
5
|
+
SIGNAL_TYPES = %w[offer answer candidate screen_share raise_hand lower_hand].freeze
|
|
7
6
|
|
|
8
7
|
def subscribed
|
|
9
|
-
return reject unless current_user
|
|
8
|
+
return reject unless connection.respond_to?(:current_user) && current_user
|
|
9
|
+
return reject if params[:room_id].blank?
|
|
10
10
|
|
|
11
|
-
@room_id = params[:room_id]
|
|
12
|
-
return reject
|
|
11
|
+
@room_id = params[:room_id].to_s
|
|
12
|
+
return reject unless Fenetre.authorize_room.call(current_user, @room_id)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
return reject if
|
|
16
|
-
return reject if params[:max_participants] && participants(@room_id).size >= params[:max_participants].to_i
|
|
14
|
+
@options = Fenetre.room_options.call(current_user, @room_id).symbolize_keys
|
|
15
|
+
return reject if @options[:room_locked] && user_role != :host
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
@accepted = true
|
|
18
|
+
stream_from room_stream
|
|
19
|
+
stream_from user_stream(current_user.id)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def unsubscribed
|
|
22
|
-
|
|
23
|
+
@accepted = false
|
|
24
|
+
return unless @joined
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
@joined = false
|
|
27
|
+
remaining = Fenetre.presence.leave(@room_id, current_user.id)
|
|
28
|
+
broadcast('type' => 'leave', 'from' => current_user.id, 'participants' => remaining)
|
|
29
|
+
instrument(:leave)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
def
|
|
30
|
-
return unless @
|
|
31
|
-
|
|
32
|
-
init_participants(@room_id)
|
|
33
|
-
type = data['type'] || data[:type]
|
|
34
|
-
payload = data['payload'] || data[:payload]
|
|
35
|
-
return if type == 'screen_share' && !params[:enable_screen_sharing]
|
|
36
|
-
|
|
37
|
-
broadcast_signal(@room_id, type, current_user.id, payload)
|
|
38
|
-
end
|
|
32
|
+
def join_room(_data = {})
|
|
33
|
+
return unless @accepted && !@joined
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'from' => current_user.id,
|
|
47
|
-
'participants' => participants(@room_id).dup
|
|
48
|
-
}
|
|
49
|
-
message['topic'] = params[:room_topic] if params[:room_topic]
|
|
50
|
-
message['max_participants'] = params[:max_participants] if params[:max_participants]
|
|
51
|
-
message['turbo_stream'] = '<turbo-stream action="append">...</turbo-stream>'
|
|
52
|
-
ActionCable.server.broadcast(room_stream(@room_id), message)
|
|
53
|
-
log_analytics(:join, current_user.id, @room_id)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def kick(data)
|
|
57
|
-
return unless user_role == :host
|
|
35
|
+
participants = Fenetre.presence.join(@room_id, current_user.id, @options[:max_participants])
|
|
36
|
+
unless participants
|
|
37
|
+
@accepted = false
|
|
38
|
+
stop_all_streams
|
|
39
|
+
return reject
|
|
40
|
+
end
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
@joined = true
|
|
43
|
+
message = { 'type' => 'join', 'from' => current_user.id, 'participants' => participants }
|
|
44
|
+
message['topic'] = @options[:room_topic] if @options[:room_topic]
|
|
45
|
+
broadcast(message)
|
|
46
|
+
instrument(:join)
|
|
63
47
|
end
|
|
64
48
|
|
|
65
|
-
def
|
|
66
|
-
return unless
|
|
67
|
-
|
|
68
|
-
user_id = data['user_id'] || data[:user_id]
|
|
69
|
-
ActionCable.server.broadcast(room_stream(@room_id),
|
|
70
|
-
{ 'type' => 'mute', 'from' => current_user.id,
|
|
71
|
-
'payload' => { 'user_id' => user_id } })
|
|
72
|
-
end
|
|
49
|
+
def signal(data)
|
|
50
|
+
return unless @accepted && @joined
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
return unless
|
|
52
|
+
type = data['type'].to_s
|
|
53
|
+
return unless SIGNAL_TYPES.include?(type)
|
|
54
|
+
return if type == 'screen_share' && @options[:enable_screen_sharing] == false
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{ 'type' => 'unmute', 'from' => current_user.id,
|
|
80
|
-
'payload' => { 'user_id' => user_id } })
|
|
56
|
+
message = { 'type' => type, 'from' => current_user.id, 'payload' => data['payload'] }
|
|
57
|
+
deliver(message, data['to'])
|
|
81
58
|
end
|
|
82
59
|
|
|
83
60
|
def chat(data)
|
|
84
|
-
return unless @
|
|
61
|
+
return unless @accepted && @joined
|
|
62
|
+
return if data['to'].present? && !@options[:enable_private_chat]
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
return if
|
|
64
|
+
message = data['message'].to_s.strip.each_char.take(2000).join
|
|
65
|
+
return if message.empty?
|
|
88
66
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ActionCable.server.broadcast(room_stream(@room_id),
|
|
92
|
-
{ 'type' => 'chat', 'from' => current_user.id,
|
|
93
|
-
'payload' => { 'message' => message, 'to' => to } })
|
|
67
|
+
deliver({ 'type' => 'chat', 'from' => current_user.id,
|
|
68
|
+
'payload' => { 'message' => message, 'to' => data['to'] } }, data['to'])
|
|
94
69
|
end
|
|
70
|
+
alias send_message chat
|
|
95
71
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
public_send(action, data)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def reject
|
|
103
|
-
@_rejected = true
|
|
104
|
-
super if defined?(super)
|
|
72
|
+
def kick(data)
|
|
73
|
+
moderate('kick', data)
|
|
105
74
|
end
|
|
106
75
|
|
|
107
|
-
def
|
|
108
|
-
|
|
76
|
+
def mute(data)
|
|
77
|
+
moderate('mute', data)
|
|
109
78
|
end
|
|
110
79
|
|
|
111
|
-
def
|
|
112
|
-
|
|
80
|
+
def unmute(data)
|
|
81
|
+
moderate('unmute', data)
|
|
113
82
|
end
|
|
114
83
|
|
|
115
84
|
private
|
|
116
85
|
|
|
117
86
|
def user_role
|
|
118
|
-
connection.respond_to?(:user_role) ? connection.user_role : nil
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def room_stream(room_id)
|
|
122
|
-
"fenetre_video_chat_#{room_id}"
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def init_participants(room_id)
|
|
126
|
-
@@participants[room_id] ||= []
|
|
87
|
+
connection.respond_to?(:user_role) ? connection.user_role&.to_sym : nil
|
|
127
88
|
end
|
|
128
89
|
|
|
129
|
-
def
|
|
130
|
-
|
|
90
|
+
def room_stream
|
|
91
|
+
"fenetre_video_chat_#{@room_id}"
|
|
131
92
|
end
|
|
132
93
|
|
|
133
|
-
def
|
|
134
|
-
|
|
94
|
+
def user_stream(user_id)
|
|
95
|
+
"fenetre_user_#{[@room_id, user_id.to_s].to_json}"
|
|
135
96
|
end
|
|
136
97
|
|
|
137
|
-
def
|
|
138
|
-
|
|
98
|
+
def broadcast(message)
|
|
99
|
+
ActionCable.server.broadcast(room_stream, message)
|
|
139
100
|
end
|
|
140
101
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
102
|
+
def deliver(message, to)
|
|
103
|
+
if to.present?
|
|
104
|
+
message['to'] = to
|
|
105
|
+
ActionCable.server.broadcast(user_stream(to), message)
|
|
106
|
+
else
|
|
107
|
+
broadcast(message)
|
|
108
|
+
end
|
|
145
109
|
end
|
|
146
110
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def log_analytics(event, user_id, room_id)
|
|
152
|
-
$fenetre_analytics ||= []
|
|
153
|
-
$fenetre_analytics << [event, user_id, room_id]
|
|
154
|
-
end
|
|
111
|
+
def moderate(type, data)
|
|
112
|
+
return unless @accepted && @joined && user_role == :host && data['user_id'].present?
|
|
155
113
|
|
|
156
|
-
|
|
157
|
-
|
|
114
|
+
deliver({ 'type' => type, 'from' => current_user.id,
|
|
115
|
+
'payload' => { 'user_id' => data['user_id'] } }, data['user_id'])
|
|
158
116
|
end
|
|
159
117
|
|
|
160
|
-
def
|
|
161
|
-
|
|
118
|
+
def instrument(event)
|
|
119
|
+
ActiveSupport::Notifications.instrument("#{event}.fenetre", user_id: current_user.id, room_id: @room_id)
|
|
162
120
|
end
|
|
163
121
|
end
|
|
164
122
|
end
|