@decentnetwork/beagle 0.1.126 → 0.1.127
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.
- package/dist/cli.js +1 -0
- package/dist/desktop/app.js +118 -7
- package/dist/friend-greetings.d.ts +45 -0
- package/dist/friend-greetings.js +119 -0
- package/dist/pending-requests.d.ts +3 -0
- package/dist/pending-requests.js +13 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +31 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -173,6 +173,7 @@ async function main() {
|
|
|
173
173
|
downloadsDir: resolve(args.configDir, "downloads"),
|
|
174
174
|
messagesPath: resolve(args.configDir, "messages.json"),
|
|
175
175
|
servicePeersPath: resolve(args.configDir, "service-peers-local.json"),
|
|
176
|
+
greetingsPath: resolve(args.configDir, "friend-greetings.json"),
|
|
176
177
|
callIce: {
|
|
177
178
|
keyFile: resolve(decentlanCarrierDir(args.configDir), "keypair.json"),
|
|
178
179
|
bootstrapNodes,
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.127";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -272,7 +272,7 @@ window.__DK_UI_VERSION="0.1.126";
|
|
|
272
272
|
};
|
|
273
273
|
(thread || []).forEach((m, i) => {
|
|
274
274
|
const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
|
|
275
|
-
if (!hit || m && m.file) {
|
|
275
|
+
if (!hit || m && (m.file || m.kind === "friend-request")) {
|
|
276
276
|
flush();
|
|
277
277
|
out.push(m);
|
|
278
278
|
return;
|
|
@@ -529,7 +529,10 @@ window.__DK_UI_VERSION="0.1.126";
|
|
|
529
529
|
// Brief 30: a validated component form, already whitelisted host-side.
|
|
530
530
|
// Passed through untouched — the renderer treats every string as text,
|
|
531
531
|
// never markup.
|
|
532
|
-
components: m.components
|
|
532
|
+
components: m.components,
|
|
533
|
+
// 'friend-request': the accepted request's greeting, merged in by the
|
|
534
|
+
// host (beagle's friend-greetings.ts); chat.jsx draws its own bubble.
|
|
535
|
+
kind: m.kind
|
|
533
536
|
};
|
|
534
537
|
}
|
|
535
538
|
var DK_TAIL_MIN = 30;
|
|
@@ -626,8 +629,16 @@ window.__DK_UI_VERSION="0.1.126";
|
|
|
626
629
|
if (!c)
|
|
627
630
|
return;
|
|
628
631
|
const arr = [...c.byId.values()].sort((a, b) => a.ts - b.ts);
|
|
629
|
-
const
|
|
630
|
-
|
|
632
|
+
const withDay = [];
|
|
633
|
+
let lastDay = null;
|
|
634
|
+
for (const m of arr) {
|
|
635
|
+
const day = dkDayLabel(m.ts);
|
|
636
|
+
if (day !== lastDay) {
|
|
637
|
+
withDay.push({ day });
|
|
638
|
+
lastDay = day;
|
|
639
|
+
}
|
|
640
|
+
withDay.push(dkThreadMsg(m));
|
|
641
|
+
}
|
|
631
642
|
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
632
643
|
setEarlier((e) => e[peerId] === c.more ? e : Object.assign({}, e, { [peerId]: c.more }));
|
|
633
644
|
}, []);
|
|
@@ -1728,6 +1739,25 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1728
1739
|
return { kind: "punk", id };
|
|
1729
1740
|
return { kind: "ens" };
|
|
1730
1741
|
}
|
|
1742
|
+
var REQUEST_TRANSPORTS = ["lan", "carrier", "direct", "relay", "express", "webrtc", "online", "offline", "tcp", "udp", "p2p"];
|
|
1743
|
+
function unquote(s) {
|
|
1744
|
+
const m = /^"([\s\S]*)"$/.exec(s);
|
|
1745
|
+
return m ? m[1] : s;
|
|
1746
|
+
}
|
|
1747
|
+
function requestGreeting(r) {
|
|
1748
|
+
const hello = r && typeof r.hello === "string" ? r.hello.trim() : "";
|
|
1749
|
+
const via = r && typeof r.via === "string" ? r.via.trim() : "";
|
|
1750
|
+
const isTransport = !!via && REQUEST_TRANSPORTS.includes(via.toLowerCase());
|
|
1751
|
+
const isName = !!via && !!r && typeof r.name === "string" && r.name.trim() === via;
|
|
1752
|
+
return {
|
|
1753
|
+
greeting: hello || (via && !isTransport && !isName ? unquote(via).trim() : ""),
|
|
1754
|
+
transport: isTransport ? via : ""
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
var GREETING_KIND = "friend-request";
|
|
1758
|
+
function isGreetingItem(m) {
|
|
1759
|
+
return !!m && m.kind === GREETING_KIND;
|
|
1760
|
+
}
|
|
1731
1761
|
var DK_CMD_HIST_KEY = "beagle.assistant.history";
|
|
1732
1762
|
var DK_CMD_HIST_MAX = 50;
|
|
1733
1763
|
function RequestsBlock({ T, requests, onAct }) {
|
|
@@ -1779,7 +1809,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1779
1809
|
overflow: "hidden",
|
|
1780
1810
|
textOverflow: "ellipsis",
|
|
1781
1811
|
whiteSpace: "nowrap"
|
|
1782
|
-
} }, r.name) : /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", fontStyle: "italic" } }, T.noName || "no name given"), /* @__PURE__ */ React.createElement(Mono, { size: 11, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, "via "
|
|
1812
|
+
} }, r.name) : /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", fontStyle: "italic" } }, T.noName || "no name given"), /* @__PURE__ */ React.createElement(Mono, { size: 11, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, requestGreeting(r).transport ? "via " + requestGreeting(r).transport + " \xB7 " : "", r.time))), /* @__PURE__ */ React.createElement(DkRequestGreeting, { T, text: requestGreeting(r).greeting }), r.descr && /* @__PURE__ */ React.createElement("div", { style: {
|
|
1783
1813
|
fontFamily: "var(--ui)",
|
|
1784
1814
|
fontSize: 12,
|
|
1785
1815
|
color: "var(--dim, var(--faint))",
|
|
@@ -1788,6 +1818,76 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1788
1818
|
paddingLeft: 8
|
|
1789
1819
|
} }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
|
|
1790
1820
|
}
|
|
1821
|
+
function DkRequestGreeting({ T, text }) {
|
|
1822
|
+
const [open, setOpen] = React.useState(false);
|
|
1823
|
+
const [clipped, setClipped] = React.useState(false);
|
|
1824
|
+
const ref = React.useRef(null);
|
|
1825
|
+
React.useLayoutEffect(() => {
|
|
1826
|
+
const el = ref.current;
|
|
1827
|
+
if (el && !open)
|
|
1828
|
+
setClipped(el.scrollHeight > el.clientHeight + 1);
|
|
1829
|
+
}, [text, open]);
|
|
1830
|
+
if (!text)
|
|
1831
|
+
return null;
|
|
1832
|
+
const toggle = () => {
|
|
1833
|
+
try {
|
|
1834
|
+
const sel = window.getSelection && window.getSelection();
|
|
1835
|
+
if (sel && String(sel).length)
|
|
1836
|
+
return;
|
|
1837
|
+
} catch (e) {
|
|
1838
|
+
}
|
|
1839
|
+
if (clipped || open)
|
|
1840
|
+
setOpen((o) => !o);
|
|
1841
|
+
};
|
|
1842
|
+
return /* @__PURE__ */ React.createElement(
|
|
1843
|
+
"div",
|
|
1844
|
+
{
|
|
1845
|
+
onClick: toggle,
|
|
1846
|
+
title: !open && clipped ? T.showAll || "show all" : void 0,
|
|
1847
|
+
style: { cursor: clipped || open ? "pointer" : "text" }
|
|
1848
|
+
},
|
|
1849
|
+
/* @__PURE__ */ React.createElement("div", { style: { background: "var(--bub-them)", border: "1px solid var(--line)", borderRadius: 8, padding: "6px 9px" } }, /* @__PURE__ */ React.createElement("div", { ref, style: {
|
|
1850
|
+
fontFamily: "var(--ui)",
|
|
1851
|
+
fontSize: 12.5,
|
|
1852
|
+
color: "var(--text)",
|
|
1853
|
+
lineHeight: 1.45,
|
|
1854
|
+
whiteSpace: "pre-wrap",
|
|
1855
|
+
overflowWrap: "anywhere",
|
|
1856
|
+
userSelect: "text",
|
|
1857
|
+
WebkitUserSelect: "text",
|
|
1858
|
+
...open ? { maxHeight: 240, overflowY: "auto" } : { display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }
|
|
1859
|
+
} }, text)),
|
|
1860
|
+
(clipped || open) && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--accent)", marginTop: 3 } }, open ? T.showLess || "show less" : T.showAll || "show all")
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
function DkGreetingMsg({ m, peer, T }) {
|
|
1864
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0 10px" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: {
|
|
1865
|
+
maxWidth: "75%",
|
|
1866
|
+
padding: "8px 12px",
|
|
1867
|
+
borderRadius: 12,
|
|
1868
|
+
borderBottomLeftRadius: 4,
|
|
1869
|
+
background: "var(--panel-2)",
|
|
1870
|
+
border: "1px dashed var(--accent)",
|
|
1871
|
+
color: "var(--text)"
|
|
1872
|
+
} }, /* @__PURE__ */ React.createElement("div", { style: {
|
|
1873
|
+
display: "flex",
|
|
1874
|
+
alignItems: "center",
|
|
1875
|
+
gap: 6,
|
|
1876
|
+
marginBottom: 4,
|
|
1877
|
+
fontFamily: "var(--mono)",
|
|
1878
|
+
fontSize: 10.5,
|
|
1879
|
+
color: "var(--accent)",
|
|
1880
|
+
fontWeight: 600
|
|
1881
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: "userPlus", size: 12, stroke: 2.2, color: "var(--accent)" }), /* @__PURE__ */ React.createElement("span", null, T.friendRequestItem || "Friend request"), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)", fontWeight: 400 } }, "\xB7 ", m.time)), /* @__PURE__ */ React.createElement("div", { style: {
|
|
1882
|
+
fontFamily: "var(--ui)",
|
|
1883
|
+
fontSize: 13.5,
|
|
1884
|
+
lineHeight: 1.5,
|
|
1885
|
+
whiteSpace: "pre-wrap",
|
|
1886
|
+
overflowWrap: "anywhere",
|
|
1887
|
+
userSelect: "text",
|
|
1888
|
+
WebkitUserSelect: "text"
|
|
1889
|
+
} }, m.text)));
|
|
1890
|
+
}
|
|
1791
1891
|
function DkRequestAvatar({ r, size, radius }) {
|
|
1792
1892
|
const [urlBroken, setUrlBroken] = React.useState(false);
|
|
1793
1893
|
const url = requestAvatarUrl(r);
|
|
@@ -2562,7 +2662,12 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
2562
2662
|
return null;
|
|
2563
2663
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
2564
2664
|
}
|
|
2565
|
-
function Msg(
|
|
2665
|
+
function Msg(props) {
|
|
2666
|
+
if (isGreetingItem(props.m))
|
|
2667
|
+
return /* @__PURE__ */ React.createElement(DkGreetingMsg, { m: props.m, peer: props.peer, T: props.T });
|
|
2668
|
+
return /* @__PURE__ */ React.createElement(DkMsgBody, { ...props });
|
|
2669
|
+
}
|
|
2670
|
+
function DkMsgBody({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
2566
2671
|
const mine = m.from === "me";
|
|
2567
2672
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
2568
2673
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -6468,6 +6573,9 @@ ${peer.address}`
|
|
|
6468
6573
|
dirSearch: "search name / userid\u2026",
|
|
6469
6574
|
dirLoading: "loading directory\u2026",
|
|
6470
6575
|
noName: "no name given",
|
|
6576
|
+
friendRequestItem: "Friend request",
|
|
6577
|
+
showAll: "show all",
|
|
6578
|
+
showLess: "show less",
|
|
6471
6579
|
dirEmpty: "nobody matches",
|
|
6472
6580
|
dirError: "Could not load the directory:",
|
|
6473
6581
|
bridgeEmpty: "Nobody else is listed on this bridge yet. You are the first.",
|
|
@@ -6667,6 +6775,9 @@ ${peer.address}`
|
|
|
6667
6775
|
dirSearch: "\u641C\u7D22\u540D\u5B57 / userid\u2026",
|
|
6668
6776
|
dirLoading: "\u6B63\u5728\u52A0\u8F7D\u540D\u5355\u2026",
|
|
6669
6777
|
noName: "\u5BF9\u65B9\u6CA1\u6709\u586B\u540D\u5B57",
|
|
6778
|
+
friendRequestItem: "\u597D\u53CB\u8BF7\u6C42",
|
|
6779
|
+
showAll: "\u5C55\u5F00\u5168\u6587",
|
|
6780
|
+
showLess: "\u6536\u8D77",
|
|
6670
6781
|
dirEmpty: "\u6CA1\u6709\u5339\u914D\u7684\u4EBA",
|
|
6671
6782
|
dirError: "\u540D\u5355\u52A0\u8F7D\u5931\u8D25\uFF1A",
|
|
6672
6783
|
bridgeEmpty: "\u8FD9\u4E2A bridge \u4E0A\u8FD8\u6CA1\u6709\u522B\u4EBA\uFF0C\u4F60\u662F\u7B2C\u4E00\u4E2A\u3002",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const GREETING_KIND = "friend-request";
|
|
2
|
+
/** One greeting as stored, per friend. */
|
|
3
|
+
export interface StoredGreeting {
|
|
4
|
+
text: string;
|
|
5
|
+
/** When the request arrived (ms). The item is dated here, not at accept. */
|
|
6
|
+
ts: number;
|
|
7
|
+
}
|
|
8
|
+
/** The shape merged into a /api/chat-history page — a raw store record with
|
|
9
|
+
* `kind` so beagle-ui draws the distinct bubble. */
|
|
10
|
+
export interface GreetingItem {
|
|
11
|
+
id: string;
|
|
12
|
+
dir: "in";
|
|
13
|
+
text: string;
|
|
14
|
+
ts: number;
|
|
15
|
+
kind: typeof GREETING_KIND;
|
|
16
|
+
}
|
|
17
|
+
/** A request's arrival time in ms. The embedded host stores a number, the lan
|
|
18
|
+
* daemon an ISO string. Undefined when neither parses. */
|
|
19
|
+
export declare function arrivalMs(v: unknown): number | undefined;
|
|
20
|
+
export declare class GreetingStore {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(path: string);
|
|
23
|
+
/** Keep the greeting of an accepted request. Skips an empty / whitespace
|
|
24
|
+
* greeting, and a repeat request carrying the same text (one item per
|
|
25
|
+
* request, not one per click). Returns whether an item was added. */
|
|
26
|
+
record(peer: string, hello: unknown, arrivedAt: unknown, now?: number): boolean;
|
|
27
|
+
/** The items for one conversation, oldest first. */
|
|
28
|
+
items(peer: string): GreetingItem[];
|
|
29
|
+
/** Forget a friend's greetings (friend removed together with the chat). */
|
|
30
|
+
remove(peer: string): void;
|
|
31
|
+
}
|
|
32
|
+
/** Merge greeting items into one /api/chat-history page (oldest first).
|
|
33
|
+
*
|
|
34
|
+
* A greeting belongs on a page when it is older than `before` (if given)
|
|
35
|
+
* and, when the page is full (`limit` records — there are older ones the
|
|
36
|
+
* page left out), no older than the page's oldest record; otherwise it is
|
|
37
|
+
* shown when the user loads earlier and reaches it. Already-present ids are
|
|
38
|
+
* not added twice. Returns a new array. */
|
|
39
|
+
export declare function mergeGreetings<M extends {
|
|
40
|
+
ts?: number;
|
|
41
|
+
id?: string;
|
|
42
|
+
}>(page: readonly M[], greetings: readonly GreetingItem[], opts?: {
|
|
43
|
+
before?: number;
|
|
44
|
+
limit?: number;
|
|
45
|
+
}): Array<M | GreetingItem>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The greeting a requester typed into a friend request, kept after accept.
|
|
2
|
+
//
|
|
3
|
+
// Before this, the greeting lived only in the pending list: readable while the
|
|
4
|
+
// request was open (and drawn as "via <greeting>"), gone the moment it was
|
|
5
|
+
// accepted. On accept beagle now writes it into that friend's conversation as
|
|
6
|
+
// an incoming "friend request" item, dated at the request time.
|
|
7
|
+
//
|
|
8
|
+
// Local only — nothing is sent. Stored in beagle's own file, not in
|
|
9
|
+
// messages.json: with the daemon backend that file belongs to the agentnet
|
|
10
|
+
// daemon, which holds it in memory and would overwrite anything we wrote.
|
|
11
|
+
// /api/chat-history merges these items into the page it returns, so the
|
|
12
|
+
// daemon's unread counts and last-message preview never see them (spec:
|
|
13
|
+
// not unread, no notification, not the "last message" over newer ones).
|
|
14
|
+
// See decent-hq docs/tasks/TASK-2026-11-04-friend-request-greeting.md.
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname } from "node:path";
|
|
17
|
+
export const GREETING_KIND = "friend-request";
|
|
18
|
+
/** A request's arrival time in ms. The embedded host stores a number, the lan
|
|
19
|
+
* daemon an ISO string. Undefined when neither parses. */
|
|
20
|
+
export function arrivalMs(v) {
|
|
21
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0)
|
|
22
|
+
return v;
|
|
23
|
+
if (typeof v === "string" && v) {
|
|
24
|
+
const n = Date.parse(v);
|
|
25
|
+
if (Number.isFinite(n) && n > 0)
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
export class GreetingStore {
|
|
31
|
+
#path;
|
|
32
|
+
#byPeer = {};
|
|
33
|
+
constructor(path) {
|
|
34
|
+
this.#path = path;
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(path)) {
|
|
37
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
38
|
+
for (const [peer, list] of Object.entries(raw?.byPeer ?? {})) {
|
|
39
|
+
if (!Array.isArray(list))
|
|
40
|
+
continue;
|
|
41
|
+
const ok = list.filter((g) => !!g && typeof g.text === "string" && typeof g.ts === "number");
|
|
42
|
+
if (ok.length)
|
|
43
|
+
this.#byPeer[peer] = ok;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A corrupt file loses the greetings, never the chat: start empty.
|
|
49
|
+
this.#byPeer = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Keep the greeting of an accepted request. Skips an empty / whitespace
|
|
53
|
+
* greeting, and a repeat request carrying the same text (one item per
|
|
54
|
+
* request, not one per click). Returns whether an item was added. */
|
|
55
|
+
record(peer, hello, arrivedAt, now = Date.now()) {
|
|
56
|
+
if (!peer || typeof hello !== "string")
|
|
57
|
+
return false;
|
|
58
|
+
const text = hello.trim();
|
|
59
|
+
if (!text)
|
|
60
|
+
return false;
|
|
61
|
+
const list = this.#byPeer[peer] ?? [];
|
|
62
|
+
if (list.some((g) => g.text.trim() === text))
|
|
63
|
+
return false;
|
|
64
|
+
list.push({ text, ts: arrivalMs(arrivedAt) ?? now });
|
|
65
|
+
list.sort((a, b) => a.ts - b.ts);
|
|
66
|
+
this.#byPeer[peer] = list;
|
|
67
|
+
this.#save();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** The items for one conversation, oldest first. */
|
|
71
|
+
items(peer) {
|
|
72
|
+
return (this.#byPeer[peer] ?? []).map((g) => ({
|
|
73
|
+
id: `${GREETING_KIND}:${g.ts}`,
|
|
74
|
+
dir: "in",
|
|
75
|
+
text: g.text,
|
|
76
|
+
ts: g.ts,
|
|
77
|
+
kind: GREETING_KIND,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
/** Forget a friend's greetings (friend removed together with the chat). */
|
|
81
|
+
remove(peer) {
|
|
82
|
+
if (!this.#byPeer[peer])
|
|
83
|
+
return;
|
|
84
|
+
delete this.#byPeer[peer];
|
|
85
|
+
this.#save();
|
|
86
|
+
}
|
|
87
|
+
#save() {
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(dirname(this.#path), { recursive: true });
|
|
90
|
+
const tmp = `${this.#path}.${process.pid}.tmp`;
|
|
91
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, byPeer: this.#byPeer }, null, 2), "utf-8");
|
|
92
|
+
renameSync(tmp, this.#path);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Not fatal: the greeting stays for this run and the chat is unaffected.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Merge greeting items into one /api/chat-history page (oldest first).
|
|
100
|
+
*
|
|
101
|
+
* A greeting belongs on a page when it is older than `before` (if given)
|
|
102
|
+
* and, when the page is full (`limit` records — there are older ones the
|
|
103
|
+
* page left out), no older than the page's oldest record; otherwise it is
|
|
104
|
+
* shown when the user loads earlier and reaches it. Already-present ids are
|
|
105
|
+
* not added twice. Returns a new array. */
|
|
106
|
+
export function mergeGreetings(page, greetings, opts = {}) {
|
|
107
|
+
if (!greetings.length)
|
|
108
|
+
return page.slice();
|
|
109
|
+
const full = opts.limit !== undefined && page.length >= opts.limit;
|
|
110
|
+
const oldest = page.length ? Math.min(...page.map((m) => (typeof m.ts === "number" ? m.ts : Infinity))) : Infinity;
|
|
111
|
+
const have = new Set(page.map((m) => m.id).filter(Boolean));
|
|
112
|
+
const add = greetings.filter((g) => !have.has(g.id) &&
|
|
113
|
+
(opts.before === undefined || g.ts < opts.before) &&
|
|
114
|
+
(!full || g.ts >= oldest));
|
|
115
|
+
if (!add.length)
|
|
116
|
+
return page.slice();
|
|
117
|
+
// Stable: a greeting sorts before a message with the same timestamp.
|
|
118
|
+
return [...add, ...page].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
119
|
+
}
|
|
@@ -26,3 +26,6 @@ export declare function refreshPendingAvatars<E extends {
|
|
|
26
26
|
* the same convention as `peers[]` — and the card then falls back to the
|
|
27
27
|
* beagles.eth lookup and a generated picture. */
|
|
28
28
|
export declare function desktopRequestRow(p: Record<string, unknown>, i: number, fmtTime: (ts?: number) => string): Record<string, unknown>;
|
|
29
|
+
/** The pending entry the UI's Accept refers to: the card's id is the userid,
|
|
30
|
+
* or the address when the request carried no userid. */
|
|
31
|
+
export declare function findPending(pending: ReadonlyArray<Record<string, unknown>>, id: unknown): Record<string, unknown> | undefined;
|
package/dist/pending-requests.js
CHANGED
|
@@ -60,10 +60,22 @@ export function desktopRequestRow(p, i, fmtTime) {
|
|
|
60
60
|
// was nothing to decide on.
|
|
61
61
|
name: p.name || "",
|
|
62
62
|
descr: p.descr || "",
|
|
63
|
-
|
|
63
|
+
// The greeting the requester typed. It used to ride in `via`, so the card
|
|
64
|
+
// drew it as "via <greeting>" — mislabeled, and gone after accept.
|
|
65
|
+
// beagle-ui >= 0.2.36 reads `hello`; for an older UI, `via` stays the
|
|
66
|
+
// transport word it always fell back to.
|
|
67
|
+
hello: typeof p.hello === "string" ? p.hello : "",
|
|
68
|
+
via: p.via || "lan",
|
|
64
69
|
time: fmtTime(p.arrivedAt),
|
|
65
70
|
// The requester's avatar, drawn before accept: URL first, then punk.
|
|
66
71
|
punkId: a.punkId ?? null,
|
|
67
72
|
avatarUrl: a.avatarUrl ?? null,
|
|
68
73
|
};
|
|
69
74
|
}
|
|
75
|
+
/** The pending entry the UI's Accept refers to: the card's id is the userid,
|
|
76
|
+
* or the address when the request carried no userid. */
|
|
77
|
+
export function findPending(pending, id) {
|
|
78
|
+
if (typeof id !== "string" || !id)
|
|
79
|
+
return undefined;
|
|
80
|
+
return pending.find((p) => p.userid === id || p.address === id || p.pubkey === id);
|
|
81
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface BeagleServerOptions {
|
|
|
32
32
|
* service peers on top of the list shipped in the app. Written only by the
|
|
33
33
|
* Assistant's `/nodes refresh`, and only after the user ticks the rows. */
|
|
34
34
|
servicePeersPath?: string;
|
|
35
|
+
/** <configDir>/friend-greetings.json — the greetings of accepted friend
|
|
36
|
+
* requests, shown as the first item of that chat. Beagle's own file (see
|
|
37
|
+
* friend-greetings.ts). Undefined = greetings are not kept. */
|
|
38
|
+
greetingsPath?: string;
|
|
35
39
|
/** The swappable backend, so the UI can show which one is live and drive the
|
|
36
40
|
* LAN handoff. Optional: a caller that pins one backend just omits it and
|
|
37
41
|
* the network panel falls back to "no virtual LAN here". */
|
package/dist/server.js
CHANGED
|
@@ -26,7 +26,8 @@ import { bridgeRoster } from "./bridge-roster.js";
|
|
|
26
26
|
import { isSupervised } from "./relaunch.js";
|
|
27
27
|
import { historyStats } from "./assistant-stats.js";
|
|
28
28
|
import { readFriendStore, pickPunk } from "./friend-store.js";
|
|
29
|
-
import { desktopRequestRow } from "./pending-requests.js";
|
|
29
|
+
import { desktopRequestRow, findPending } from "./pending-requests.js";
|
|
30
|
+
import { GreetingStore, mergeGreetings } from "./friend-greetings.js";
|
|
30
31
|
import { createDiscoverFetch } from "./discover-cache.js";
|
|
31
32
|
import { applyWalletBind } from "@decentnetwork/beagle-ui/wallet-proof";
|
|
32
33
|
import { settleUpload } from "./upload-settle.js";
|
|
@@ -947,6 +948,7 @@ export function startBeagleServer(opts) {
|
|
|
947
948
|
const host = opts.listenHost ?? "127.0.0.1";
|
|
948
949
|
const port = opts.listenPort ?? 8765;
|
|
949
950
|
const log = opts.log ?? ((s) => console.log(s));
|
|
951
|
+
const greetings = opts.greetingsPath ? new GreetingStore(opts.greetingsPath) : null;
|
|
950
952
|
const sendJson = (res, code, body) => {
|
|
951
953
|
res.writeHead(code, { "content-type": "application/json" });
|
|
952
954
|
res.end(JSON.stringify(body));
|
|
@@ -2436,7 +2438,18 @@ export function startBeagleServer(opts) {
|
|
|
2436
2438
|
const bef = Number(q.get("before"));
|
|
2437
2439
|
const before = Number.isFinite(bef) && bef > 0 ? bef : undefined;
|
|
2438
2440
|
const r = await opts.call({ op: "chat-history", userid: peer, peer, before, limit });
|
|
2439
|
-
|
|
2441
|
+
const data = (r.ok ? (r.data ?? { chats: {} }) : { chats: {} });
|
|
2442
|
+
// The greeting of the accepted friend request, as the chat's first
|
|
2443
|
+
// item (friend-greetings.ts). Merged here, never written to the
|
|
2444
|
+
// daemon's store, so it is not unread and not the roster preview.
|
|
2445
|
+
if (greetings && peer) {
|
|
2446
|
+
const items = greetings.items(peer);
|
|
2447
|
+
if (items.length) {
|
|
2448
|
+
data.chats = data.chats ?? {};
|
|
2449
|
+
data.chats[peer] = mergeGreetings(data.chats[peer] ?? [], items, { before, limit });
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
sendJson(res, 200, data);
|
|
2440
2453
|
return;
|
|
2441
2454
|
}
|
|
2442
2455
|
// Brief 30: submit a rendered form. The values are the user's own input;
|
|
@@ -2556,6 +2569,8 @@ export function startBeagleServer(opts) {
|
|
|
2556
2569
|
if (req.method === "POST" && url === "/api/friend-remove") {
|
|
2557
2570
|
const { userid } = await readBody(req);
|
|
2558
2571
|
const r = await opts.call({ op: "friend-remove", userid });
|
|
2572
|
+
if (r.ok)
|
|
2573
|
+
greetings?.remove(userid);
|
|
2559
2574
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
2560
2575
|
return;
|
|
2561
2576
|
}
|
|
@@ -2637,7 +2652,21 @@ export function startBeagleServer(opts) {
|
|
|
2637
2652
|
}
|
|
2638
2653
|
if (req.method === "POST" && url === "/api/accept") {
|
|
2639
2654
|
const { userid } = await readBody(req);
|
|
2655
|
+
// Read the request BEFORE accepting: accept drops it from pending, and
|
|
2656
|
+
// with it the greeting the requester typed.
|
|
2657
|
+
let entry;
|
|
2658
|
+
if (greetings) {
|
|
2659
|
+
try {
|
|
2660
|
+
const pend = await opts.call({ op: "friends-pending" });
|
|
2661
|
+
if (pend.ok)
|
|
2662
|
+
entry = findPending(pend.data?.pending ?? [], userid);
|
|
2663
|
+
}
|
|
2664
|
+
catch { /* no greeting then; the accept still goes through */ }
|
|
2665
|
+
}
|
|
2640
2666
|
const r = await opts.call({ op: "friends-accept", userid });
|
|
2667
|
+
if (r.ok && greetings && entry) {
|
|
2668
|
+
greetings.record(entry.userid || userid, entry.hello, entry.arrivedAt);
|
|
2669
|
+
}
|
|
2641
2670
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
2642
2671
|
return;
|
|
2643
2672
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.127",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"start": "node dist/cli.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
29
|
+
"@decentnetwork/beagle-ui": "^0.2.36",
|
|
30
30
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
31
31
|
"@decentnetwork/lan": "^0.1.313",
|
|
32
32
|
"@decentnetwork/peer": "^0.1.170",
|