@yaebal/panel 0.0.2 → 0.0.5
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/README.md +156 -96
- package/lib/index.d.ts +43 -12
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +253 -20
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +81 -3
- package/lib/index.test.js.map +1 -1
- package/lib/panel-html.d.ts +2 -2
- package/lib/panel-html.d.ts.map +1 -1
- package/lib/panel-html.js +412 -115
- package/lib/panel-html.js.map +1 -1
- package/lib/serve.d.ts.map +1 -1
- package/lib/serve.js.map +1 -1
- package/lib/sqlite.d.ts +2 -5
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +76 -18
- package/lib/sqlite.js.map +1 -1
- package/lib/sqlite.test.js +41 -8
- package/lib/sqlite.test.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +104 -4
- package/src/index.ts +327 -30
- package/src/panel-html.ts +412 -115
- package/src/serve.ts +1 -1
- package/src/sqlite.test.ts +47 -9
- package/src/sqlite.ts +94 -21
package/src/panel-html.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** the operator panel ui
|
|
1
|
+
/** the operator panel ui - a single static page: token login, then the live chat view. */
|
|
2
2
|
export const PANEL_HTML = `<!doctype html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
@@ -6,82 +6,266 @@ export const PANEL_HTML = `<!doctype html>
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
7
|
<title>yaebal panel</title>
|
|
8
8
|
<style>
|
|
9
|
-
:root {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light;
|
|
11
|
+
--primary:#ffffff; --secondary:#000000; --white:#ffffff; --gray:#75757e;
|
|
12
|
+
--blue:#2f8af9; --red:#ed2236;
|
|
13
|
+
--button:#f4f4f4; --button-hover:#ededed; --button-press:#e8e8e8;
|
|
14
|
+
--button-stroke:rgba(0,0,0,.06); --button-text:#282828;
|
|
15
|
+
--sidebar-bg:#fbfbfb; --sidebar-stroke:rgba(0,0,0,.06);
|
|
16
|
+
--content-border:rgba(0,0,0,.08); --input-border:#adadb7;
|
|
17
|
+
--radius:18px; --radius-sm:12px; --sidebar-width:320px;
|
|
18
|
+
--shadow:0 18px 70px rgba(0,0,0,.14);
|
|
19
|
+
}
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
color-scheme: dark;
|
|
23
|
+
--primary:#000000; --secondary:#e1e1e1; --gray:#818181;
|
|
24
|
+
--blue:#2a7ce1; --red:#ff5b70;
|
|
25
|
+
--button:#191919; --button-hover:#242424; --button-press:#2a2a2a;
|
|
26
|
+
--button-stroke:rgba(255,255,255,.05); --button-text:#e1e1e1;
|
|
27
|
+
--sidebar-bg:#0c0c0c; --sidebar-stroke:rgba(255,255,255,.05);
|
|
28
|
+
--content-border:rgba(255,255,255,.08); --input-border:#383838;
|
|
29
|
+
--shadow:0 18px 70px rgba(0,0,0,.42);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
* { box-sizing:border-box; margin:0; }
|
|
33
|
+
html, body { height:100%; }
|
|
34
|
+
body {
|
|
35
|
+
min-height:100%; overflow:hidden; background:var(--primary); color:var(--secondary);
|
|
36
|
+
font:15px/1.5 "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
37
|
+
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
38
|
+
}
|
|
39
|
+
button, input { font:inherit; }
|
|
40
|
+
button { color:inherit; }
|
|
41
|
+
svg { display:block; width:18px; height:18px; }
|
|
42
|
+
.ico { display:inline-flex; align-items:center; justify-content:center; flex:none; }
|
|
43
|
+
:focus-visible { outline:solid 2px var(--blue); outline-offset:-2px; }
|
|
44
|
+
::selection { background:var(--secondary); color:var(--primary); }
|
|
45
|
+
::-webkit-scrollbar { width:10px; height:10px; }
|
|
46
|
+
::-webkit-scrollbar-thumb { background:var(--button-press); border-radius:10px; border:2px solid var(--primary); }
|
|
47
|
+
|
|
48
|
+
@keyframes fade-in { from { opacity:0; } to { opacity:1; } }
|
|
49
|
+
@keyframes rise-in { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
|
|
50
|
+
@keyframes slide-in { from { opacity:0; transform:translateX(-10px); } to { opacity:1; transform:translateX(0); } }
|
|
51
|
+
@keyframes msg-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
52
|
+
@keyframes viewer-in { from { opacity:0; backdrop-filter:blur(0); } to { opacity:1; backdrop-filter:blur(10px); } }
|
|
53
|
+
@keyframes soft-pulse { 0%, 100% { transform:scaleY(.72); opacity:.45; } 50% { transform:scaleY(1); opacity:.95; } }
|
|
54
|
+
|
|
55
|
+
#login { width:min(380px, calc(100vw - 32px)); min-height:100%; margin:auto; display:flex; align-items:center; justify-content:center; padding:24px; }
|
|
56
|
+
#login .card { width:100%; padding:24px; border:1px solid var(--content-border); border-radius:20px; background:var(--sidebar-bg); animation:rise-in .32s ease both; }
|
|
57
|
+
#login .mark { width:44px; height:44px; border-radius:14px; display:grid; place-items:center; margin:0 auto 18px; background:var(--secondary); color:var(--primary); }
|
|
58
|
+
#login .mark svg { width:22px; height:22px; }
|
|
59
|
+
#login .brand { text-align:center; font-size:20px; font-weight:700; letter-spacing:-.5px; }
|
|
60
|
+
#login .brand b { font-weight:700; }
|
|
61
|
+
#login .sub { margin:4px 0 20px; text-align:center; color:var(--gray); font-size:13px; font-weight:500; }
|
|
62
|
+
#login input, #login button { width:100%; height:44px; border-radius:14px; padding:0 14px; }
|
|
63
|
+
#login input { border:1px solid var(--input-border); background:var(--primary); color:var(--secondary); text-align:center; outline:none; transition:border-color .16s ease, box-shadow .16s ease; }
|
|
64
|
+
#login input:focus { border-color:var(--blue); box-shadow:0 0 0 4px color-mix(in srgb, var(--blue) 15%, transparent); }
|
|
65
|
+
#login button { margin-top:10px; border:0; background:var(--secondary); color:var(--primary); font-weight:600; cursor:pointer; transition:opacity .16s ease, transform .16s ease; }
|
|
66
|
+
#login button:hover { opacity:.86; }
|
|
67
|
+
#login button:active { transform:translateY(1px); }
|
|
68
|
+
#login button:disabled { opacity:.55; cursor:default; transform:none; }
|
|
69
|
+
#login .err { min-height:18px; margin-top:12px; text-align:center; color:var(--red); font-size:12px; }
|
|
70
|
+
|
|
71
|
+
#app { height:100%; display:none; animation:fade-in .2s ease both; }
|
|
28
72
|
body.authed #login { display:none; }
|
|
29
73
|
body.authed #app { display:flex; }
|
|
30
|
-
#chats { width:
|
|
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
|
-
|
|
74
|
+
#chats { width:var(--sidebar-width); flex:none; display:flex; flex-direction:column; min-height:0; background:var(--sidebar-bg); border-right:1px solid var(--sidebar-stroke); }
|
|
75
|
+
#main { flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; background:var(--primary); }
|
|
76
|
+
|
|
77
|
+
.side-top { flex:none; padding:20px 16px 18px; display:flex; align-items:center; justify-content:space-between; gap:12px; border-bottom:1px solid var(--sidebar-stroke); }
|
|
78
|
+
.side-title { min-width:0; display:flex; align-items:center; gap:12px; }
|
|
79
|
+
.side-logo { width:38px; height:38px; border-radius:13px; display:grid; place-items:center; background:var(--secondary); color:var(--primary); flex:none; }
|
|
80
|
+
.side-logo svg { width:18px; height:18px; }
|
|
81
|
+
.side-title h1 { font-size:15px; font-weight:600; letter-spacing:-.3px; }
|
|
82
|
+
.side-title p { margin-top:1px; color:var(--gray); font-size:12.5px; font-weight:500; }
|
|
83
|
+
.ghost { width:38px; height:38px; padding:0; border:0; border-radius:14px; display:grid; place-items:center; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; cursor:pointer; transition:background .16s ease, transform .16s ease; }
|
|
84
|
+
.ghost:hover { background:var(--button-hover); }
|
|
85
|
+
.ghost:active { transform:scale(.97); }
|
|
86
|
+
|
|
87
|
+
#chat-list { flex:1; min-height:0; overflow:auto; padding:10px 8px; }
|
|
88
|
+
.chat { width:100%; border:0; background:transparent; color:inherit; display:grid; grid-template-columns:42px 1fr; gap:11px; padding:9px 10px; border-radius:13px; text-align:left; cursor:pointer; animation:slide-in .24s ease both; transition:background .16s ease, color .16s ease, transform .16s ease; }
|
|
89
|
+
.chat:hover { background:var(--button-hover); }
|
|
90
|
+
.chat:active { transform:scale(.99); }
|
|
91
|
+
.chat.on { background:var(--secondary); color:var(--primary); }
|
|
92
|
+
.avatar { width:42px; height:42px; border-radius:13px; display:grid; place-items:center; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; flex:none; }
|
|
93
|
+
.chat.on .avatar { background:var(--primary); color:var(--secondary); }
|
|
94
|
+
.avatar.small { width:38px; height:38px; border-radius:12px; }
|
|
95
|
+
.avatar svg { width:18px; height:18px; }
|
|
96
|
+
.chat-body { min-width:0; align-self:center; }
|
|
97
|
+
.chat-line { display:flex; align-items:baseline; justify-content:space-between; gap:8px; min-width:0; }
|
|
98
|
+
.chat-name { font-weight:600; letter-spacing:-.2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
99
|
+
.chat-handle { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }
|
|
100
|
+
.chat.on .chat-handle, .chat.on .chat-time, .chat.on .preview { color:color-mix(in srgb, var(--primary) 70%, transparent); }
|
|
101
|
+
.chat-time { color:var(--gray); font-size:11.5px; flex:none; }
|
|
102
|
+
.preview { min-width:0; display:flex; align-items:center; gap:5px; margin-top:4px; color:var(--gray); }
|
|
103
|
+
.preview .ico svg { width:13px; height:13px; }
|
|
104
|
+
.preview-label { font-weight:600; font-size:12px; flex:none; color:inherit; }
|
|
105
|
+
.preview-text { min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:12.5px; }
|
|
106
|
+
|
|
107
|
+
#empty { flex:1; display:grid; place-items:center; color:var(--gray); animation:fade-in .22s ease both; }
|
|
108
|
+
#empty .empty-card { text-align:center; padding:28px; }
|
|
109
|
+
#empty .empty-icon { width:58px; height:58px; border:1px solid var(--content-border); border-radius:18px; display:grid; place-items:center; margin:0 auto 14px; background:var(--sidebar-bg); }
|
|
110
|
+
#empty svg { width:26px; height:26px; color:var(--gray); }
|
|
111
|
+
.thread-head { flex:none; min-height:68px; padding:14px 20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--content-border); background:var(--primary); }
|
|
112
|
+
.thread-head .meta { min-width:0; }
|
|
113
|
+
.thread-head .name { font-weight:600; letter-spacing:-.25px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
114
|
+
.thread-head .sub { color:var(--gray); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
115
|
+
.back { display:none; }
|
|
116
|
+
#log { flex:1; min-height:0; overflow:auto; padding:22px 24px; display:flex; flex-direction:column; gap:8px; }
|
|
117
|
+
#log .more { align-self:center; border:1px solid var(--content-border); border-radius:999px; background:var(--sidebar-bg); color:var(--gray); padding:6px 13px; cursor:pointer; font-size:12px; transition:border-color .16s ease, color .16s ease; }
|
|
118
|
+
#log .more:hover { border-color:var(--blue); color:var(--secondary); }
|
|
119
|
+
.msg { display:flex; flex-direction:column; max-width:min(560px, 72%); gap:4px; animation:msg-in .22s ease both; }
|
|
120
|
+
.msg.in { align-self:flex-start; transform-origin:left bottom; }
|
|
121
|
+
.msg.out { align-self:flex-end; align-items:flex-end; transform-origin:right bottom; }
|
|
122
|
+
.bubble { border:1px solid var(--content-border); border-radius:16px; padding:9px 11px; background:var(--sidebar-bg); color:var(--secondary); }
|
|
123
|
+
.out .bubble { border-color:var(--blue); background:var(--blue); color:var(--white); }
|
|
124
|
+
.cap { padding:3px 0 0; white-space:pre-wrap; word-break:break-word; }
|
|
125
|
+
.time { color:var(--gray); font-size:11.5px; padding:0 4px; }
|
|
126
|
+
.out .time { color:var(--gray); }
|
|
127
|
+
|
|
128
|
+
.media-frame { position:relative; overflow:hidden; border:0; padding:0; cursor:pointer; color:inherit; background:var(--button); border-radius:14px; display:block; max-width:340px; transition:transform .18s ease, filter .18s ease; }
|
|
129
|
+
.media-frame:hover { transform:translateY(-1px); filter:brightness(1.03); }
|
|
130
|
+
.media-frame img, .media-frame video { display:block; width:100%; max-width:340px; max-height:380px; object-fit:cover; border-radius:14px; }
|
|
131
|
+
.expand { position:absolute; right:8px; top:8px; width:30px; height:30px; border:1px solid rgba(255,255,255,.28); border-radius:10px; background:rgba(0,0,0,.42); color:#fff; display:grid; place-items:center; opacity:0; transform:translateY(-4px); transition:opacity .16s ease, transform .16s ease; }
|
|
132
|
+
.media-frame:hover .expand, .video-card:hover .expand { opacity:1; transform:translateY(0); }
|
|
133
|
+
.video-card { position:relative; border:1px solid var(--content-border); border-radius:15px; padding:4px; background:var(--button); transition:transform .18s ease; }
|
|
134
|
+
.video-card:hover { transform:translateY(-1px); }
|
|
135
|
+
.video-card video { border-radius:12px; background:#000; width:min(390px, 70vw); max-height:380px; display:block; }
|
|
136
|
+
.video-badge { position:absolute; left:12px; top:12px; display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; background:rgba(0,0,0,.56); color:#fff; font-size:11px; font-weight:600; }
|
|
137
|
+
.video-badge svg { width:13px; height:13px; }
|
|
138
|
+
.voice-card, .audio-card, .doc-card { display:flex; align-items:center; gap:11px; min-width:min(300px, 68vw); padding:10px; border-radius:14px; background:var(--button); border:1px solid var(--button-stroke); color:inherit; text-decoration:none; }
|
|
139
|
+
.out .voice-card, .out .audio-card, .out .doc-card { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }
|
|
140
|
+
.voice-mark, .doc-mark { width:38px; height:38px; border-radius:12px; display:grid; place-items:center; flex:none; background:var(--primary); color:var(--secondary); box-shadow:0 0 0 1px var(--button-stroke) inset; }
|
|
141
|
+
.out .voice-mark, .out .doc-mark { background:rgba(255,255,255,.18); color:#fff; }
|
|
142
|
+
.wave { display:flex; align-items:center; gap:3px; height:26px; flex:1; min-width:62px; }
|
|
143
|
+
.wave i { display:block; width:3px; border-radius:999px; background:currentColor; transform-origin:center; animation:soft-pulse 1.45s ease-in-out infinite; }
|
|
144
|
+
.wave i:nth-child(1) { height:10px; animation-delay:0s; } .wave i:nth-child(2) { height:18px; animation-delay:.08s; } .wave i:nth-child(3) { height:24px; animation-delay:.16s; }
|
|
145
|
+
.wave i:nth-child(4) { height:14px; animation-delay:.24s; } .wave i:nth-child(5) { height:22px; animation-delay:.32s; } .wave i:nth-child(6) { height:12px; animation-delay:.40s; }
|
|
146
|
+
.voice-card audio, .audio-card audio { width:142px; max-width:36vw; height:30px; }
|
|
147
|
+
.doc-title { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:600; }
|
|
148
|
+
.doc-sub { color:var(--gray); font-size:12px; }
|
|
149
|
+
.out .doc-sub { color:rgba(255,255,255,.72); }
|
|
150
|
+
.album-grid { display:grid; grid-template-columns:repeat(2, minmax(108px, 1fr)); gap:5px; max-width:340px; }
|
|
151
|
+
.album-grid .media-frame, .album-grid .video-card { max-width:none; }
|
|
152
|
+
.album-grid img, .album-grid video { height:142px; max-height:142px; }
|
|
153
|
+
.keyboard { display:flex; flex-direction:column; gap:5px; margin-top:8px; min-width:min(280px, 58vw); }
|
|
154
|
+
.keyboard-row { display:flex; gap:5px; }
|
|
155
|
+
.key { flex:1; min-width:0; display:flex; align-items:center; justify-content:center; gap:6px; padding:7px 9px; border-radius:11px; border:1px solid var(--content-border); background:var(--primary); color:inherit; font-size:12.5px; font-weight:600; text-decoration:none; transition:background .16s ease, transform .16s ease; }
|
|
156
|
+
.out .key { background:rgba(255,255,255,.14); border-color:rgba(255,255,255,.18); }
|
|
157
|
+
.key:hover { background:var(--button-hover); transform:translateY(-1px); }
|
|
158
|
+
.out .key:hover { background:rgba(255,255,255,.2); }
|
|
159
|
+
.key span:last-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
160
|
+
.key svg { width:13px; height:13px; opacity:.82; }
|
|
161
|
+
.eventline { align-self:center; max-width:min(520px, 86%); display:flex; align-items:center; gap:9px; padding:7px 12px; border:1px solid var(--content-border); border-radius:999px; background:var(--sidebar-bg); color:var(--gray); font-size:12.5px; animation:msg-in .22s ease both; }
|
|
162
|
+
.eventline .event-title { color:var(--secondary); font-weight:600; }
|
|
163
|
+
.eventline svg { width:14px; height:14px; color:var(--blue); }
|
|
164
|
+
|
|
165
|
+
#composer { flex:none; display:flex; align-items:center; gap:10px; padding:14px 18px; border-top:1px solid var(--content-border); background:var(--primary); }
|
|
166
|
+
#composer input.text { flex:1; height:44px; min-width:0; background:var(--button); border:1px solid var(--button-stroke); color:var(--secondary); border-radius:15px; padding:0 14px; outline:none; transition:border-color .16s ease, box-shadow .16s ease, background .16s ease; }
|
|
167
|
+
#composer input.text:focus { background:var(--primary); border-color:var(--blue); box-shadow:0 0 0 4px color-mix(in srgb, var(--blue) 14%, transparent); }
|
|
168
|
+
#composer button { height:44px; border:0; border-radius:15px; cursor:pointer; transition:background .16s ease, transform .16s ease, opacity .16s ease; }
|
|
169
|
+
#composer button:active { transform:scale(.98); }
|
|
170
|
+
#composer .attach { width:44px; background:var(--button); color:var(--button-text); box-shadow:0 0 0 1px var(--button-stroke) inset; display:grid; place-items:center; }
|
|
171
|
+
#composer .attach:hover { background:var(--button-hover); }
|
|
172
|
+
#composer .send { display:inline-flex; align-items:center; gap:8px; padding:0 17px; font-weight:600; background:var(--secondary); color:var(--primary); }
|
|
173
|
+
#composer .send:hover { opacity:.86; }
|
|
174
|
+
|
|
175
|
+
#viewer[hidden] { display:none; }
|
|
176
|
+
#viewer { position:fixed; inset:0; z-index:20; display:grid; place-items:center; padding:22px; background:rgba(0,0,0,.72); animation:viewer-in .18s ease both; }
|
|
177
|
+
#viewer .viewer-card { max-width:min(980px, 100%); max-height:100%; display:flex; flex-direction:column; gap:12px; animation:rise-in .22s ease both; }
|
|
178
|
+
#viewer .viewer-top { display:flex; justify-content:flex-end; }
|
|
179
|
+
#viewer .viewer-close { width:40px; height:40px; border:1px solid rgba(255,255,255,.16); border-radius:14px; background:rgba(255,255,255,.08); color:#fff; display:grid; place-items:center; cursor:pointer; }
|
|
180
|
+
#viewer .viewer-frame { display:grid; place-items:center; min-height:0; }
|
|
181
|
+
#viewer img, #viewer video { max-width:100%; max-height:78vh; border-radius:18px; box-shadow:var(--shadow); background:#000; }
|
|
182
|
+
#viewer .viewer-caption { color:rgba(255,255,255,.82); text-align:center; max-width:760px; }
|
|
183
|
+
|
|
184
|
+
@media (max-width: 780px) {
|
|
185
|
+
#chats { width:100%; }
|
|
186
|
+
#main { display:none; }
|
|
187
|
+
body.chat-open #chats { display:none; }
|
|
188
|
+
body.chat-open #main { display:flex; }
|
|
189
|
+
.back { display:grid; }
|
|
190
|
+
.msg { max-width:88%; }
|
|
191
|
+
#log { padding:16px 14px; }
|
|
192
|
+
#composer { padding:10px; }
|
|
193
|
+
#composer .send span:last-child { display:none; }
|
|
194
|
+
}
|
|
195
|
+
@media (prefers-reduced-motion: reduce) {
|
|
196
|
+
*, *::before, *::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; }
|
|
197
|
+
}
|
|
59
198
|
</style>
|
|
60
199
|
</head>
|
|
61
200
|
<body>
|
|
62
201
|
<form id="login">
|
|
63
|
-
<div class="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
202
|
+
<div class="card">
|
|
203
|
+
<div class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"/><path d="M8.5 10.5h7M8.5 14h4.25"/></svg></div>
|
|
204
|
+
<div class="brand"><b>@yaebal</b>/panel</div>
|
|
205
|
+
<div class="sub">secure operator console</div>
|
|
206
|
+
<input id="token" type="password" placeholder="access token" autocomplete="off" autofocus />
|
|
207
|
+
<button id="go" type="submit">authorize</button>
|
|
208
|
+
<div class="err" id="err"></div>
|
|
209
|
+
</div>
|
|
68
210
|
</form>
|
|
69
211
|
|
|
70
212
|
<div id="app">
|
|
71
|
-
<
|
|
72
|
-
|
|
213
|
+
<aside id="chats">
|
|
214
|
+
<div class="side-top">
|
|
215
|
+
<div class="side-title">
|
|
216
|
+
<div class="side-logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"/><path d="M8.5 10.5h7M8.5 14h4.25"/></svg></div>
|
|
217
|
+
<div><h1>operator panel</h1><p>live private chats</p></div>
|
|
218
|
+
</div>
|
|
219
|
+
<button id="logout" class="ghost" type="button" aria-label="log out"></button>
|
|
220
|
+
</div>
|
|
221
|
+
<div id="chat-list"></div>
|
|
222
|
+
</aside>
|
|
223
|
+
<main id="main"><div id="empty"><div class="empty-card"><div class="empty-icon"></div><div>select a chat</div></div></div></main>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div id="viewer" hidden>
|
|
227
|
+
<div class="viewer-card">
|
|
228
|
+
<div class="viewer-top"><button class="viewer-close" type="button" aria-label="close viewer"></button></div>
|
|
229
|
+
<div class="viewer-frame"></div>
|
|
230
|
+
<div class="viewer-caption"></div>
|
|
231
|
+
</div>
|
|
73
232
|
</div>
|
|
74
233
|
|
|
75
234
|
<script>
|
|
76
235
|
const BASE = "__BASE__";
|
|
77
236
|
const KEY = "yaebal-panel-token" + BASE;
|
|
78
237
|
let token = sessionStorage.getItem(KEY) || "";
|
|
79
|
-
let active = null, oldest = null, es = null;
|
|
238
|
+
let active = null, oldest = null, es = null, chatsCache = [];
|
|
239
|
+
|
|
240
|
+
const ICONS = {
|
|
241
|
+
logout:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M10 6H6.8A1.8 1.8 0 0 0 5 7.8v8.4A1.8 1.8 0 0 0 6.8 18H10"/><path d="M14 8l4 4-4 4"/><path d="M18 12H9"/></svg>',
|
|
242
|
+
empty:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 7.5A3.5 3.5 0 0 1 8.5 4h7A3.5 3.5 0 0 1 19 7.5v4A3.5 3.5 0 0 1 15.5 15H12l-4.2 3.6V15A3.5 3.5 0 0 1 5 11.5v-4Z"/></svg>',
|
|
243
|
+
attach:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="m8.5 12.5 5.9-5.9a3.2 3.2 0 0 1 4.5 4.5l-7.1 7.1a4.6 4.6 0 0 1-6.5-6.5l7.3-7.3"/><path d="m9.5 15.1 7-7"/></svg>',
|
|
244
|
+
send:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M20 4 10.6 20l-1.8-7.2L4 10.5 20 4Z"/><path d="m8.8 12.8 5.4-3.5"/></svg>',
|
|
245
|
+
back:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M15 6 9 12l6 6"/></svg>',
|
|
246
|
+
close:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M7 7l10 10M17 7 7 17"/></svg>',
|
|
247
|
+
image:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="5" width="16" height="14" rx="3"/><path d="m7 16 3.5-3.5 2.7 2.7 1.8-1.8L18 16"/><circle cx="9" cy="9" r="1.2"/></svg>',
|
|
248
|
+
video:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="6" width="11" height="12" rx="3"/><path d="m15 10 5-3v10l-5-3"/></svg>',
|
|
249
|
+
mic:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="9" y="4" width="6" height="10" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>',
|
|
250
|
+
audio:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 18V7l10-2v11"/><circle cx="6" cy="18" r="3"/><circle cx="16" cy="16" r="3"/></svg>',
|
|
251
|
+
file:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M7 3h6l4 4v14H7z"/><path d="M13 3v5h5M9.5 13h5M9.5 16h5"/></svg>',
|
|
252
|
+
sticker:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 4h12v9l-6 7H6z"/><path d="M12 20v-7h6M9 9h.01M14 9h.01"/></svg>',
|
|
253
|
+
callback:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="7" width="16" height="10" rx="4"/><path d="M8 12h8"/></svg>',
|
|
254
|
+
event:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3v3M12 18v3M4.6 7.5l2.6 1.5M16.8 15l2.6 1.5M4.6 16.5 7.2 15M16.8 9l2.6-1.5"/><circle cx="12" cy="12" r="3.2"/></svg>',
|
|
255
|
+
keyboard:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="6" width="16" height="12" rx="3"/><path d="M7 10h.01M10.5 10h.01M14 10h.01M17.5 10h.01M7 14h7M16.5 14h.01"/></svg>',
|
|
256
|
+
search:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></svg>',
|
|
257
|
+
link:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M10 13a4 4 0 0 0 5.7 0l2-2a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 11a4 4 0 0 0-5.7 0l-2 2A4 4 0 0 0 12 18.7l1-1"/></svg>',
|
|
258
|
+
user:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="8" r="3.5"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg>'
|
|
259
|
+
};
|
|
80
260
|
|
|
81
261
|
const el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; };
|
|
262
|
+
const icon = (name, c) => { const s = el("span", "ico" + (c ? " " + c : "")); s.innerHTML = ICONS[name] || ICONS.file; return s; };
|
|
82
263
|
const api = (p, opt = {}) => fetch(BASE + p, { ...opt, headers: { ...(opt.headers||{}), authorization: "Bearer " + token } });
|
|
83
264
|
|
|
84
|
-
|
|
265
|
+
document.getElementById("logout").append(icon("logout"));
|
|
266
|
+
document.querySelector("#empty .empty-icon").append(icon("empty"));
|
|
267
|
+
document.querySelector(".viewer-close").append(icon("close"));
|
|
268
|
+
|
|
85
269
|
const login = document.getElementById("login");
|
|
86
270
|
login.onsubmit = async (e) => {
|
|
87
271
|
e.preventDefault();
|
|
@@ -96,19 +280,13 @@ login.onsubmit = async (e) => {
|
|
|
96
280
|
enter();
|
|
97
281
|
};
|
|
98
282
|
document.getElementById("logout").onclick = () => {
|
|
99
|
-
sessionStorage.removeItem(KEY); token = ""; active = null;
|
|
283
|
+
sessionStorage.removeItem(KEY); token = ""; active = null; chatsCache = [];
|
|
100
284
|
if (es) { es.close(); es = null; }
|
|
101
|
-
document.body.classList.remove("authed");
|
|
285
|
+
document.body.classList.remove("authed", "chat-open");
|
|
102
286
|
document.getElementById("token").value = "";
|
|
103
287
|
};
|
|
104
288
|
|
|
105
|
-
function enter() {
|
|
106
|
-
document.body.classList.add("authed");
|
|
107
|
-
loadChats();
|
|
108
|
-
openStream();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/* ---- realtime: instant via SSE, with a slow polling safety net ---- */
|
|
289
|
+
function enter() { document.body.classList.add("authed"); loadChats(); openStream(); }
|
|
112
290
|
function openStream() {
|
|
113
291
|
if (es || !window.EventSource) return;
|
|
114
292
|
es = new EventSource(BASE + "/api/stream?token=" + encodeURIComponent(token));
|
|
@@ -117,56 +295,162 @@ function openStream() {
|
|
|
117
295
|
if (active && e.chatId === active) openChat(active, true);
|
|
118
296
|
loadChats();
|
|
119
297
|
});
|
|
120
|
-
es.onerror = () => {};
|
|
298
|
+
es.onerror = () => {};
|
|
121
299
|
}
|
|
122
300
|
setInterval(() => { if (token) (active ? openChat(active, true) : loadChats()); }, 8000);
|
|
123
301
|
|
|
124
|
-
|
|
302
|
+
function displayName(c) {
|
|
303
|
+
const full = [c.firstName, c.lastName].filter(Boolean).join(" ").trim();
|
|
304
|
+
return full || (c.username ? "@" + c.username : c.name || "chat " + c.id);
|
|
305
|
+
}
|
|
306
|
+
function handleName(c) {
|
|
307
|
+
if (c.username) return "@" + c.username;
|
|
308
|
+
const name = displayName(c);
|
|
309
|
+
return c.name && c.name !== name ? c.name : "";
|
|
310
|
+
}
|
|
311
|
+
function avatar(c, small) {
|
|
312
|
+
const a = el("div", "avatar" + (small ? " small" : ""));
|
|
313
|
+
a.append(icon("user"));
|
|
314
|
+
return a;
|
|
315
|
+
}
|
|
316
|
+
function time(ts) {
|
|
317
|
+
if (!ts) return "";
|
|
318
|
+
const d = new Date(ts * 1000);
|
|
319
|
+
return d.toLocaleTimeString([], { hour:"2-digit", minute:"2-digit" });
|
|
320
|
+
}
|
|
321
|
+
const isPlaceholder = (t) => /^\\[[a-z_]+\\]$/.test(t || "");
|
|
322
|
+
const placeholderKind = (t) => { const m = /^\\[([a-z_]+)\\]$/.exec(t || ""); return m && m[1]; };
|
|
323
|
+
function mediaIcon(type) {
|
|
324
|
+
if (type === "photo") return "image";
|
|
325
|
+
if (type === "video" || type === "animation" || type === "video_note") return "video";
|
|
326
|
+
if (type === "voice") return "mic";
|
|
327
|
+
if (type === "audio") return "audio";
|
|
328
|
+
if (type === "sticker") return "sticker";
|
|
329
|
+
return "file";
|
|
330
|
+
}
|
|
331
|
+
function mediaLabel(type) { return String(type || "media").replace(/_/g, " "); }
|
|
332
|
+
function eventIcon(type) { return type === "callback" ? "callback" : "event"; }
|
|
333
|
+
|
|
125
334
|
async function loadChats() {
|
|
126
335
|
const res = await api("/api/chats"); if (!res.ok) return;
|
|
127
|
-
|
|
128
|
-
const box = document.getElementById("
|
|
129
|
-
box.
|
|
130
|
-
for (const c of
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
336
|
+
chatsCache = await res.json();
|
|
337
|
+
const box = document.getElementById("chat-list");
|
|
338
|
+
box.innerHTML = "";
|
|
339
|
+
for (const c of chatsCache) box.append(chatRow(c));
|
|
340
|
+
}
|
|
341
|
+
function chatRow(c) {
|
|
342
|
+
const row = el("button", "chat" + (c.id === active ? " on" : "")); row.type = "button";
|
|
343
|
+
row.append(avatar(c));
|
|
344
|
+
const body = el("div", "chat-body");
|
|
345
|
+
const line = el("div", "chat-line");
|
|
346
|
+
line.append(el("div", "chat-name", displayName(c)), el("div", "chat-time", time(c.lastDate)));
|
|
347
|
+
body.append(line);
|
|
348
|
+
const handle = handleName(c); if (handle) body.append(el("div", "chat-handle", handle));
|
|
349
|
+
const preview = el("div", "preview");
|
|
350
|
+
const kind = c.lastAttachmentType || placeholderKind(c.lastText);
|
|
351
|
+
if (c.lastEventType) {
|
|
352
|
+
preview.append(icon(eventIcon(c.lastEventType)), el("span", "preview-label", "event"), el("span", "preview-text", c.lastText));
|
|
353
|
+
} else if (kind) {
|
|
354
|
+
const caption = isPlaceholder(c.lastText) ? "" : c.lastText;
|
|
355
|
+
preview.append(icon(mediaIcon(kind)), el("span", "preview-label", mediaLabel(kind)), el("span", "preview-text", caption || "media message"));
|
|
356
|
+
} else {
|
|
357
|
+
preview.append(el("span", "preview-text", c.lastText || "message"));
|
|
135
358
|
}
|
|
359
|
+
body.append(preview);
|
|
360
|
+
row.append(body);
|
|
361
|
+
row.onclick = () => openChat(c.id);
|
|
362
|
+
return row;
|
|
136
363
|
}
|
|
364
|
+
function chatById(id) { return chatsCache.find((c) => c.id === id) || { id, name:"chat " + id, lastText:"", lastDate:0 }; }
|
|
137
365
|
|
|
138
|
-
/* ---- media rendering ---- */
|
|
139
366
|
const fileSrc = (att) => BASE + "/api/file?id=" + encodeURIComponent(att.fileId) + "&token=" + encodeURIComponent(token);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
function attEl(att) {
|
|
367
|
+
function attEl(att, caption) {
|
|
143
368
|
const src = fileSrc(att);
|
|
144
|
-
if (att.type === "photo" || att.type === "sticker") {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
369
|
+
if (att.type === "photo" || att.type === "sticker") {
|
|
370
|
+
const btn = el("button", "media-frame image"); btn.type = "button";
|
|
371
|
+
const img = el("img"); img.src = src; img.loading = "lazy"; img.alt = att.type;
|
|
372
|
+
btn.append(img, el("span", "expand")); btn.querySelector(".expand").append(icon("search"));
|
|
373
|
+
btn.onclick = () => openViewer(att, caption);
|
|
374
|
+
return btn;
|
|
375
|
+
}
|
|
376
|
+
if (att.type === "video" || att.type === "animation" || att.type === "video_note") {
|
|
377
|
+
const box = el("div", "video-card");
|
|
378
|
+
const v = el("video"); v.src = src; v.controls = true; v.preload = "metadata";
|
|
379
|
+
const badge = el("div", "video-badge"); badge.append(icon("video"), el("span", null, mediaLabel(att.type)));
|
|
380
|
+
const expand = el("button", "expand"); expand.type = "button"; expand.append(icon("search")); expand.onclick = () => openViewer(att, caption);
|
|
381
|
+
box.append(v, badge, expand);
|
|
382
|
+
return box;
|
|
383
|
+
}
|
|
384
|
+
if (att.type === "voice") {
|
|
385
|
+
const card = el("div", "voice-card");
|
|
386
|
+
const mark = el("div", "voice-mark"); mark.append(icon("mic"));
|
|
387
|
+
const wave = el("div", "wave"); for (let i = 0; i < 6; i++) wave.append(el("i"));
|
|
388
|
+
const a = el("audio"); a.src = src; a.controls = true; a.preload = "metadata";
|
|
389
|
+
card.append(mark, wave, a);
|
|
390
|
+
return card;
|
|
391
|
+
}
|
|
392
|
+
if (att.type === "audio") {
|
|
393
|
+
const card = el("div", "audio-card");
|
|
394
|
+
const mark = el("div", "voice-mark"); mark.append(icon("audio"));
|
|
395
|
+
const a = el("audio"); a.src = src; a.controls = true; a.preload = "metadata";
|
|
396
|
+
card.append(mark, a);
|
|
397
|
+
return card;
|
|
398
|
+
}
|
|
399
|
+
const link = el("a", "doc-card"); link.href = src; link.target = "_blank"; link.rel = "noreferrer";
|
|
400
|
+
const mark = el("div", "doc-mark"); mark.append(icon("file"));
|
|
401
|
+
const meta = el("div"); meta.append(el("div", "doc-title", att.fileName || mediaLabel(att.type)), el("div", "doc-sub", att.mimeType || "telegram file"));
|
|
402
|
+
link.append(mark, meta);
|
|
403
|
+
return link;
|
|
404
|
+
}
|
|
405
|
+
function keyboardEl(k) {
|
|
406
|
+
const box = el("div", "keyboard");
|
|
407
|
+
for (const row of k.rows || []) {
|
|
408
|
+
const r = el("div", "keyboard-row");
|
|
409
|
+
for (const b of row) {
|
|
410
|
+
const node = b.url ? el("a", "key") : el("div", "key");
|
|
411
|
+
if (b.url) { node.href = b.url; node.target = "_blank"; node.rel = "noreferrer"; }
|
|
412
|
+
node.append(icon(b.kind === "url" ? "link" : "keyboard"), el("span", null, b.text));
|
|
413
|
+
r.append(node);
|
|
414
|
+
}
|
|
415
|
+
box.append(r);
|
|
416
|
+
}
|
|
417
|
+
return box;
|
|
418
|
+
}
|
|
419
|
+
function eventBubble(m) {
|
|
420
|
+
const e = el("div", "eventline");
|
|
421
|
+
e.append(icon(eventIcon(m.event.type)), el("span", "event-title", m.event.title));
|
|
422
|
+
if (m.event.detail) e.append(el("span", null, m.event.detail));
|
|
423
|
+
return e;
|
|
148
424
|
}
|
|
149
|
-
|
|
150
425
|
function bubble(m) {
|
|
151
|
-
|
|
426
|
+
if (m.event) return eventBubble(m);
|
|
427
|
+
const wrap = el("div", "msg " + m.direction);
|
|
428
|
+
const card = el("div", "bubble");
|
|
152
429
|
const atts = m.attachments || [];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
430
|
+
if (m.mediaGroupId && atts.length) {
|
|
431
|
+
const grid = el("div", "album-grid");
|
|
432
|
+
for (const a of atts) grid.append(attEl(a, m.text));
|
|
433
|
+
card.append(grid);
|
|
434
|
+
} else {
|
|
435
|
+
for (const a of atts) card.append(attEl(a, m.text));
|
|
436
|
+
}
|
|
437
|
+
if (m.text && !(atts.length && isPlaceholder(m.text))) card.append(el("div", atts.length ? "cap" : "cap", m.text));
|
|
438
|
+
if (m.keyboard) card.append(keyboardEl(m.keyboard));
|
|
439
|
+
wrap.append(card, el("div", "time", time(m.date)));
|
|
440
|
+
return wrap;
|
|
441
|
+
}
|
|
442
|
+
function addToAlbum(node, m) {
|
|
443
|
+
const grid = node.querySelector(".album-grid"); if (!grid) return;
|
|
444
|
+
for (const a of m.attachments || []) grid.append(attEl(a, m.text));
|
|
445
|
+
if (m.text && !isPlaceholder(m.text)) node.querySelector(".bubble").append(el("div", "cap", m.text));
|
|
156
446
|
}
|
|
157
|
-
|
|
158
|
-
/* merge consecutive messages sharing a media_group_id into one album bubble */
|
|
159
447
|
function renderMsgs(msgs) {
|
|
160
448
|
const out = [];
|
|
161
449
|
let group = null, groupId = null;
|
|
162
450
|
for (const m of msgs) {
|
|
163
|
-
if (m.mediaGroupId && m.mediaGroupId === groupId && group) {
|
|
164
|
-
for (const a of m.attachments || []) group.insertBefore(attEl(a), group.querySelector(".cap"));
|
|
165
|
-
if (m.text && !isPlaceholder(m.text)) group.append(el("div", "cap", m.text));
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
451
|
+
if (!m.event && m.mediaGroupId && m.mediaGroupId === groupId && group) { addToAlbum(group, m); continue; }
|
|
168
452
|
const b = bubble(m);
|
|
169
|
-
if (m.mediaGroupId) {
|
|
453
|
+
if (!m.event && m.mediaGroupId) { group = b; groupId = m.mediaGroupId; }
|
|
170
454
|
else { group = null; groupId = null; }
|
|
171
455
|
out.push(b);
|
|
172
456
|
}
|
|
@@ -174,29 +458,28 @@ function renderMsgs(msgs) {
|
|
|
174
458
|
}
|
|
175
459
|
|
|
176
460
|
async function openChat(id, keepScroll) {
|
|
177
|
-
active = id;
|
|
461
|
+
active = id; document.body.classList.add("chat-open");
|
|
178
462
|
if (!keepScroll) loadChats();
|
|
179
463
|
const res = await api("/api/chats/" + id + "?limit=200"); if (!res.ok) return;
|
|
180
|
-
const msgs = await res.json();
|
|
181
|
-
|
|
182
|
-
|
|
464
|
+
const msgs = await res.json(); oldest = msgs.length ? msgs[0].date : null;
|
|
465
|
+
const c = chatById(id);
|
|
183
466
|
const main = document.getElementById("main");
|
|
184
467
|
const prevTop = keepScroll ? (main.querySelector("#log")?.scrollTop ?? null) : null;
|
|
185
468
|
main.innerHTML = "";
|
|
186
469
|
|
|
470
|
+
const head = el("div", "thread-head");
|
|
471
|
+
const back = el("button", "ghost back"); back.type = "button"; back.append(icon("back")); back.onclick = () => { document.body.classList.remove("chat-open"); };
|
|
472
|
+
const meta = el("div", "meta"); meta.append(el("div", "name", displayName(c)), el("div", "sub", handleName(c) || "private chat"));
|
|
473
|
+
head.append(back, avatar(c, true), meta);
|
|
187
474
|
const log = el("div"); log.id = "log";
|
|
188
|
-
if (msgs.length >= 200) {
|
|
189
|
-
const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log);
|
|
190
|
-
log.append(more);
|
|
191
|
-
}
|
|
475
|
+
if (msgs.length >= 200) { const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log); log.append(more); }
|
|
192
476
|
for (const b of renderMsgs(msgs)) log.append(b);
|
|
193
477
|
|
|
194
478
|
const form = el("form"); form.id = "composer";
|
|
195
479
|
const fileInput = el("input"); fileInput.type = "file"; fileInput.style.display = "none";
|
|
196
|
-
const attach = el("button", "attach"
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const btn = el("button", null, "send"); btn.type = "submit";
|
|
480
|
+
const attach = el("button", "attach"); attach.type = "button"; attach.title = "send a file"; attach.append(icon("attach")); attach.onclick = () => fileInput.click();
|
|
481
|
+
const input = el("input", "text"); input.placeholder = "reply..."; input.autocomplete = "off";
|
|
482
|
+
const btn = el("button", "send"); btn.type = "submit"; btn.append(icon("send"), el("span", null, "send"));
|
|
200
483
|
form.append(attach, fileInput, input, btn);
|
|
201
484
|
|
|
202
485
|
fileInput.onchange = async () => {
|
|
@@ -204,7 +487,7 @@ async function openChat(id, keepScroll) {
|
|
|
204
487
|
const fd = new FormData(); fd.append("file", file);
|
|
205
488
|
if (input.value.trim()) fd.append("caption", input.value.trim());
|
|
206
489
|
input.value = ""; fileInput.value = "";
|
|
207
|
-
await api("/api/chats/" + id + "/send", { method: "POST", body: fd });
|
|
490
|
+
await api("/api/chats/" + id + "/send", { method: "POST", body: fd });
|
|
208
491
|
openChat(id);
|
|
209
492
|
};
|
|
210
493
|
form.onsubmit = async (e) => {
|
|
@@ -214,10 +497,9 @@ async function openChat(id, keepScroll) {
|
|
|
214
497
|
await api("/api/chats/" + id + "/send", { method:"POST", headers:{"content-type":"application/json"}, body: JSON.stringify({ text }) });
|
|
215
498
|
openChat(id);
|
|
216
499
|
};
|
|
217
|
-
main.append(log, form);
|
|
500
|
+
main.append(head, log, form);
|
|
218
501
|
log.scrollTop = prevTop != null ? prevTop : log.scrollHeight;
|
|
219
502
|
}
|
|
220
|
-
|
|
221
503
|
async function loadEarlier(id, log) {
|
|
222
504
|
if (oldest == null) return;
|
|
223
505
|
const res = await api("/api/chats/" + id + "?limit=200&before=" + oldest); if (!res.ok) return;
|
|
@@ -228,9 +510,24 @@ async function loadEarlier(id, log) {
|
|
|
228
510
|
const frag = document.createDocumentFragment();
|
|
229
511
|
if (older.length >= 200) { const more = el("button", "more", "load earlier"); more.onclick = () => loadEarlier(id, log); frag.append(more); }
|
|
230
512
|
for (const b of renderMsgs(older)) frag.append(b);
|
|
231
|
-
log.querySelector(".more")?.remove();
|
|
232
|
-
|
|
513
|
+
log.querySelector(".more")?.remove(); log.insertBefore(frag, anchor);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function openViewer(att, caption) {
|
|
517
|
+
const viewer = document.getElementById("viewer"), frame = viewer.querySelector(".viewer-frame"), cap = viewer.querySelector(".viewer-caption");
|
|
518
|
+
frame.innerHTML = ""; cap.textContent = caption && !isPlaceholder(caption) ? caption : "";
|
|
519
|
+
const src = fileSrc(att);
|
|
520
|
+
if (att.type === "video" || att.type === "animation" || att.type === "video_note") {
|
|
521
|
+
const v = el("video"); v.src = src; v.controls = true; v.autoplay = true; frame.append(v);
|
|
522
|
+
} else {
|
|
523
|
+
const img = el("img"); img.src = src; img.alt = att.type; frame.append(img);
|
|
524
|
+
}
|
|
525
|
+
viewer.hidden = false;
|
|
233
526
|
}
|
|
527
|
+
function closeViewer() { document.getElementById("viewer").hidden = true; }
|
|
528
|
+
document.querySelector(".viewer-close").onclick = closeViewer;
|
|
529
|
+
document.getElementById("viewer").onclick = (e) => { if (e.target.id === "viewer") closeViewer(); };
|
|
530
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeViewer(); });
|
|
234
531
|
|
|
235
532
|
if (token) enter();
|
|
236
533
|
</script>
|