@decentnetwork/lan 0.1.87 → 0.1.88

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.
Binary file
Binary file
Binary file
Binary file
package/dist/ui/server.js CHANGED
@@ -62,8 +62,22 @@ export function startFriendUi(opts) {
62
62
  sendJson(res, 200, { me, friends, pending: pend });
63
63
  return;
64
64
  }
65
+ // Rich friend list (alias / status / unread / last message). Falls back
66
+ // to the diag-derived list on older daemons that lack the op.
67
+ if (req.method === "GET" && url === "/api/friends-list") {
68
+ const r = await opts.call({ op: "friends-list" });
69
+ if (r.ok) {
70
+ sendJson(res, 200, r.data ?? { friends: [] });
71
+ return;
72
+ }
73
+ const diag = await opts.call({ op: "diag" });
74
+ const friends = diag.ok ? (diag.data?.friends ?? []) : [];
75
+ sendJson(res, 200, { friends });
76
+ return;
77
+ }
65
78
  if (req.method === "GET" && url === "/api/chat-history") {
66
- const r = await opts.call({ op: "chat-history" });
79
+ const peer = new URL(req.url || "/", "http://x").searchParams.get("peer") || undefined;
80
+ const r = await opts.call({ op: "chat-history", userid: peer });
67
81
  sendJson(res, 200, r.ok ? (r.data ?? { chats: {} }) : { chats: {} });
68
82
  return;
69
83
  }
@@ -73,6 +87,24 @@ export function startFriendUi(opts) {
73
87
  sendJson(res, r.ok ? 200 : 400, r);
74
88
  return;
75
89
  }
90
+ if (req.method === "POST" && url === "/api/chat-mark-read") {
91
+ const { userid } = await readBody(req);
92
+ const r = await opts.call({ op: "chat-mark-read", userid });
93
+ sendJson(res, r.ok ? 200 : 400, r);
94
+ return;
95
+ }
96
+ if (req.method === "POST" && url === "/api/friend-remove") {
97
+ const { userid } = await readBody(req);
98
+ const r = await opts.call({ op: "friend-remove", userid });
99
+ sendJson(res, r.ok ? 200 : 400, r);
100
+ return;
101
+ }
102
+ if (req.method === "POST" && url === "/api/friend-alias") {
103
+ const { userid, alias } = await readBody(req);
104
+ const r = await opts.call({ op: "friend-set-alias", userid, alias });
105
+ sendJson(res, r.ok ? 200 : 400, r);
106
+ return;
107
+ }
76
108
  if (req.method === "GET" && url === "/api/routes") {
77
109
  let routes = { regions: [], default: "direct" };
78
110
  if (existsSync(opts.routesPath)) {
@@ -205,13 +237,19 @@ function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg
205
237
 
206
238
  async function api(path, body){ const r = await fetch(path, body?{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}:{}); return r.json(); }
207
239
 
208
- window.friendsById = {};
240
+ window.friendsById = {}; window.me = {};
209
241
  async function refresh(){
210
- let s; try { s = await api('/api/state'); } catch(e){ return; }
211
- const pend = s.pending || [], fr = s.friends || [], me = s.me || {};
212
- document.getElementById('me').textContent = me.ip
213
- ? ('My IP: ' + me.ip + (me.userid ? ' · ' + short(me.userid) : ''))
214
- : (me.userid ? ('userid: ' + short(me.userid) + ' · lan/TUN not up') : 'daemon: no identity');
242
+ let s, fl;
243
+ try { [s, fl] = await Promise.all([api('/api/state'), api('/api/friends-list')]); } catch(e){ return; }
244
+ const pend = s.pending || [], me = s.me || {};
245
+ const fr = (fl && fl.friends) || s.friends || [];
246
+ window.me = me;
247
+ const idStr = me.address || me.userid || '';
248
+ document.getElementById('me').innerHTML =
249
+ (me.ip ? ('My IP: <b>' + esc(me.ip) + '</b> · ') : (me.userid ? '' : 'daemon: no identity '))
250
+ + (me.userid ? ('<span title="'+esc(me.userid)+'">'+esc(short(me.userid))+'</span> ') : '')
251
+ + (idStr ? '<button onclick="copyId()" style="padding:.1rem .5rem;font-size:.75rem">copy my address</button>' : '')
252
+ + (me.userid && !me.installed ? ' · <span style="color:#e67e22">lan/TUN not up</span>' : '');
215
253
  fr.forEach(f => { window.friendsById[f.userid||f.carrierId] = f; });
216
254
  document.getElementById('pcount').textContent = pend.length ? '('+pend.length+')' : '';
217
255
  document.getElementById('fcount').textContent = fr.length ? '('+fr.length+')' : '';
@@ -222,13 +260,24 @@ async function refresh(){
222
260
  <button class="accept" onclick="act('accept','\${esc(p.userid)}')">Accept</button>
223
261
  <button class="reject" onclick="act('reject','\${esc(p.userid)}')">Reject</button>
224
262
  </div>\`).join('') : '<div class="empty">No pending requests.</div>';
225
- document.getElementById('friends').innerHTML = fr.length ? fr.map(f => \`
226
- <div class="row" style="cursor:pointer" onclick="openChat('\${esc(f.userid||f.carrierId)}')" title="open chat">
263
+ document.getElementById('friends').innerHTML = fr.length ? fr.map(f => {
264
+ const uid = f.userid||f.carrierId;
265
+ const nm = f.alias || f.name || short(uid);
266
+ const lm = f.lastMessage;
267
+ const preview = lm ? ((lm.dir==='out'?'You: ':'') + lm.text) : (f.status||'offline');
268
+ const badge = f.unread ? '<span style="background:#e74c3c;color:#fff;border-radius:10px;padding:0 .45rem;font-size:.7rem;margin-left:.4rem">'+f.unread+'</span>' : '';
269
+ return \`<div class="row" style="cursor:pointer" onclick="openChat('\${esc(uid)}')" title="open chat">
227
270
  <span class="dot \${esc(f.status||'offline')}"></span>
228
- <div class="meta"><div class="name">\${esc(f.name || 'unnamed')}</div>
229
- <div class="sub">\${esc(f.status||'')} · \${esc(short(f.userid||f.carrierId))}\${f.virtualIp? ' · '+esc(f.virtualIp):''}</div></div>
230
- </div>\`).join('') : '<div class="empty">No friends yet.</div>';
271
+ <div class="meta"><div class="name">\${esc(nm)}\${badge}</div>
272
+ <div class="sub">\${esc(preview)}</div></div>
273
+ <button onclick="event.stopPropagation();editAlias('\${esc(uid)}')" title="rename" style="padding:.2rem .55rem">✎</button>
274
+ <button class="reject" onclick="event.stopPropagation();delFriend('\${esc(uid)}')" title="remove friend" style="padding:.2rem .55rem">×</button>
275
+ </div>\`;
276
+ }).join('') : '<div class="empty">No friends yet.</div>';
231
277
  }
278
+ function copyId(){ const id=(window.me&&(window.me.address||window.me.userid))||''; if(id&&navigator.clipboard){ navigator.clipboard.writeText(id); toast('Your address copied — share it so others can add you'); } }
279
+ async function delFriend(uid){ if(!confirm('Remove this friend and your messages with them?')) return; const r=await api('/api/friend-remove',{userid:uid}); toast(r.ok?'Removed':(r.error||'failed')); if(chatWith===uid) closeChat(); refresh(); }
280
+ async function editAlias(uid){ const cur=(window.friendsById[uid]||{}).alias||''; const a=prompt('Local name for this friend (empty to clear):', cur); if(a===null) return; const r=await api('/api/friend-alias',{userid:uid,alias:a}); toast(r.ok?'Saved':(r.error||'failed')); refresh(); }
232
281
  async function act(kind, userid){ const r = await api('/api/'+kind, {userid}); toast(r.ok? (kind==='accept'?'Accepted':'Rejected') : (r.error||'failed')); refresh(); }
233
282
  async function addFriend(){ const a=document.getElementById('addr'); const v=a.value.trim(); if(!v) return; const r=await api('/api/add',{address:v}); toast(r.ok?'Friend-request sent':(r.error||'failed')); if(r.ok) a.value=''; refresh(); }
234
283
  document.getElementById('addr').addEventListener('keydown', e=>{ if(e.key==='Enter') addFriend(); });
@@ -237,9 +286,10 @@ let chatWith = null, chatTimer = null;
237
286
  async function openChat(userid){
238
287
  chatWith = userid;
239
288
  const f = window.friendsById[userid] || {};
240
- document.getElementById('chatName').textContent = f.name || 'unnamed';
289
+ document.getElementById('chatName').textContent = f.alias || f.name || short(userid);
241
290
  document.getElementById('chatSub').textContent = (f.status||'') + ' · ' + short(userid);
242
291
  document.getElementById('chat').style.display = 'block';
292
+ api('/api/chat-mark-read', {userid}); // clears the unread badge
243
293
  await renderChat();
244
294
  clearInterval(chatTimer); chatTimer = setInterval(renderChat, 2000);
245
295
  document.getElementById('chatInput').focus();
@@ -247,7 +297,7 @@ async function openChat(userid){
247
297
  function closeChat(){ chatWith = null; clearInterval(chatTimer); document.getElementById('chat').style.display='none'; refresh(); }
248
298
  async function renderChat(){
249
299
  if(!chatWith) return;
250
- let h; try { h = await api('/api/chat-history'); } catch(e){ return; }
300
+ let h; try { h = await api('/api/chat-history?peer='+encodeURIComponent(chatWith)); } catch(e){ return; }
251
301
  const msgs = (h.chats && h.chats[chatWith]) || [];
252
302
  const log = document.getElementById('chatLog');
253
303
  log.innerHTML = msgs.length ? msgs.map(m => \`<div style="align-self:\${m.dir==='out'?'flex-end':'flex-start'};max-width:75%;padding:.4rem .7rem;border-radius:12px;background:\${m.dir==='out'?'#3478f6':'#8883'};color:\${m.dir==='out'?'#fff':'inherit'}">\${esc(m.text)}</div>\`).join('') : '<div class="empty">No messages yet — say hi.</div>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.87",
3
+ "version": "0.1.88",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",