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.
@@ -1,225 +1,286 @@
1
- /* Fenetre Video Chat Styles - Dark and Light Themes */
1
+ /* Fenetre Video Chat — stage-first layout for Zoom/Skype/Jitsi-class calls */
2
+
2
3
  .fenetre-video-chat-container {
3
- border-radius: 12px;
4
- box-shadow: 0 2px 16px rgba(0,0,0,0.18);
5
- padding: 1.5rem;
6
- max-width: 700px;
7
- margin: 2rem auto;
8
- font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
9
- transition: background 0.2s, color 0.2s;
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
- .fenetre-theme-dark {
13
- background: #181c20;
14
- color: #f3f6fa;
15
- }
16
- .fenetre-theme-dark h3 {
17
- color: #e0e6ef;
18
- }
19
- .fenetre-theme-dark ul#fenetre-participant-list,
20
- .fenetre-theme-dark #fenetre-chat-box {
21
- background: #23272e;
22
- color: #f3f6fa;
23
- }
24
- .fenetre-theme-dark ul#fenetre-participant-list li {
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
- background: #f7fafd;
34
- color: #23272e;
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
- .fenetre-theme-light h3 {
37
- color: #23272e;
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
- .fenetre-theme-light ul#fenetre-participant-list,
40
- .fenetre-theme-light #fenetre-chat-box {
41
- background: #e9eef3;
42
- color: #23272e;
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
- .fenetre-theme-light ul#fenetre-participant-list li {
45
- border-bottom: 1px solid #e9eef3;
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
- .fenetre-theme-light video {
48
- border: 2px solid #e9eef3;
49
- background: #fff;
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-top: 1.2em;
54
- margin-bottom: 0.5em;
81
+ margin: 0.25rem 0;
82
+ font-size: 0.8rem;
55
83
  font-weight: 600;
56
- letter-spacing: 0.01em;
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
- min-height: 2em;
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
- margin-top: 1em;
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
- /* Connection status styles */
90
- .fenetre-connection-status {
91
- display: inline-block;
92
- padding: 4px 8px;
93
- border-radius: 12px;
94
- font-size: 0.8rem;
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-status-connecting {
100
- background-color: #ffeeba;
101
- color: #856404;
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-status-connected {
105
- background-color: #d4edda;
106
- color: #155724;
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-status-disconnected {
110
- background-color: #f8d7da;
111
- color: #721c24;
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-status-reconnecting {
115
- background-color: #e2e3e5;
116
- color: #383d41;
117
- animation: pulse 1.5s infinite;
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-status-error {
121
- background-color: #f8d7da;
122
- color: #721c24;
123
- font-weight: 600;
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: 5px;
130
- right: 5px;
131
- border-radius: 8px;
132
- padding: 2px 6px;
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.5);
163
+ background: rgba(0, 0, 0, 0.55);
135
164
  color: white;
136
165
  }
137
166
 
138
- .fenetre-peer-connecting {
139
- background-color: rgba(255, 193, 7, 0.8);
140
- color: #212529;
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-peer-connected {
144
- background-color: rgba(40, 167, 69, 0.8);
145
- color: white;
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-peer-disconnected, .fenetre-peer-failed {
149
- background-color: rgba(220, 53, 69, 0.8);
150
- color: white;
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
- /* Remote video container styling */
154
- .fenetre-remote-video-container {
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-color: #17a2b8;
163
- color: white;
205
+ background: var(--fenetre-accent);
206
+ color: #fff;
164
207
  }
165
208
 
166
- .fenetre-theme-dark .fenetre-control-button.screen-sharing {
167
- background-color: #138496;
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-theme-light .fenetre-control-button.screen-sharing {
171
- background-color: #17a2b8;
215
+ .fenetre-chat-container {
216
+ display: flex;
217
+ flex-direction: column;
218
+ flex: 1;
219
+ min-height: 0;
172
220
  }
173
221
 
174
- /* Animation for reconnecting status */
175
- @keyframes pulse {
176
- 0% {
177
- opacity: 1;
178
- }
179
- 50% {
180
- opacity: 0.5;
181
- }
182
- 100% {
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
- /* Control buttons styles */
188
- .fenetre-controls {
189
- display: flex;
190
- gap: 10px;
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-control-button {
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
- align-items: center;
203
- justify-content: center;
204
- transition: all 0.2s;
241
+ gap: 0.4rem;
242
+ margin-top: 0.5rem;
205
243
  }
206
244
 
207
- .fenetre-theme-dark .fenetre-control-button {
208
- background-color: #343a40;
209
- color: #f8f9fa;
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-theme-light .fenetre-control-button {
213
- background-color: #e9ecef;
214
- color: #212529;
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
- .fenetre-control-button:hover {
218
- opacity: 0.9;
265
+ @keyframes fenetre-pulse {
266
+ 0%, 100% { opacity: 1; }
267
+ 50% { opacity: 0.55; }
219
268
  }
220
269
 
221
- .fenetre-control-button.video-off,
222
- .fenetre-control-button.audio-off {
223
- background-color: #dc3545;
224
- color: white;
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
- # Handles signaling messages for WebRTC
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 if @room_id.blank?
11
+ @room_id = params[:room_id].to_s
12
+ return reject unless Fenetre.authorize_room.call(current_user, @room_id)
13
13
 
14
- init_participants(@room_id)
15
- return reject if params[:room_locked] && user_role != :host
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
- stream_from room_stream(@room_id)
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
- return unless @room_id
23
+ @accepted = false
24
+ return unless @joined
23
25
 
24
- remove_participant(@room_id, current_user.id)
25
- broadcast_leave(@room_id, current_user.id)
26
- log_analytics(:leave, current_user.id, @room_id)
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 signal(data)
30
- return unless @room_id
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
- def join_room(_data)
41
- return unless @room_id
42
-
43
- add_participant(@room_id, current_user.id)
44
- message = {
45
- 'type' => 'join',
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
- user_id = data['user_id'] || data[:user_id]
60
- ActionCable.server.broadcast(room_stream(@room_id),
61
- { 'type' => 'kick', 'from' => current_user.id,
62
- 'payload' => { 'user_id' => user_id } })
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 mute(data)
66
- return unless user_role == :host
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
- def unmute(data)
75
- return unless user_role == :host
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
- user_id = data['user_id'] || data[:user_id]
78
- ActionCable.server.broadcast(room_stream(@room_id),
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 @room_id
61
+ return unless @accepted && @joined
62
+ return if data['to'].present? && !@options[:enable_private_chat]
85
63
 
86
- init_participants(@room_id)
87
- return if (data['to'] || data[:to]) && !params[:enable_private_chat]
64
+ message = data['message'].to_s.strip.each_char.take(2000).join
65
+ return if message.empty?
88
66
 
89
- message = data['message'] || data[:message]
90
- to = data['to'] || data[:to]
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 perform(action, data = {})
97
- raise NoMethodError, "undefined action '#{action}' for #{self.class}" unless respond_to?(action)
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 rejected?
108
- !!@_rejected
76
+ def mute(data)
77
+ moderate('mute', data)
109
78
  end
110
79
 
111
- def confirmed?
112
- !rejected?
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 participants(room_id)
130
- @@participants[room_id] ||= []
90
+ def room_stream
91
+ "fenetre_video_chat_#{@room_id}"
131
92
  end
132
93
 
133
- def add_participant(room_id, user_id)
134
- @@participants[room_id] << user_id unless @@participants[room_id].include?(user_id)
94
+ def user_stream(user_id)
95
+ "fenetre_user_#{[@room_id, user_id.to_s].to_json}"
135
96
  end
136
97
 
137
- def remove_participant(room_id, user_id)
138
- @@participants[room_id]&.delete(user_id)
98
+ def broadcast(message)
99
+ ActionCable.server.broadcast(room_stream, message)
139
100
  end
140
101
 
141
- def broadcast_leave(room_id, user_id)
142
- ActionCable.server.broadcast(room_stream(room_id),
143
- { 'type' => 'leave', 'from' => user_id,
144
- 'participants' => participants(room_id).dup })
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 broadcast_signal(room_id, type, from, payload)
148
- ActionCable.server.broadcast(room_stream(room_id), { 'type' => type, 'from' => from, 'payload' => payload })
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
- def stringify_keys(hash)
157
- hash.transform_keys(&:to_s)
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 broadcast_to_room(message)
161
- ActionCable.server.broadcast(room_stream(@room_id), stringify_keys(message))
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