/
index.html
377 lines (342 loc) · 11.8 KB
/
index.html
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name=viewport content=width=device-width,initial-scale=1,user-scalable=no>
<link rel=icon href=./favicon.ico>
<title>Janus - P2P Blog & Chat</title>
<script src="https://cdn.jsdelivr.net/npm/latent-peer@10.1/build.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
}
#chat {
max-width: 600px;
margin: 20px auto;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.input-line {
display: flex;
}
.input-line > * {
margin: 0;
}
#chat-window {
box-sizing: border-box;
width: 100%;
height: 300px;
font-size: 1rem;
background-color: #eaeaea;
border: none;
padding: 10px;
overflow-y: auto;
overflow-x: hidden;
resize: none;
border-radius: 0.25rem;
}
#chat-input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
#send-button {
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
}
#send-button:hover {
background-color: #0056b3;
}
#blog {
max-width: 600px;
margin: 20px auto;
}
:is(input, button):active {
outline: 1px solid blue;
}
.connection-status {
text-align: center;
margin-top: 20px;
}
.progress-bar {
width: 100%;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-indeterminate {
width: 30%;
height: 5px;
background-color: #007bff;
animation: progress-indeterminate 2s linear infinite;
}
@keyframes progress-indeterminate {
0% {
margin-left: -30%;
margin-right: 100%;
}
50% {
margin-left: 100%;
margin-right: -30%;
}
100% {
margin-left: 100%;
margin-right: -30%;
}
}
</style>
</head>
<body>
<div class="connection-status" style="cursor: wait;">Generating your p2p locator...</div>
<div class="progress-bar">
<div class="progress-indeterminate"></div>
</div>
<div id="chat">
<textarea id="chat-window" readonly></textarea>
<div class=input-line>
<input autofocus required type="text" id="chat-input" placeholder="" maxlength=1024>
<button id="send-button">Send</button>
</div>
</div>
<section id="blog"></section> <!-- Blog content will be inserted here -->
<script>
window.addEventListener('beforeunload', function (e) {
// Cancel the event
e.preventDefault();
// Chrome requires returnValue to be set
e.returnValue = 'Leaving this page will destroy the P2P connection. Are you sure?';
});
</script>
<script>
const Decoder = new TextDecoder('utf-8');
const urlParams = new URL(location).searchParams;
const signalData = urlParams.get('data');
const issueUrl = urlParams.get('issue');
const alertBoxMessage = `Your p2p locator is now in your clipboard. Paste it into the comment on the issue.\n\nWhen you close this dialog we will return you to the issue.\n\nPlease note: if you close this chat page you will end the p2p connection.`;
let peer;
let stream;
let chatWindow;
let input;
let status;
let send;
let signalWindow;
document.addEventListener('DOMContentLoaded', async () => {
chatWindow = document.getElementById('chat-window');
input = document.getElementById('chat-input');
status = document.querySelector('.connection-status');
progressBar = document.querySelector('.progress-bar');
send = document.getElementById('send-button');
send.addEventListener('click', () => {
const message = input.value.trim().substring(0, 1024); // Limit to 1024 chars
if (message && peer) {
peer.send(JSON.stringify({ type: 'chat', msg: message }));
chatWindow.value += `Me: ${message}\n`; // Display own messages
input.value = '';
input.focus();
scrollToBottom(chatWindow);
}
});
input.addEventListener('keypress', pressed => {
if ( pressed.key == "Enter" ) {
send.click();
}
});
if (signalData) {
if ( isSafariBrowser() ) {
await requestMediaAccess();
}
let maxRetries = 1;
let i = 0;
while(! (await establishConnection())) {
console.log(`Retrying in 3 seconds...`);
await sleep(3000);
if ( i++ > maxRetries ) break;
}
async function establishConnection() {
let resolve;
const pr = new Promise(res => resolve = res);
peer = new SimplePeer({
trickle: false,
initiator: false,
});
peer.on('signal', async data => {
const signalData = btoa(JSON.stringify(data));
const janusLink = `janus://p2p_locator/${signalData}`;
requestAnimationFrame(() => {
status.innerText = "p2p locator ready!";
status.style.cursor = 'default';
progressBar.remove();
});
await copyToClipboard(janusLink, () => {
alert(alertBoxMessage);
signalWindow = window.open(issueUrl, '_blank');
});
});
peer.on('connect', () => {
console.log('Connected to peer!');
status.innerText = 'Connected';
chatWindow.value = "";
window.focus();
let hasAlerted = false;
setTimeout(() => resolve(true), 0);
// Stop any media as it's not used
if ( stream ) {
setTimeout(() => stream.getTracks().forEach(track => track.stop()), 15000);
}
// close the signal window
signalWindow?.close?.();
function showAlert() {
if (!hasAlerted) {
setTimeout(() => alert('Connected'), 500);
hasAlerted = true;
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible') {
showAlert();
}
}, { once: true });
window.addEventListener("focus", showAlert, { once: true });
});
peer.on('data', data => {
data = data.toString();
console.log('Received message:', data);
try {
const message = JSON.parse(data);
if (message.type === 'blog') {
document.getElementById('blog').innerHTML = message.content;
} else if (message.type === 'chat') {
chatWindow.value += `${message.handle || 'Me'}: ${message.msg}\n`;
scrollToBottom(chatWindow);
}
} catch(e) {
console.warn(`Could not parse message:`, e, JSON.stringify(data).slice(0, 140));
}
});
peer.on('close', () => {
console.info(`P2P closed`);
resolve(false);
setTimeout(() => stream.getTracks().forEach(track => track.stop()), 0);
status.innerText = 'Disconnected';
});
peer.on('error', err => {
console.error('P2P Error:', err);
resolve(false);
setTimeout(() => stream.getTracks().forEach(track => track.stop()), 0);
status.innerText = 'Disconnected';
});
try {
const decodedData = JSON.parse(atob(signalData));
peer._pc.restartIce();
peer.signal(decodedData);
} catch (err) {
console.error('Error parsing signaling data:', err);
}
return pr;
}
} else {
console.error('No signaling data found in URL.');
}
});
async function copyToClipboard(text, onSuccess) {
const tryClipboardCopy = async () => {
try {
await navigator.clipboard.writeText(text);
onSuccess();
} catch (err) {
console.error('Could not copy signal data to clipboard:', err);
// Fallback to execCommand in Safari or manual copy
tryExecCommandCopy();
}
};
const tryExecCommandCopy = () => {
const chatWindow = document.querySelector('#chat-window');
chatWindow.value = text;
chatWindow.focus();
chatWindow.select();
try {
if (document.execCommand('copy')) {
onSuccess();
} else {
throw new Error('ExecCommand failed');
}
} catch (e) {
console.error('Fallback copy method failed', e);
alert('Please manually copy the selected text in the textbox');
setupManualCopyListener();
}
};
const setupManualCopyListener = () => {
document.addEventListener('copy', function onCopy(event) {
if (event.target === document.querySelector('#chat-window')) {
document.removeEventListener('copy', onCopy);
onSuccess();
}
});
};
// Safari needs a user gesture to allow clipboard API
if (isSafariBrowser() || !window.isSecureContext) {
await sleep(300);
alert('When you close this dialog, please tap the screen to copy your locator to the clipboard.');
document.onclick = async () => {
document.onclick = null; // Remove this handler after the click
await tryClipboardCopy();
};
} else {
await tryClipboardCopy();
}
}
async function sleep(ms) {
return new Promise(res => setTimeout(res, ms));
}
function isSafariBrowser() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
function scrollToBottom(element) {
element.scroll({
top: element.scrollHeight,
behavior: 'smooth'
});
}
// this func is needed due to a quirk in mobile Safari
async function requestMediaAccess() {
try {
// Check if the device is mobile
if (deviceIsMobile()) {
// Check current permission states for microphone and camera
const micPerm = await navigator.permissions.query({ name: 'microphone' });
if (micPerm.state !== 'granted') {
// Show explainer only if permissions haven't been granted yet
alert('Due to a quirk in iOS Safari, we need media permissions to enable P2P chat. ' +
'We will stop the tracks immediately, as we don\'t use the stream.');
}
// Request media access for audio on mobile devices
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// If permissions were not already granted, inform the user they can revoke them
if (micPerm.state !== 'granted') {
alert(`Media access granted.\n\nIf you don't want to be asked again, you can click on the red mic icon, then on Website Settings, then on Audio, and click Allow.\n\nFor privacy we will shut your mic as soon as the connection is made, and the mic icon will disappear.\n\nBut for convenience, we'll give you around 10 seconds after that, in case you wish to click the icon to set Allow.`);
}
}
} catch (error) {
console.error('Media access error:', error);
alert('Unfortunately, in Mobile Safari we can not create the P2P connection without media access. You may reload to try again if you wish.');
throw new Error('Failed to get media access');
}
}
// Utility function to determine if the device is mobile
function deviceIsMobile() {
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
}
</script>
</body>
</html>