Inayatgaming commited on
Commit
0978ad0
Β·
verified Β·
1 Parent(s): df0e978

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +304 -0
app.js ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // app.js
2
+ (() => {
3
+ const WS_URI = "wss://promptly-xnm3.onrender.com";
4
+ const statusEl = document.getElementById("status");
5
+ const connectBtn = document.getElementById("connectBtn");
6
+ const disconnectBtn = document.getElementById("disconnectBtn");
7
+ const modeText = document.getElementById("modeText");
8
+ const messagesEl = document.getElementById("messages");
9
+ const inputEl = document.getElementById("input");
10
+ const sendBtn = document.getElementById("sendBtn");
11
+ const fileBtn = document.getElementById("fileBtn");
12
+ const filePicker = document.getElementById("filePicker");
13
+ const loginOverlay = document.getElementById("loginOverlay");
14
+ const loginForm = document.getElementById("loginForm");
15
+ const loginUsername = document.getElementById("loginUsername");
16
+ const loginPassword = document.getElementById("loginPassword");
17
+ const loginGuest = document.getElementById("loginGuest");
18
+ const themeToggle = document.getElementById("themeToggle");
19
+ const root = document.getElementById("root");
20
+ const onlineCountEl = document.getElementById("onlineCount");
21
+
22
+ let ws = null;
23
+ let handshakeStep = 0;
24
+ let awaiting = null;
25
+ let lastUsername = "";
26
+ let connected = false;
27
+ let pendingLogin = null; // {username,password}
28
+
29
+ // Utility
30
+ function stripAnsi(str){ return String(str).replace(/\x1b\[[0-9;]*m/g,''); }
31
+ function setStatus(connectedNow){
32
+ connected = !!connectedNow;
33
+ if(connected){
34
+ statusEl.classList.remove("disconnected");
35
+ statusEl.classList.add("connected");
36
+ statusEl.textContent = "Connected";
37
+ disconnectBtn.disabled = false;
38
+ connectBtn.disabled = true;
39
+ }else{
40
+ statusEl.classList.remove("connected");
41
+ statusEl.classList.add("disconnected");
42
+ statusEl.textContent = "Disconnected";
43
+ disconnectBtn.disabled = true;
44
+ connectBtn.disabled = false;
45
+ }
46
+ }
47
+
48
+ function appendMessage(content, cls='system'){
49
+ const el = document.createElement('div');
50
+ el.className = 'msg ' + (cls === 'me' ? 'me' : (cls === 'file' ? 'file' : 'system'));
51
+ if(typeof content === 'string'){
52
+ el.textContent = content;
53
+ } else if(content.type === 'file'){
54
+ // content: {filename, blob, isImage}
55
+ const meta = document.createElement('div');
56
+ meta.className = 'meta';
57
+ meta.textContent = `πŸ“₯ Received ${content.filename}`;
58
+ el.appendChild(meta);
59
+ if(content.isImage){
60
+ const wrap = document.createElement('a');
61
+ wrap.href = URL.createObjectURL(content.blob);
62
+ wrap.download = content.filename;
63
+ wrap.className = 'img-wrap';
64
+ const img = document.createElement('img');
65
+ img.src = wrap.href;
66
+ img.alt = content.filename;
67
+ wrap.appendChild(img);
68
+ el.appendChild(wrap);
69
+ // download link below
70
+ const dl = document.createElement('a');
71
+ dl.href = wrap.href;
72
+ dl.download = content.filename;
73
+ dl.className = 'download-link muted';
74
+ dl.textContent = `Download ${content.filename}`;
75
+ el.appendChild(dl);
76
+ // revoke after a bit when user navigates away
77
+ setTimeout(()=>URL.revokeObjectURL(wrap.href), 60000);
78
+ } else {
79
+ const dl = document.createElement('a');
80
+ dl.href = URL.createObjectURL(content.blob);
81
+ dl.download = content.filename;
82
+ dl.className = 'download-link muted';
83
+ dl.textContent = `Download ${content.filename}`;
84
+ el.appendChild(dl);
85
+ setTimeout(()=>URL.revokeObjectURL(dl.href), 60000);
86
+ }
87
+ } else {
88
+ el.textContent = JSON.stringify(content);
89
+ }
90
+ messagesEl.appendChild(el);
91
+ messagesEl.scrollTo({top: messagesEl.scrollHeight, behavior: 'smooth'});
92
+ }
93
+
94
+ function connect(){
95
+ if(ws) try{ ws.close() }catch(e){}
96
+ ws = new WebSocket(WS_URI);
97
+ ws.binaryType = 'arraybuffer';
98
+ ws.addEventListener('open', ()=>{
99
+ setStatus(true);
100
+ appendMessage('🟒 Connected to ' + WS_URI);
101
+ handshakeStep = 0;
102
+ awaiting = null;
103
+ modeText.textContent = 'Handshake';
104
+ // if pending login already provided, we will act when user submits login
105
+ });
106
+ ws.addEventListener('message', (ev) => {
107
+ let data = ev.data;
108
+ if(data instanceof ArrayBuffer){
109
+ // binary from server: try to treat as file (unlikely); skip for now
110
+ appendMessage('πŸ“Ž Received binary data (unsupported preview)', 'system');
111
+ return;
112
+ } else {
113
+ data = stripAnsi(String(data));
114
+ }
115
+
116
+ // Detect server-sent file marker: 'πŸ“₯ filename base64'
117
+ if(data.startsWith('πŸ“₯')){
118
+ const m = data.match(/^πŸ“₯\s+(\S+)\s+([\s\S]+)$/);
119
+ if(m){
120
+ const fname = m[1];
121
+ const b64 = m[2];
122
+ try{
123
+ const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
124
+ const blob = new Blob([bytes]);
125
+ const isImage = /\.(png|jpe?g|gif|webp|svg)$/i.test(fname);
126
+ appendMessage({type:'file', filename: fname, blob, isImage}, 'file');
127
+ return;
128
+ }catch(e){
129
+ appendMessage('❌ File parse error: ' + e.toString());
130
+ }
131
+ }
132
+ }
133
+
134
+ appendMessage(data, 'system');
135
+ // handshake flow handling
136
+ if(data.toLowerCase().includes('please enter password') || data.toLowerCase().includes('enter password')){
137
+ awaiting = 'password';
138
+ modeText.textContent = 'Entering password';
139
+ } else if(data.toLowerCase().includes('enter your username') || data.toLowerCase().includes('enter username')){
140
+ awaiting = 'username';
141
+ modeText.textContent = 'Entering username';
142
+ } else if(data.toLowerCase().includes('welcome') || data.toLowerCase().includes('joined the chat')){
143
+ awaiting = null;
144
+ modeText.textContent = 'Chat';
145
+ } else if(data.toLowerCase().includes('online users')){
146
+ // try to parse count
147
+ const m = data.match(/Online users\s*\(?(\d+)\)?/i);
148
+ if(m) onlineCountEl.textContent = m[1];
149
+ }
150
+ });
151
+
152
+ ws.addEventListener('close', ()=>{
153
+ appendMessage('πŸ”΄ Connection closed.');
154
+ setStatus(false);
155
+ ws = null;
156
+ });
157
+
158
+ ws.addEventListener('error', (e)=>{
159
+ appendMessage('❌ Connection error. Check server & console.');
160
+ setStatus(false);
161
+ });
162
+ }
163
+
164
+ function disconnect(){
165
+ if(ws) try{ ws.close(); }catch(e){}
166
+ ws = null;
167
+ setStatus(false);
168
+ appendMessage('Disconnected by client.');
169
+ }
170
+
171
+ // send helper
172
+ function sendText(msg){
173
+ if(!ws || ws.readyState !== WebSocket.OPEN){
174
+ appendMessage('❗ Not connected.', 'system');
175
+ return;
176
+ }
177
+ ws.send(msg);
178
+ appendMessage(`[You] ${msg}`, 'me');
179
+ }
180
+
181
+ // file upload: read file -> base64 -> send as /send filename base64 (server expects same)
182
+ filePicker.addEventListener('change', async (e)=>{
183
+ const f = e.target.files && e.target.files[0];
184
+ if(!f) return;
185
+ const reader = new FileReader();
186
+ reader.onload = () => {
187
+ try{
188
+ const ab = reader.result;
189
+ const bytes = new Uint8Array(ab);
190
+ let binary = '';
191
+ const chunk = 0x8000;
192
+ for(let i=0;i<bytes.length;i+=chunk){
193
+ const sub = bytes.subarray(i, Math.min(i+chunk, bytes.length));
194
+ binary += String.fromCharCode.apply(null, sub);
195
+ }
196
+ const b64 = btoa(binary);
197
+ const payload = `/send ${f.name} ${b64}`;
198
+ if(ws && ws.readyState === WebSocket.OPEN){
199
+ ws.send(payload);
200
+ appendMessage(`[file] ${f.name}`, 'me');
201
+ } else appendMessage('❗ Not connected.', 'system');
202
+ }catch(err){
203
+ appendMessage('❌ File read error: ' + err.toString(), 'system');
204
+ }
205
+ };
206
+ reader.onerror = ()=> appendMessage('❌ Could not read file.', 'system');
207
+ reader.readAsArrayBuffer(f);
208
+ });
209
+
210
+ fileBtn.addEventListener('click', ()=> filePicker.click());
211
+
212
+ // login handling: send password then username preserving handshake order
213
+ loginForm.addEventListener('submit', (ev)=>{
214
+ ev.preventDefault();
215
+ const username = loginUsername.value.trim();
216
+ const password = loginPassword.value;
217
+ pendingLogin = {username, password};
218
+ // ensure connected
219
+ if(!ws || ws.readyState !== WebSocket.OPEN){
220
+ appendMessage('Connecting to server...', 'system');
221
+ connect();
222
+ // wait until open
223
+ const waitOpen = () => new Promise(res=>{
224
+ if(ws && ws.readyState === WebSocket.OPEN) return res();
225
+ const t = setInterval(()=>{ if(ws && ws.readyState === WebSocket.OPEN){ clearInterval(t); res(); } }, 150);
226
+ // timeout fallback
227
+ setTimeout(()=>res(), 5000);
228
+ });
229
+ waitOpen().then(()=> doHandshakeLogin(pendingLogin));
230
+ } else doHandshakeLogin(pendingLogin);
231
+ });
232
+
233
+ loginGuest.addEventListener('click', ()=>{
234
+ loginUsername.value = 'Guest' + Math.floor(Math.random()*9000+1000);
235
+ loginPassword.value = '';
236
+ loginForm.dispatchEvent(new Event('submit', {cancelable:true}));
237
+ });
238
+
239
+ function doHandshakeLogin({username,password}){
240
+ // send password then username with slight delay to emulate terminal client handshake
241
+ try{
242
+ if(password) ws.send(password);
243
+ appendMessage('[You] (password entered)', 'me');
244
+ setTimeout(()=>{
245
+ ws.send(username);
246
+ appendMessage('[You] ' + username, 'me');
247
+ // hide login overlay and switch to chat
248
+ loginOverlay.style.display = 'none';
249
+ // keep username stored
250
+ lastUsername = username;
251
+ modeText.textContent = 'Chat';
252
+ inputEl.focus();
253
+ }, 300);
254
+ }catch(e){
255
+ appendMessage('❌ Login send error: ' + e.toString(), 'system');
256
+ }
257
+ }
258
+
259
+ // message send
260
+ sendBtn.addEventListener('click', ()=>{
261
+ const txt = inputEl.value.trim();
262
+ if(!txt) return;
263
+ sendText(txt);
264
+ inputEl.value = '';
265
+ });
266
+
267
+ inputEl.addEventListener('keydown', (e)=>{
268
+ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendBtn.click(); }
269
+ });
270
+
271
+ // connect/disconnect controls
272
+ connectBtn.addEventListener('click', ()=> connect());
273
+ disconnectBtn.addEventListener('click', ()=> disconnect());
274
+
275
+ // theme toggle
276
+ themeToggle.addEventListener('click', ()=>{
277
+ if(root.classList.contains('theme-dark')){
278
+ root.classList.remove('theme-dark'); root.classList.add('theme-light');
279
+ document.documentElement.style.setProperty('--bg-dark','#f5f7fb');
280
+ document.documentElement.style.setProperty('--text','#071029');
281
+ themeToggle.textContent = '🌞';
282
+ } else {
283
+ root.classList.remove('theme-light'); root.classList.add('theme-dark');
284
+ themeToggle.textContent = 'πŸŒ—';
285
+ document.documentElement.style.removeProperty('--bg-dark');
286
+ document.documentElement.style.removeProperty('--text');
287
+ }
288
+ });
289
+
290
+ // auto-connect on load
291
+ window.addEventListener('load', ()=>{
292
+ connect();
293
+ // show login overlay (auto)
294
+ loginOverlay.style.display = 'flex';
295
+ loginUsername.focus();
296
+ // register sw if available
297
+ if('serviceWorker' in navigator){
298
+ navigator.serviceWorker.register('service-worker.js').catch(()=>{/*ignore*/});
299
+ }
300
+ });
301
+
302
+ // expose for debugging
303
+ window._promptly = {connect,disconnect, wsRef: ()=> ws};
304
+ })();