@awiki/dsh-plugin 0.2.4 → 0.3.0-rc.1
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 +131 -22
- package/README.zh.md +110 -20
- package/THIRD_PARTY_NOTICES.md +8 -1
- package/cordis.patch.yml +5 -0
- package/lib/client.js +12534 -2647
- package/lib/client.js.map +1 -1
- package/lib/index.js +2431 -92
- package/lib/model-proxy-contract.js +96 -0
- package/lib/provider.js +4 -1
- package/lib/sdk-adapter-DLhLrLI6.mjs +886 -0
- package/lib/typert.host.js +1269 -534
- package/lib/typert.remote-client.d.ts +47 -1
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +1254 -141
- package/lib/types/attachment-cache.d.ts +28 -0
- package/lib/types/attachment-cache.d.ts.map +1 -0
- package/lib/types/attachment-cache.js +194 -0
- package/lib/types/attachment-cache.js.map +1 -0
- package/lib/types/client/AwikiGroupDetails.d.ts +16 -0
- package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -0
- package/lib/types/client/AwikiGroupDetails.js +108 -0
- package/lib/types/client/AwikiGroupDetails.js.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts +21 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.js +148 -0
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -0
- package/lib/types/client/AwikiIdentityPage.d.ts +11 -0
- package/lib/types/client/AwikiIdentityPage.d.ts.map +1 -0
- package/lib/types/client/AwikiIdentityPage.js +8 -0
- package/lib/types/client/AwikiIdentityPage.js.map +1 -0
- package/lib/types/client/AwikiMail.d.ts +15 -0
- package/lib/types/client/AwikiMail.d.ts.map +1 -0
- package/lib/types/client/AwikiMail.js +380 -0
- package/lib/types/client/AwikiMail.js.map +1 -0
- package/lib/types/client/AwikiOnboarding.d.ts +22 -0
- package/lib/types/client/AwikiOnboarding.d.ts.map +1 -0
- package/lib/types/client/AwikiOnboarding.js +104 -0
- package/lib/types/client/AwikiOnboarding.js.map +1 -0
- package/lib/types/client/AwikiOverlay.d.ts +10 -0
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +370 -127
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiProfileCard.d.ts +9 -0
- package/lib/types/client/AwikiProfileCard.d.ts.map +1 -0
- package/lib/types/client/AwikiProfileCard.js +90 -0
- package/lib/types/client/AwikiProfileCard.js.map +1 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts +24 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -0
- package/lib/types/client/AwikiRecoveryForm.js +148 -0
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -0
- package/lib/types/client/AwikiSettingsSection.d.ts +18 -4
- package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
- package/lib/types/client/AwikiSettingsSection.js +277 -23
- package/lib/types/client/AwikiSettingsSection.js.map +1 -1
- package/lib/types/client/MentionText.d.ts +7 -0
- package/lib/types/client/MentionText.d.ts.map +1 -0
- package/lib/types/client/MentionText.js +10 -0
- package/lib/types/client/MentionText.js.map +1 -0
- package/lib/types/client/RechargeComingSoonDialog.d.ts +11 -0
- package/lib/types/client/RechargeComingSoonDialog.d.ts.map +1 -0
- package/lib/types/client/RechargeComingSoonDialog.js +8 -0
- package/lib/types/client/RechargeComingSoonDialog.js.map +1 -0
- package/lib/types/client/controller.d.ts +174 -4
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +975 -79
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/image-cache.d.ts +20 -0
- package/lib/types/client/image-cache.d.ts.map +1 -0
- package/lib/types/client/image-cache.js +177 -0
- package/lib/types/client/image-cache.js.map +1 -0
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +102 -42
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/mail-list-cache.d.ts +15 -0
- package/lib/types/client/mail-list-cache.d.ts.map +1 -0
- package/lib/types/client/mail-list-cache.js +212 -0
- package/lib/types/client/mail-list-cache.js.map +1 -0
- package/lib/types/client/mentions.d.ts +51 -0
- package/lib/types/client/mentions.d.ts.map +1 -0
- package/lib/types/client/mentions.js +165 -0
- package/lib/types/client/mentions.js.map +1 -0
- package/lib/types/client/model-availability-controller.d.ts +27 -0
- package/lib/types/client/model-availability-controller.d.ts.map +1 -0
- package/lib/types/client/model-availability-controller.js +111 -0
- package/lib/types/client/model-availability-controller.js.map +1 -0
- package/lib/types/client/model-proxy-controller.d.ts +45 -0
- package/lib/types/client/model-proxy-controller.d.ts.map +1 -0
- package/lib/types/client/model-proxy-controller.js +306 -0
- package/lib/types/client/model-proxy-controller.js.map +1 -0
- package/lib/types/client/recharge-availability.d.ts +6 -0
- package/lib/types/client/recharge-availability.d.ts.map +1 -0
- package/lib/types/client/recharge-availability.js +6 -0
- package/lib/types/client/recharge-availability.js.map +1 -0
- package/lib/types/client/settings-locales.d.ts +1 -1
- package/lib/types/client/settings-locales.d.ts.map +1 -1
- package/lib/types/client/settings-locales.js +186 -8
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +34 -2
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/index.d.ts +95 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +968 -37
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener-state.d.ts +23 -0
- package/lib/types/listener-state.d.ts.map +1 -0
- package/lib/types/listener-state.js +124 -0
- package/lib/types/listener-state.js.map +1 -0
- package/lib/types/listener.d.ts +96 -0
- package/lib/types/listener.d.ts.map +1 -0
- package/lib/types/listener.js +575 -0
- package/lib/types/listener.js.map +1 -0
- package/lib/types/mail.d.ts +8 -0
- package/lib/types/mail.d.ts.map +1 -0
- package/lib/types/mail.js +75 -0
- package/lib/types/mail.js.map +1 -0
- package/lib/types/model-proxy-contract.d.ts +67 -0
- package/lib/types/model-proxy-contract.d.ts.map +1 -0
- package/lib/types/model-proxy-contract.js +135 -0
- package/lib/types/model-proxy-contract.js.map +1 -0
- package/lib/types/provider-api.d.ts +106 -2
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +3 -0
- package/lib/types/provider.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts +43 -2
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +622 -24
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/sent-mail-store.d.ts +22 -0
- package/lib/types/sent-mail-store.d.ts.map +1 -0
- package/lib/types/sent-mail-store.js +246 -0
- package/lib/types/sent-mail-store.js.map +1 -0
- package/lib/types/tools.d.ts +10 -0
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/tools.js +98 -0
- package/lib/types/tools.js.map +1 -1
- package/lib/types/types.d.ts +254 -2
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +31 -17
- package/lib/sdk-adapter-DK6RqV-c.mjs +0 -355
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/** AWiki trigger, identity registration, and direct/group messaging drawer. */
|
|
3
3
|
import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
-
import { IconChevronLeftOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconCloseOutline16, IconCopyOutline16, IconDataOutline16, IconDownloadOutline16,
|
|
4
|
+
import { IconChevronLeftOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconCloseOutline16, IconCopyOutline16, IconDataOutline16, IconDownloadOutline16, IconGlobeOutline14, IconGoalOutline16, IconLoadingOutline16, IconPaperclipOutline16, IconPlusOutline16, IconRefreshOutline16, IconRefreshOutline14, IconSendOutline16, IconSettingsOutline16, IconSparkle16, Button, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
5
|
import { AWIKI_ME_APP_ICON_DATA_URL } from "./assets.js";
|
|
6
6
|
import { createAttachmentObjectUrl, fileToBase64, saveDownloadedAttachment } from "./file.js";
|
|
7
|
+
import { AwikiMail } from "./AwikiMail.js";
|
|
8
|
+
import { AwikiGroupDetails } from "./AwikiGroupDetails.js";
|
|
9
|
+
import { AwikiIdentityAccess } from "./AwikiIdentityAccess.js";
|
|
10
|
+
import { AwikiProfileCard } from "./AwikiProfileCard.js";
|
|
11
|
+
import { MentionText } from "./MentionText.js";
|
|
12
|
+
import { activeMentionQuery, codePointIndexToUtf16Index, insertMention, mentionCandidates, protocolMentions, transformMentionDrafts, utf16IndexToCodePointIndex, } from "./mentions.js";
|
|
7
13
|
import css from './AwikiOverlay.module.css';
|
|
8
14
|
export const AWIKI_LAUNCHER_POSITION_KEY = 'dsh-awiki-launcher-position-v1';
|
|
15
|
+
export const AWIKI_DRAWER_FRAME_KEY = 'dsh-awiki-drawer-frame-v1';
|
|
9
16
|
const LAUNCHER_SIZE = 48;
|
|
10
17
|
const LAUNCHER_EDGE_GAP = 8;
|
|
11
18
|
const LAUNCHER_RIGHT_OFFSET = 28;
|
|
@@ -15,10 +22,15 @@ const DRAWER_LONG_PRESS_MS = 300;
|
|
|
15
22
|
const DRAWER_ANCHOR_GAP = 8;
|
|
16
23
|
const DRAWER_EDGE_GAP = 8;
|
|
17
24
|
const DRAWER_NOMINAL_WIDTH = 720;
|
|
25
|
+
const MAIL_DRAWER_NOMINAL_WIDTH = 1040;
|
|
18
26
|
const DRAWER_NOMINAL_HEIGHT = 720;
|
|
19
27
|
const DRAWER_HORIZONTAL_RESERVE = 80;
|
|
28
|
+
const DRAWER_COMPACT_HORIZONTAL_RESERVE = 56;
|
|
29
|
+
const DRAWER_MIN_WIDTH = 360;
|
|
30
|
+
const DRAWER_MIN_HEIGHT = 360;
|
|
20
31
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
21
32
|
const HISTORY_BOTTOM_THRESHOLD = 24;
|
|
33
|
+
const DRAWER_RESIZE_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
|
|
22
34
|
/** Keep the floating launcher fully reachable inside the current viewport. */
|
|
23
35
|
export function clampAwikiLauncherPosition(position, width, height) {
|
|
24
36
|
return {
|
|
@@ -69,6 +81,40 @@ export function resolveAwikiDrawerPlacement(launcher, panelWidth, panelHeight, v
|
|
|
69
81
|
top: Math.min(Math.max(selected.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - panelHeight - DRAWER_EDGE_GAP)),
|
|
70
82
|
};
|
|
71
83
|
}
|
|
84
|
+
/** Keep a user-sized AWiki drawer reachable and within the current viewport. */
|
|
85
|
+
export function clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight) {
|
|
86
|
+
const maxWidth = Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2);
|
|
87
|
+
const maxHeight = Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2);
|
|
88
|
+
const minWidth = Math.min(DRAWER_MIN_WIDTH, maxWidth);
|
|
89
|
+
const minHeight = Math.min(DRAWER_MIN_HEIGHT, maxHeight);
|
|
90
|
+
const width = Math.min(Math.max(frame.width, minWidth), maxWidth);
|
|
91
|
+
const height = Math.min(Math.max(frame.height, minHeight), maxHeight);
|
|
92
|
+
return {
|
|
93
|
+
left: Math.min(Math.max(frame.left, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportWidth - width - DRAWER_EDGE_GAP)),
|
|
94
|
+
top: Math.min(Math.max(frame.top, DRAWER_EDGE_GAP), Math.max(DRAWER_EDGE_GAP, viewportHeight - height - DRAWER_EDGE_GAP)),
|
|
95
|
+
width,
|
|
96
|
+
height,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Resize one or two drawer boundaries while keeping the opposite boundaries fixed. */
|
|
100
|
+
export function resizeAwikiDrawerFrame(frame, direction, deltaX, deltaY, viewportWidth, viewportHeight) {
|
|
101
|
+
const origin = clampAwikiDrawerFrame(frame, viewportWidth, viewportHeight);
|
|
102
|
+
const minWidth = Math.min(DRAWER_MIN_WIDTH, Math.max(1, viewportWidth - DRAWER_EDGE_GAP * 2));
|
|
103
|
+
const minHeight = Math.min(DRAWER_MIN_HEIGHT, Math.max(1, viewportHeight - DRAWER_EDGE_GAP * 2));
|
|
104
|
+
let left = origin.left;
|
|
105
|
+
let top = origin.top;
|
|
106
|
+
let right = origin.left + origin.width;
|
|
107
|
+
let bottom = origin.top + origin.height;
|
|
108
|
+
if (direction.includes('w'))
|
|
109
|
+
left = Math.min(Math.max(left + deltaX, DRAWER_EDGE_GAP), right - minWidth);
|
|
110
|
+
if (direction.includes('e'))
|
|
111
|
+
right = Math.max(Math.min(right + deltaX, viewportWidth - DRAWER_EDGE_GAP), left + minWidth);
|
|
112
|
+
if (direction.includes('n'))
|
|
113
|
+
top = Math.min(Math.max(top + deltaY, DRAWER_EDGE_GAP), bottom - minHeight);
|
|
114
|
+
if (direction.includes('s'))
|
|
115
|
+
bottom = Math.max(Math.min(bottom + deltaY, viewportHeight - DRAWER_EDGE_GAP), top + minHeight);
|
|
116
|
+
return clampAwikiDrawerFrame({ left, top, width: right - left, height: bottom - top }, viewportWidth, viewportHeight);
|
|
117
|
+
}
|
|
72
118
|
function defaultLauncherPosition() {
|
|
73
119
|
return clampAwikiLauncherPosition({
|
|
74
120
|
left: window.innerWidth - LAUNCHER_SIZE - LAUNCHER_RIGHT_OFFSET,
|
|
@@ -99,6 +145,30 @@ function saveLauncherPosition(position) {
|
|
|
99
145
|
// The launcher remains usable when session storage is unavailable.
|
|
100
146
|
}
|
|
101
147
|
}
|
|
148
|
+
function readDrawerFrame() {
|
|
149
|
+
try {
|
|
150
|
+
const stored = window.sessionStorage.getItem(AWIKI_DRAWER_FRAME_KEY);
|
|
151
|
+
if (stored !== null) {
|
|
152
|
+
const value = JSON.parse(stored);
|
|
153
|
+
const { left, top, width, height } = value;
|
|
154
|
+
if ([left, top, width, height].every(item => typeof item === 'number' && Number.isFinite(item))) {
|
|
155
|
+
return clampAwikiDrawerFrame({ left: left, top: top, width: width, height: height }, window.innerWidth, window.innerHeight);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// The default drawer remains usable when session storage is unavailable.
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function saveDrawerFrame(frame) {
|
|
165
|
+
try {
|
|
166
|
+
window.sessionStorage.setItem(AWIKI_DRAWER_FRAME_KEY, JSON.stringify(frame));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Resizing remains usable when session storage is unavailable.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
102
172
|
function callPointerCapture(target, method, pointerId) {
|
|
103
173
|
const capture = Reflect.get(target, method);
|
|
104
174
|
if (typeof capture === 'function')
|
|
@@ -115,112 +185,15 @@ function conversationTime(value, now = Date.now()) {
|
|
|
115
185
|
? new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(value)
|
|
116
186
|
: new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(value);
|
|
117
187
|
}
|
|
118
|
-
/** Render the identity registration form and its OTP challenge transition. */
|
|
119
|
-
function Registration(props) {
|
|
120
|
-
const [phone, setPhone] = useState('');
|
|
121
|
-
const [handle, setHandle] = useState('');
|
|
122
|
-
const [otp, setOtp] = useState('');
|
|
123
|
-
const [otpSent, setOtpSent] = useState(false);
|
|
124
|
-
const [notice, setNotice] = useState(null);
|
|
125
|
-
const [retryDeadline, setRetryDeadline] = useState(null);
|
|
126
|
-
const [retrySeconds, setRetrySeconds] = useState(0);
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (retryDeadline === null)
|
|
129
|
-
return;
|
|
130
|
-
const update = () => {
|
|
131
|
-
const remaining = Math.max(0, Math.ceil((retryDeadline - Date.now()) / 1000));
|
|
132
|
-
setRetrySeconds(remaining);
|
|
133
|
-
if (remaining === 0)
|
|
134
|
-
setRetryDeadline(null);
|
|
135
|
-
};
|
|
136
|
-
update();
|
|
137
|
-
const timer = setInterval(update, 250);
|
|
138
|
-
return () => { clearInterval(timer); };
|
|
139
|
-
}, [retryDeadline]);
|
|
140
|
-
const requestOtp = async () => {
|
|
141
|
-
const result = await props.sendRegistrationOtp({ handle: handle.trim(), phone: phone.trim() });
|
|
142
|
-
if (!result.ok)
|
|
143
|
-
return;
|
|
144
|
-
const cooldownSeconds = Math.max(0, Math.ceil(result.value.retryAfterSeconds));
|
|
145
|
-
setOtpSent(true);
|
|
146
|
-
setRetryDeadline(Date.now() + cooldownSeconds * 1000);
|
|
147
|
-
setRetrySeconds(cooldownSeconds);
|
|
148
|
-
setNotice(`验证码已发送;${cooldownSeconds} 秒后可重新获取。`);
|
|
149
|
-
};
|
|
150
|
-
const register = async () => {
|
|
151
|
-
/* v8 ignore next -- the registration action is rendered only after an OTP challenge starts. */
|
|
152
|
-
if (!otpSent)
|
|
153
|
-
return;
|
|
154
|
-
const result = await props.registerIdentity({
|
|
155
|
-
phone: phone.trim(), handle: handle.trim(), otp: otp.trim(),
|
|
156
|
-
});
|
|
157
|
-
if (!result.ok)
|
|
158
|
-
return;
|
|
159
|
-
setNotice(null);
|
|
160
|
-
};
|
|
161
|
-
return (_jsxs("div", { className: css.registration, children: [_jsx("div", { className: css.registrationIcon, children: _jsx(IconUserOutline16, { size: 24 }) }), _jsx("h3", { children: "\u6CE8\u518C AWiki \u8EAB\u4EFD" }), _jsx("p", { children: "\u8BE5\u8EAB\u4EFD\u7531\u5F53\u524D Harness \u90E8\u7F72\u4E2D\u7684\u5168\u90E8 Agent \u5171\u540C\u4F7F\u7528\u3002" }), _jsxs("label", { children: ["Handle", _jsx("input", { value: handle, onChange: (event) => { setHandle(event.target.value); }, autoComplete: "username", placeholder: "\u4F8B\u5982 alice" })] }), _jsxs("label", { children: ["\u624B\u673A\u53F7", _jsx("input", { value: phone, onChange: (event) => { setPhone(event.target.value); }, autoComplete: "tel" })] }), !otpSent ? (_jsx("button", { type: "button", className: css.primary, disabled: props.pending || phone.trim() === '' || handle.trim() === '', onClick: () => { void requestOtp(); }, children: "\u83B7\u53D6\u9A8C\u8BC1\u7801" })) : (_jsxs(_Fragment, { children: [_jsxs("label", { children: ["\u9A8C\u8BC1\u7801", _jsx("input", { value: otp, onChange: (event) => { setOtp(event.target.value); }, inputMode: "numeric", autoComplete: "one-time-code" })] }), _jsx("button", { type: "button", className: css.primary, disabled: props.pending || handle.trim() === '' || otp.trim() === '', onClick: () => { void register(); }, children: "\u6CE8\u518C\u8EAB\u4EFD" }), _jsx("button", { type: "button", className: css.linkButton, disabled: props.pending || retrySeconds > 0, onClick: () => { void requestOtp(); }, children: retrySeconds > 0 ? `${retrySeconds} 秒后重新获取` : '重新获取验证码' })] })), notice !== null && _jsx("p", { className: css.notice, role: "status", children: notice })] }));
|
|
162
|
-
}
|
|
163
|
-
/** Let a signed-out installation resume its preserved local identity. */
|
|
164
|
-
function SignedOut(props) {
|
|
165
|
-
const [error, setError] = useState(null);
|
|
166
|
-
const login = async () => {
|
|
167
|
-
setError(null);
|
|
168
|
-
const result = await props.login();
|
|
169
|
-
if (!result.ok)
|
|
170
|
-
setError(result.error);
|
|
171
|
-
};
|
|
172
|
-
return (_jsxs("div", { className: css.centerState, children: [_jsx("div", { className: css.registrationIcon, children: _jsx(IconUserOutline16, { size: 24 }) }), _jsx("h3", { children: "\u5DF2\u9000\u51FA AWiki" }), _jsx("p", { children: "\u672C\u673A\u8EAB\u4EFD\u548C\u6D88\u606F\u6570\u636E\u4ECD\u5B89\u5168\u4FDD\u7559\u3002\u91CD\u65B0\u8FDB\u5165\u540E\u4F1A\u7EE7\u7EED\u4F7F\u7528\u539F\u6765\u7684 DID \u548C Handle\u3002" }), _jsx("button", { type: "button", className: css.primary, disabled: props.pending, onClick: () => { void login(); }, children: "\u91CD\u65B0\u8FDB\u5165" }), error !== null && _jsx("small", { className: css.inlineError, role: "alert", children: error })] }));
|
|
173
|
-
}
|
|
174
188
|
/** Prefer the peer WNS display name for a direct chat; groups keep their title. */
|
|
175
189
|
function conversationLabel(conversation) {
|
|
176
190
|
return conversation.kind === 'direct'
|
|
177
191
|
? (conversation.displayName ?? conversation.title)
|
|
178
192
|
: conversation.title;
|
|
179
193
|
}
|
|
180
|
-
/**
|
|
181
|
-
function
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
/** Editable deployment identity summary shown above the conversation roster. */
|
|
185
|
-
function IdentityCard(props) {
|
|
186
|
-
const [editing, setEditing] = useState(false);
|
|
187
|
-
const [draft, setDraft] = useState(props.identity.displayName ?? '');
|
|
188
|
-
const [error, setError] = useState(null);
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
if (!editing)
|
|
191
|
-
setDraft(props.identity.displayName ?? '');
|
|
192
|
-
}, [editing, props.identity.displayName]);
|
|
193
|
-
const cancel = () => {
|
|
194
|
-
setDraft(props.identity.displayName ?? '');
|
|
195
|
-
setError(null);
|
|
196
|
-
setEditing(false);
|
|
197
|
-
};
|
|
198
|
-
const save = async () => {
|
|
199
|
-
const displayName = draft.trim();
|
|
200
|
-
const length = Array.from(displayName).length;
|
|
201
|
-
if (length === 0) {
|
|
202
|
-
setError('请输入昵称');
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (length > 50) {
|
|
206
|
-
setError('昵称不能超过 50 个字符');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
setError(null);
|
|
210
|
-
const result = await props.updateDisplayName(displayName);
|
|
211
|
-
if (!result.ok) {
|
|
212
|
-
setError(result.error);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
setDraft(result.value.displayName ?? displayName);
|
|
216
|
-
setEditing(false);
|
|
217
|
-
};
|
|
218
|
-
return (_jsxs("div", { className: css.identityCard, children: [_jsx("div", { className: css.identityNameRow, children: editing ? (_jsxs("form", { className: css.identityEditor, onSubmit: (event) => { event.preventDefault(); void save(); }, children: [_jsx("input", { "aria-label": "\u6635\u79F0", autoFocus: true, disabled: props.pending, value: draft, onChange: (event) => { setDraft(event.target.value); }, onKeyDown: (event) => {
|
|
219
|
-
if (event.key === 'Escape') {
|
|
220
|
-
event.stopPropagation();
|
|
221
|
-
cancel();
|
|
222
|
-
}
|
|
223
|
-
} }), _jsx("button", { type: "submit", "aria-label": "\u4FDD\u5B58\u6635\u79F0", disabled: props.pending, children: _jsx(IconCheckOutline16, { size: 14 }) }), _jsx("button", { type: "button", "aria-label": "\u53D6\u6D88\u4FEE\u6539\u6635\u79F0", disabled: props.pending, onClick: cancel, children: _jsx(IconCloseOutline16, { size: 14 }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: props.identity.did, side: "bottom", children: _jsx("button", { type: "button", className: css.identityName, disabled: props.pending, onClick: () => { setError(null); setEditing(true); }, children: identityLabel(props.identity) }) }), _jsx(Tooltip, { label: "\u4FEE\u6539\u6635\u79F0", side: "right", children: _jsx("button", { type: "button", className: css.identityEdit, "aria-label": "\u4FEE\u6539\u6635\u79F0", disabled: props.pending, onClick: () => { setError(null); setEditing(true); }, children: _jsx(IconEditOutline16, { size: 14 }) }) })] })) }), _jsx("small", { className: css.identityHandle, children: props.identity.handle }), _jsxs("span", { className: css.identityStatus, children: [_jsx("i", {}), "\u5728\u7EBF"] }), error !== null && _jsx("small", { className: css.identityError, role: "alert", children: error })] }));
|
|
194
|
+
/** Switch the shared identity between messaging and on-demand mail. */
|
|
195
|
+
function ModeTabs(props) {
|
|
196
|
+
return (_jsxs("div", { className: css.modeTabs, role: "tablist", "aria-label": "AWiki \u529F\u80FD", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": props.mode === 'chat', onClick: () => { props.onChange('chat'); }, children: "\u4F1A\u8BDD" }), _jsxs("button", { type: "button", role: "tab", "aria-selected": props.mode === 'mail', onClick: () => { props.onChange('mail'); }, children: ["\u90AE\u4EF6", props.mailUnreadCount > 0 && _jsx("small", { children: props.mailUnreadCount > 99 ? '99+' : props.mailUnreadCount })] })] }));
|
|
224
197
|
}
|
|
225
198
|
/** Incoming sender label: WNS display name, then Handle, then DID. */
|
|
226
199
|
function senderLabel(message, peerLabel) {
|
|
@@ -284,11 +257,11 @@ function MessageRow(props) {
|
|
|
284
257
|
}
|
|
285
258
|
saveDownloadedAttachment(result.value);
|
|
286
259
|
};
|
|
287
|
-
return (_jsxs("div", { className: css.message, "data-message-id": props.message.id, "data-outgoing": props.message.outgoing || undefined, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: senderLabel(props.message, props.peerLabel) }), _jsx("time", { children: time(props.message.sentAt) })] }), props.message.content.kind === 'text' ? (_jsx("p", { children: props.message.content.text })) : preview !== null ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.imageAttachment, "aria-label": `下载图片 ${props.message.content.attachment.fileName}`, onClick: () => { void download(); }, children: [_jsx("img", { src: preview.url, alt: props.message.content.attachment.fileName, onLoad: () => { props.onImageLoad?.(props.message.id); } }), _jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsxs("small", { children: [props.message.content.attachment.size, " \u5B57\u8282"] }), _jsx(IconDownloadOutline16, { size: 16 })] })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.attachment, disabled: previewLoading, onClick: () => { void download(); }, children: [_jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsx("small", { children: previewLoading ? '正在加载图片预览…' : `${props.message.content.attachment.size} 字节` })] }), _jsx(IconDownloadOutline16, { size: 16 })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })), error !== null && _jsx("small", { className: css.inlineError, children: error })] }));
|
|
260
|
+
return (_jsxs("div", { className: css.message, "data-message-id": props.message.id, "data-outgoing": props.message.outgoing || undefined, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: senderLabel(props.message, props.peerLabel) }), _jsx("time", { children: time(props.message.sentAt) })] }), props.message.content.kind === 'text' ? (_jsx("p", { children: _jsx(MentionText, { text: props.message.content.text, ...props.message.content.mentions === undefined ? {} : { mentions: props.message.content.mentions } }) })) : preview !== null ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.imageAttachment, "aria-label": `下载图片 ${props.message.content.attachment.fileName}`, onClick: () => { void download(); }, children: [_jsx("img", { src: preview.url, alt: props.message.content.attachment.fileName, onLoad: () => { props.onImageLoad?.(props.message.id); } }), _jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsxs("small", { children: [props.message.content.attachment.size, " \u5B57\u8282"] }), _jsx(IconDownloadOutline16, { size: 16 })] })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.attachment, disabled: previewLoading, onClick: () => { void download(); }, children: [_jsxs("span", { children: [_jsx("strong", { children: props.message.content.attachment.fileName }), _jsx("small", { children: previewLoading ? '正在加载图片预览…' : `${props.message.content.attachment.size} 字节` })] }), _jsx(IconDownloadOutline16, { size: 16 })] }), props.message.content.caption !== undefined && _jsx("p", { className: css.caption, children: props.message.content.caption })] })), error !== null && _jsx("small", { className: css.inlineError, children: error })] }));
|
|
288
261
|
}
|
|
289
262
|
/** Render one optimistic outgoing bubble while the Host confirms delivery. */
|
|
290
263
|
function PendingMessageRow(props) {
|
|
291
|
-
return (_jsxs("div", { className: css.pendingMessage, role: "status", "aria-live": "polite", "aria-label": "\u6D88\u606F\u53D1\u9001\u4E2D", children: [_jsx(IconLoadingOutline16, { className: css.pendingMessageSpinner, size: 14 }), _jsxs("div", { className: css.pendingMessageContent, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: "\u6211" }), _jsx("time", { children: time(props.draft.startedAt) })] }), props.draft.content.kind === 'text' ? (_jsx("p", { children: props.draft.content.text })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.pendingAttachment, children: [_jsx(IconPaperclipOutline16, { size: 16 }), _jsxs("span", { children: [_jsx("strong", { children: props.draft.content.fileName }), _jsxs("small", { children: [props.draft.content.size, " \u5B57\u8282"] })] })] }), props.draft.content.caption !== undefined && _jsx("p", { className: css.pendingCaption, children: props.draft.content.caption })] }))] })] }));
|
|
264
|
+
return (_jsxs("div", { className: css.pendingMessage, role: "status", "aria-live": "polite", "aria-label": "\u6D88\u606F\u53D1\u9001\u4E2D", children: [_jsx(IconLoadingOutline16, { className: css.pendingMessageSpinner, size: 14 }), _jsxs("div", { className: css.pendingMessageContent, children: [_jsxs("div", { className: css.messageMeta, children: [_jsx("span", { children: "\u6211" }), _jsx("time", { children: time(props.draft.startedAt) })] }), props.draft.content.kind === 'text' ? (_jsx("p", { children: _jsx(MentionText, { text: props.draft.content.text, mentions: protocolMentions(props.draft.content.text, props.draft.content.mentions) }) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.pendingAttachment, children: [_jsx(IconPaperclipOutline16, { size: 16 }), _jsxs("span", { children: [_jsx("strong", { children: props.draft.content.fileName }), _jsxs("small", { children: [props.draft.content.size, " \u5B57\u8282"] })] })] }), props.draft.content.caption !== undefined && _jsx("p", { className: css.pendingCaption, children: props.draft.content.caption })] }))] })] }));
|
|
292
265
|
}
|
|
293
266
|
function summaryRangeLabel(summary) {
|
|
294
267
|
const scope = summary.range.kind === 'unread' ? '未读以来' : '最近消息';
|
|
@@ -336,11 +309,16 @@ function SummaryPanel(props) {
|
|
|
336
309
|
function Chat(props) {
|
|
337
310
|
const { view } = props;
|
|
338
311
|
const [text, setText] = useState('');
|
|
312
|
+
const [mentionDrafts, setMentionDrafts] = useState([]);
|
|
313
|
+
const [mentionQuery, setMentionQuery] = useState(null);
|
|
314
|
+
const [mentionCandidateIndex, setMentionCandidateIndex] = useState(0);
|
|
339
315
|
const [file, setFile] = useState(null);
|
|
340
316
|
const [sendingDraft, setSendingDraft] = useState(null);
|
|
317
|
+
const [groupDetailsOpen, setGroupDetailsOpen] = useState(false);
|
|
341
318
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
342
319
|
const [fileError, setFileError] = useState(null);
|
|
343
320
|
const input = useRef(null);
|
|
321
|
+
const composer = useRef(null);
|
|
344
322
|
const history = useRef(null);
|
|
345
323
|
const previousConversationId = useRef(null);
|
|
346
324
|
const previousMessageTail = useRef(null);
|
|
@@ -351,10 +329,55 @@ function Chat(props) {
|
|
|
351
329
|
const [historyAwayFromBottom, setHistoryAwayFromBottom] = useState(false);
|
|
352
330
|
const [unseenMessageCount, setUnseenMessageCount] = useState(0);
|
|
353
331
|
const selected = view.conversations.find(value => value.id === view.selectedConversationId);
|
|
332
|
+
const candidates = mentionQuery === null || selected?.kind !== 'group'
|
|
333
|
+
? []
|
|
334
|
+
: mentionCandidates(view.groupMembers, view.identity.did, mentionQuery.query);
|
|
354
335
|
const summary = selected === undefined ? undefined : view.summaries[selected.id];
|
|
355
336
|
const summaryPanelId = useId();
|
|
337
|
+
const mentionListId = useId();
|
|
356
338
|
selectedConversationId.current = view.selectedConversationId;
|
|
357
|
-
const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId
|
|
339
|
+
const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId
|
|
340
|
+
&& !view.messages.some(message => message.id === sendingDraft.messageId)
|
|
341
|
+
? sendingDraft
|
|
342
|
+
: null;
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
setMentionDrafts([]);
|
|
345
|
+
setMentionQuery(null);
|
|
346
|
+
setMentionCandidateIndex(0);
|
|
347
|
+
setGroupDetailsOpen(false);
|
|
348
|
+
}, [view.selectedConversationId]);
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (mentionCandidateIndex >= candidates.length)
|
|
351
|
+
setMentionCandidateIndex(0);
|
|
352
|
+
}, [candidates.length, mentionCandidateIndex]);
|
|
353
|
+
const syncMentionQuery = (value, selectionStart) => {
|
|
354
|
+
if (selected?.kind !== 'group' || selectionStart === null) {
|
|
355
|
+
setMentionQuery(null);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setMentionQuery(activeMentionQuery(value, utf16IndexToCodePointIndex(value, selectionStart)));
|
|
359
|
+
setMentionCandidateIndex(0);
|
|
360
|
+
};
|
|
361
|
+
const chooseMention = (index) => {
|
|
362
|
+
const query = mentionQuery;
|
|
363
|
+
const candidate = candidates[index];
|
|
364
|
+
if (query === null || candidate === undefined)
|
|
365
|
+
return;
|
|
366
|
+
const insertion = insertMention(text, query, candidate, `mention-${crypto.randomUUID()}`);
|
|
367
|
+
const retained = transformMentionDrafts(text, insertion.text, mentionDrafts);
|
|
368
|
+
setText(insertion.text);
|
|
369
|
+
setMentionDrafts([...retained, insertion.mention]);
|
|
370
|
+
setMentionQuery(null);
|
|
371
|
+
setMentionCandidateIndex(0);
|
|
372
|
+
requestAnimationFrame(() => {
|
|
373
|
+
const textarea = composer.current;
|
|
374
|
+
if (textarea === null)
|
|
375
|
+
return;
|
|
376
|
+
const caret = codePointIndexToUtf16Index(insertion.text, insertion.caret);
|
|
377
|
+
textarea.focus();
|
|
378
|
+
textarea.setSelectionRange(caret, caret);
|
|
379
|
+
});
|
|
380
|
+
};
|
|
358
381
|
const markSelectedConversationReadAtBottom = () => {
|
|
359
382
|
const node = history.current;
|
|
360
383
|
const newestRendered = view.messages.at(-1);
|
|
@@ -362,7 +385,7 @@ function Chat(props) {
|
|
|
362
385
|
|| selected === undefined
|
|
363
386
|
|| newestRendered === undefined
|
|
364
387
|
|| (selected.unreadCount ?? 0) <= 0
|
|
365
|
-
|| view.
|
|
388
|
+
|| view.localPending
|
|
366
389
|
|| (selected.lastMessageAt !== undefined && newestRendered.sentAt < selected.lastMessageAt)
|
|
367
390
|
|| node.scrollHeight - node.scrollTop - node.clientHeight > HISTORY_BOTTOM_THRESHOLD)
|
|
368
391
|
return;
|
|
@@ -400,7 +423,7 @@ function Chat(props) {
|
|
|
400
423
|
};
|
|
401
424
|
useLayoutEffect(() => {
|
|
402
425
|
markSelectedConversationReadAtBottom();
|
|
403
|
-
}, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.
|
|
426
|
+
}, [selected?.id, selected?.lastMessageAt, selected?.unreadCount, view.localPending, view.messages]);
|
|
404
427
|
useLayoutEffect(() => {
|
|
405
428
|
const conversationId = view.selectedConversationId;
|
|
406
429
|
if (conversationId !== previousConversationId.current) {
|
|
@@ -416,7 +439,7 @@ function Chat(props) {
|
|
|
416
439
|
}
|
|
417
440
|
if (conversationId === null || history.current === null)
|
|
418
441
|
return;
|
|
419
|
-
if (view.
|
|
442
|
+
if (view.localPending)
|
|
420
443
|
return;
|
|
421
444
|
if (conversationAwaitingBottom.current === conversationId) {
|
|
422
445
|
if (view.messages.length === 0)
|
|
@@ -447,7 +470,7 @@ function Chat(props) {
|
|
|
447
470
|
setHistoryAwayFromBottom(true);
|
|
448
471
|
setUnseenMessageCount(current => current + appendedMessageCount);
|
|
449
472
|
}
|
|
450
|
-
}, [view.
|
|
473
|
+
}, [view.localPending, view.messages, view.selectedConversationId]);
|
|
451
474
|
useLayoutEffect(() => {
|
|
452
475
|
if (visibleSendingDraft === null || history.current === null)
|
|
453
476
|
return;
|
|
@@ -498,18 +521,24 @@ function Chat(props) {
|
|
|
498
521
|
const sendMessage = async () => {
|
|
499
522
|
if (sendingDraft !== null || view.selectedConversationId === null)
|
|
500
523
|
return;
|
|
501
|
-
const draft = text
|
|
524
|
+
const draft = text;
|
|
502
525
|
const conversationId = view.selectedConversationId;
|
|
503
526
|
if (file === null) {
|
|
504
527
|
/* v8 ignore next -- the only invocation control is disabled while both text and attachment are empty. */
|
|
505
|
-
if (draft === '')
|
|
528
|
+
if (draft.trim() === '')
|
|
506
529
|
return;
|
|
507
|
-
|
|
530
|
+
const mentions = selected?.kind === 'group' ? protocolMentions(draft, mentionDrafts) : [];
|
|
531
|
+
const messageId = `msg-${crypto.randomUUID()}`;
|
|
532
|
+
setSendingDraft({ conversationId, messageId, startedAt: Date.now(), content: { kind: 'text', text: draft, mentions: mentionDrafts } });
|
|
508
533
|
setText('');
|
|
509
|
-
|
|
534
|
+
setMentionDrafts([]);
|
|
535
|
+
setMentionQuery(null);
|
|
536
|
+
const result = await props.sendText(draft, messageId, mentions);
|
|
510
537
|
setSendingDraft(null);
|
|
511
|
-
if (!result.ok && selectedConversationId.current === conversationId)
|
|
538
|
+
if (!result.ok && selectedConversationId.current === conversationId) {
|
|
512
539
|
setText(draft);
|
|
540
|
+
setMentionDrafts(mentionDrafts);
|
|
541
|
+
}
|
|
513
542
|
return;
|
|
514
543
|
}
|
|
515
544
|
if (file.size > view.attachmentMaxBytes) {
|
|
@@ -519,31 +548,45 @@ function Chat(props) {
|
|
|
519
548
|
setFileError(null);
|
|
520
549
|
const selectedFile = file;
|
|
521
550
|
const bytesBase64 = await fileToBase64(selectedFile);
|
|
551
|
+
const messageId = `msg-${crypto.randomUUID()}`;
|
|
522
552
|
setSendingDraft({
|
|
523
553
|
conversationId,
|
|
554
|
+
messageId,
|
|
524
555
|
startedAt: Date.now(),
|
|
525
556
|
content: {
|
|
526
557
|
kind: 'attachment',
|
|
527
558
|
fileName: selectedFile.name,
|
|
528
559
|
size: selectedFile.size,
|
|
529
|
-
...(draft === '' ? {} : { caption: draft }),
|
|
560
|
+
...(draft.trim() === '' ? {} : { caption: draft.trim() }),
|
|
530
561
|
},
|
|
531
562
|
});
|
|
532
563
|
clearFile();
|
|
533
564
|
setText('');
|
|
565
|
+
setMentionDrafts([]);
|
|
566
|
+
setMentionQuery(null);
|
|
534
567
|
const result = await props.sendAttachment({
|
|
535
568
|
fileName: selectedFile.name,
|
|
536
569
|
mimeType: selectedFile.type || 'application/octet-stream',
|
|
537
570
|
bytesBase64,
|
|
538
|
-
...(draft === '' ? {} : { caption: draft }),
|
|
571
|
+
...(draft.trim() === '' ? {} : { caption: draft.trim() }),
|
|
572
|
+
clientMessageId: messageId,
|
|
539
573
|
});
|
|
540
574
|
setSendingDraft(null);
|
|
541
575
|
if (!result.ok && selectedConversationId.current === conversationId) {
|
|
542
576
|
setFile(selectedFile);
|
|
543
577
|
setText(draft);
|
|
578
|
+
setMentionDrafts(mentionDrafts);
|
|
544
579
|
}
|
|
545
580
|
};
|
|
546
|
-
return (_jsxs("div", { className: css.chat, children: [_jsxs("aside", { className: css.roster, "data-hidden": selected !== undefined || undefined,
|
|
581
|
+
return (_jsxs("div", { className: css.chat, children: [_jsxs("aside", { className: css.roster, "data-hidden": selected !== undefined || undefined, "aria-label": "\u4F1A\u8BDD", children: [_jsx(AwikiProfileCard, { identity: view.identity, profile: view.profile, pending: view.pending !== null, updateProfile: props.updateProfile }), props.modeTabs, _jsxs("div", { className: css.rosterHeader, children: [_jsx("div", { className: css.rosterTitle, children: "\u4F1A\u8BDD" }), props.composeMenu] }), view.groupRecovery !== null && (_jsxs("div", { className: css.groupRecoveryNotice, "data-status": view.groupRecovery.status, role: view.groupRecovery.status === 'blocked' ? 'alert' : 'status', children: [_jsxs("span", { children: [_jsx("strong", { children: view.groupRecovery.status === 'pending'
|
|
582
|
+
? '正在恢复旧群聊身份'
|
|
583
|
+
: view.groupRecovery.status === 'blocked'
|
|
584
|
+
? '部分旧群聊需要处理'
|
|
585
|
+
: '暂时无法检查旧群聊身份' }), _jsx("small", { children: view.groupRecovery.status === 'pending'
|
|
586
|
+
? `${view.groupRecovery.pending} 个群聊尚未完成,其他会话不受影响。`
|
|
587
|
+
: view.groupRecovery.status === 'blocked'
|
|
588
|
+
? `${view.groupRecovery.blocked} 个群聊未能自动恢复,其他会话不受影响。`
|
|
589
|
+
: '私聊和新群聊不受影响,可稍后重试。' })] }), _jsx(Tooltip, { label: "\u91CD\u8BD5\u7FA4\u804A\u8EAB\u4EFD\u6062\u590D", side: "right", children: _jsx("button", { type: "button", "aria-label": "\u91CD\u8BD5\u7FA4\u804A\u8EAB\u4EFD\u6062\u590D", disabled: view.pending !== null, onClick: () => { void props.retryGroupRebindRecovery(); }, children: _jsx(IconRefreshOutline14, { size: 14 }) }) })] })), _jsxs("div", { className: css.conversationList, children: [view.conversations.map(conversation => (_jsx(ConversationRow, { conversation: conversation, active: conversation.id === view.selectedConversationId, onSelect: () => { void props.selectConversation(conversation.id); } }, conversation.id))), view.conversations.length === 0 && _jsx("p", { className: css.empty, children: "\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u79C1\u804A\u6216\u7FA4\u804A\u3002" })] }), view.conversationsHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadMoreConversations(); }, children: "\u52A0\u8F7D\u66F4\u591A\u4F1A\u8BDD" })] }), _jsx("section", { className: css.thread, "data-visible": selected !== undefined || undefined, children: selected === undefined ? (_jsxs("div", { className: css.threadEmpty, children: [_jsx(IconGlobeOutline14, { size: 28 }), _jsx("p", { children: "\u9009\u62E9\u4E00\u4E2A\u79C1\u804A\u6216\u7FA4\u804A\u67E5\u770B\u6D88\u606F\u3002" })] })) : (_jsxs(_Fragment, { children: [_jsxs("header", { className: css.threadHeader, children: [_jsx("button", { type: "button", className: css.back, "aria-label": "\u8FD4\u56DE\u4F1A\u8BDD\u5217\u8868", onClick: () => { void props.selectConversation(null); }, children: _jsx(IconChevronLeftOutline14, {}) }), _jsxs("div", { className: css.threadTitle, children: [_jsx("strong", { children: conversationLabel(selected) }), _jsx("small", { children: selected.kind === 'direct' ? '私聊' : '群聊' })] }), selected.kind === 'group' && (_jsx(Tooltip, { label: "\u7FA4\u804A\u8BE6\u60C5", side: "bottom", children: _jsx("button", { type: "button", className: css.threadAction, "aria-label": "\u6253\u5F00\u7FA4\u804A\u8BE6\u60C5", "aria-expanded": groupDetailsOpen, onClick: () => { setGroupDetailsOpen(value => !value); }, children: _jsx(IconSettingsOutline16, { size: 15 }) }) })), _jsxs("button", { type: "button", className: css.summaryTrigger, "aria-controls": summaryPanelId, "aria-expanded": summary === undefined ? undefined : !summary.collapsed, "aria-label": summary?.status === 'loading' ? '正在生成 AI 总结' : summary?.collapsed === true ? '展开 AI 总结' : '生成 AI 总结', disabled: summary?.status === 'loading', onClick: () => {
|
|
547
590
|
if (summary !== undefined && !summary.collapsed && summary.status !== 'error') {
|
|
548
591
|
props.setSummaryCollapsed(selected.id, true);
|
|
549
592
|
}
|
|
@@ -553,9 +596,36 @@ function Chat(props) {
|
|
|
553
596
|
else {
|
|
554
597
|
void props.summarizeConversation();
|
|
555
598
|
}
|
|
556
|
-
}, children: [summary?.status === 'loading' ? _jsx(IconLoadingOutline16, { size: 14 }) : _jsx(IconSparkle16, { size: 14 }), _jsx("span", { children: summary?.status === 'loading' ? '总结中' : 'AI 总结' }), summary !== undefined && _jsx(IconChevronDownOutline14, { size: 12 })] })] }), summary !== undefined && (_jsx(SummaryPanel, { id: summaryPanelId, summary: summary, regenerate: () => { void props.summarizeConversation(); }, collapse: collapsed => { props.setSummaryCollapsed(selected.id, collapsed); }, viewSource: viewSummarySource })), _jsxs("div", { className: css.historyShell, children: [_jsxs("div", { ref: history, className: css.history, role: "log", "aria-label": "\u6D88\u606F\u8BB0\u5F55", onScroll: syncHistoryPosition, children: [view.historyHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadOlderHistory(); }, children: "\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F" }), view.
|
|
599
|
+
}, children: [summary?.status === 'loading' ? _jsx(IconLoadingOutline16, { size: 14 }) : _jsx(IconSparkle16, { size: 14 }), _jsx("span", { children: summary?.status === 'loading' ? '总结中' : 'AI 总结' }), summary !== undefined && _jsx(IconChevronDownOutline14, { size: 12 })] })] }), selected.kind === 'group' && groupDetailsOpen && (_jsx(AwikiGroupDetails, { group: view.selectedGroup, members: view.groupMembers, hasMore: view.groupMembersHasMore, identity: view.identity, pending: view.pending !== null, refreshSelectedGroup: props.refreshSelectedGroup, loadMoreGroupMembers: props.loadMoreGroupMembers, addSelectedGroupMember: props.addSelectedGroupMember, removeSelectedGroupMember: props.removeSelectedGroupMember, leaveSelectedGroup: props.leaveSelectedGroup, onClose: () => { setGroupDetailsOpen(false); } })), summary !== undefined && (_jsx(SummaryPanel, { id: summaryPanelId, summary: summary, regenerate: () => { void props.summarizeConversation(); }, collapse: collapsed => { props.setSummaryCollapsed(selected.id, collapsed); }, viewSource: viewSummarySource })), _jsxs("div", { className: css.historyShell, children: [_jsxs("div", { ref: history, className: css.history, role: "log", "aria-label": "\u6D88\u606F\u8BB0\u5F55", onScroll: syncHistoryPosition, children: [view.historyHasMore && _jsx("button", { type: "button", className: css.more, onClick: () => { void props.loadOlderHistory(); }, children: "\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F" }), view.localPending && (_jsxs("div", { className: css.historyLoading, role: "status", "aria-live": "polite", "aria-label": "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u6D88\u606F", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsx("span", { children: "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u6D88\u606F\u2026" })] })), !view.localPending && view.refreshing && view.messages.length === 0 && (_jsxs("div", { className: css.historyLoading, role: "status", "aria-live": "polite", "aria-label": "\u6B63\u5728\u540C\u6B65\u6D88\u606F", children: [_jsx(IconLoadingOutline16, { size: 18 }), _jsx("span", { children: "\u6B63\u5728\u540C\u6B65\u6D88\u606F\u2026" })] })), view.messages.map(message => (_jsx(MessageRow, { message: message, peerLabel: selected.kind === 'direct' ? conversationLabel(selected) : undefined, download: props.downloadAttachment, onImageLoad: scrollAfterInitialImage }, message.id))), visibleSendingDraft !== null && _jsx(PendingMessageRow, { draft: visibleSendingDraft }), !view.localPending && !view.refreshing && view.messages.length === 0 && visibleSendingDraft === null && _jsx("p", { className: css.empty, children: "\u6682\u65E0\u6D88\u606F\u3002" })] }), historyAwayFromBottom && (_jsxs("button", { type: "button", className: css.latestMessages, "aria-label": unseenMessageCount === 0 ? '下滑到最新消息' : `有 ${unseenMessageCount} 条新消息,下滑到最新消息`, onClick: () => { scrollHistoryToLatest(true); }, children: [_jsx(IconChevronDownOutline14, { size: 14 }), unseenMessageCount > 0 && _jsxs("span", { children: ["\u65B0\u6D88\u606F\uFF08", unseenMessageCount, "\uFF09"] })] }))] }), _jsxs("div", { className: css.composer, children: [fileError !== null && _jsx("small", { className: css.inlineError, role: "alert", children: fileError }), _jsxs("div", { className: css.composeInput, children: [file !== null && (_jsxs("div", { className: css.filePreview, "data-image": previewUrl !== null || undefined, children: [previewUrl === null
|
|
557
600
|
? _jsx("span", { className: css.filePreviewIcon, children: _jsx(IconPaperclipOutline16, {}) })
|
|
558
|
-
: _jsx("img", { src: previewUrl, alt: file.name }), previewUrl === null && _jsx("span", { className: css.filePreviewName, children: file.name }), _jsx("button", { type: "button", className: css.removeFile, "aria-label": `移除附件 ${file.name}`, onClick: clearFile, children: _jsx(IconCloseOutline16, { size: 12 }) })] })), _jsx("
|
|
601
|
+
: _jsx("img", { src: previewUrl, alt: file.name }), previewUrl === null && _jsx("span", { className: css.filePreviewName, children: file.name }), _jsx("button", { type: "button", className: css.removeFile, "aria-label": `移除附件 ${file.name}`, onClick: clearFile, children: _jsx(IconCloseOutline16, { size: 12 }) })] })), mentionQuery !== null && candidates.length > 0 && (_jsx("div", { id: mentionListId, className: css.mentionCandidates, role: "listbox", "aria-label": "\u53EF\u63D0\u53CA\u7684\u7FA4\u6210\u5458", children: candidates.map((candidate, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === mentionCandidateIndex, onMouseDown: (event) => { event.preventDefault(); chooseMention(index); }, children: [_jsx("span", { children: candidate.label.slice(0, 1).toLocaleUpperCase() }), _jsx("strong", { children: candidate.label }), _jsx("small", { children: candidate.member.handle ?? candidate.member.did })] }, candidate.member.did))) })), _jsx("textarea", { ref: composer, value: text, "aria-controls": mentionQuery !== null && candidates.length > 0 ? mentionListId : undefined, "aria-expanded": mentionQuery !== null && candidates.length > 0, "aria-autocomplete": "list", onChange: (event) => {
|
|
602
|
+
const next = event.target.value;
|
|
603
|
+
setMentionDrafts(current => transformMentionDrafts(text, next, current));
|
|
604
|
+
setText(next);
|
|
605
|
+
syncMentionQuery(next, event.target.selectionStart);
|
|
606
|
+
}, onSelect: (event) => { syncMentionQuery(text, event.currentTarget.selectionStart); }, onKeyDown: (event) => {
|
|
607
|
+
if (mentionQuery !== null && candidates.length > 0) {
|
|
608
|
+
if (event.key === 'ArrowDown') {
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
setMentionCandidateIndex(index => (index + 1) % candidates.length);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (event.key === 'ArrowUp') {
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
setMentionCandidateIndex(index => (index - 1 + candidates.length) % candidates.length);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if ((event.key === 'Enter' || event.key === 'Tab') && !event.nativeEvent.isComposing) {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
chooseMention(mentionCandidateIndex);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (event.key === 'Escape') {
|
|
624
|
+
event.preventDefault();
|
|
625
|
+
setMentionQuery(null);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
559
629
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing)
|
|
560
630
|
return;
|
|
561
631
|
event.preventDefault();
|
|
@@ -573,17 +643,30 @@ export function AwikiOverlay(props) {
|
|
|
573
643
|
const view = props.useAwiki(state => state);
|
|
574
644
|
const titleId = useId();
|
|
575
645
|
const composeTitleId = useId();
|
|
646
|
+
const groupComposeTitleId = useId();
|
|
647
|
+
const groupJoinTitleId = useId();
|
|
576
648
|
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
|
577
649
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
578
650
|
const [composeDirect, setComposeDirect] = useState(false);
|
|
579
651
|
const [peerHandle, setPeerHandle] = useState('');
|
|
580
652
|
const [composeError, setComposeError] = useState(null);
|
|
653
|
+
const [composeGroup, setComposeGroup] = useState(false);
|
|
654
|
+
const [groupName, setGroupName] = useState('');
|
|
655
|
+
const [groupMembers, setGroupMembers] = useState('');
|
|
656
|
+
const [groupComposeError, setGroupComposeError] = useState(null);
|
|
657
|
+
const [joinGroupOpen, setJoinGroupOpen] = useState(false);
|
|
658
|
+
const [joinGroupDid, setJoinGroupDid] = useState('');
|
|
659
|
+
const [joinGroupError, setJoinGroupError] = useState(null);
|
|
581
660
|
const [logoutOpen, setLogoutOpen] = useState(false);
|
|
582
661
|
const [logoutPending, setLogoutPending] = useState(false);
|
|
583
662
|
const [logoutError, setLogoutError] = useState(null);
|
|
663
|
+
const [mode, setMode] = useState('chat');
|
|
664
|
+
const [mailUnreadCount, setMailUnreadCount] = useState(0);
|
|
584
665
|
const [launcherPosition, setLauncherPosition] = useState(readLauncherPosition);
|
|
666
|
+
const [drawerFrame, setDrawerFrame] = useState(readDrawerFrame);
|
|
585
667
|
const [launcherDragging, setLauncherDragging] = useState(false);
|
|
586
668
|
const [drawerDragging, setDrawerDragging] = useState(false);
|
|
669
|
+
const [drawerResizing, setDrawerResizing] = useState(null);
|
|
587
670
|
const [drawerDragDirection, setDrawerDragDirection] = useState(null);
|
|
588
671
|
const launcherRef = useRef(null);
|
|
589
672
|
const rememberedConversationId = useRef(null);
|
|
@@ -591,12 +674,21 @@ export function AwikiOverlay(props) {
|
|
|
591
674
|
const suppressLauncherClick = useRef(false);
|
|
592
675
|
const launcherDrag = useRef(null);
|
|
593
676
|
const drawerDrag = useRef(null);
|
|
677
|
+
const drawerResize = useRef(null);
|
|
594
678
|
const registered = view.status === 'ready' && view.identity !== null;
|
|
595
679
|
const unreadCount = view.conversations.reduce((total, conversation) => total + (conversation.unreadCount ?? 0), 0);
|
|
596
680
|
const unreadLabel = unreadCount > 99 ? '99+' : String(unreadCount);
|
|
597
|
-
const
|
|
598
|
-
const
|
|
681
|
+
const nominalDrawerWidth = Math.min(mode === 'mail' ? MAIL_DRAWER_NOMINAL_WIDTH : DRAWER_NOMINAL_WIDTH, Math.max(1, window.innerWidth - (window.innerWidth <= 620 ? DRAWER_COMPACT_HORIZONTAL_RESERVE : DRAWER_HORIZONTAL_RESERVE)));
|
|
682
|
+
const nominalDrawerHeight = Math.min(DRAWER_NOMINAL_HEIGHT, Math.max(1, window.innerHeight - DRAWER_EDGE_GAP * 2));
|
|
683
|
+
const drawerWidth = drawerFrame?.width ?? nominalDrawerWidth;
|
|
684
|
+
const drawerHeight = drawerFrame?.height ?? nominalDrawerHeight;
|
|
599
685
|
const drawerPlacement = resolveAwikiDrawerPlacement(launcherPosition, drawerWidth, drawerHeight, window.innerWidth, window.innerHeight, drawerDragDirection ?? undefined);
|
|
686
|
+
const renderedDrawerFrame = drawerFrame ?? {
|
|
687
|
+
left: drawerPlacement.left,
|
|
688
|
+
top: drawerPlacement.top,
|
|
689
|
+
width: drawerWidth,
|
|
690
|
+
height: drawerHeight,
|
|
691
|
+
};
|
|
600
692
|
useEffect(() => {
|
|
601
693
|
void props.open();
|
|
602
694
|
return props.close;
|
|
@@ -629,11 +721,21 @@ export function AwikiOverlay(props) {
|
|
|
629
721
|
setComposeDirect(false);
|
|
630
722
|
setPeerHandle('');
|
|
631
723
|
setComposeError(null);
|
|
724
|
+
setComposeGroup(false);
|
|
725
|
+
setGroupName('');
|
|
726
|
+
setGroupMembers('');
|
|
727
|
+
setGroupComposeError(null);
|
|
728
|
+
setJoinGroupOpen(false);
|
|
729
|
+
setJoinGroupDid('');
|
|
730
|
+
setJoinGroupError(null);
|
|
731
|
+
setMode('chat');
|
|
632
732
|
const drag = drawerDrag.current;
|
|
633
733
|
if (drag !== null)
|
|
634
734
|
clearTimeout(drag.timer);
|
|
635
735
|
drawerDrag.current = null;
|
|
736
|
+
drawerResize.current = null;
|
|
636
737
|
setDrawerDragging(false);
|
|
738
|
+
setDrawerResizing(null);
|
|
637
739
|
setDrawerDragDirection(null);
|
|
638
740
|
if (wasOpen && view.selectedConversationId !== null) {
|
|
639
741
|
rememberedConversationId.current = view.selectedConversationId;
|
|
@@ -659,6 +761,14 @@ export function AwikiOverlay(props) {
|
|
|
659
761
|
setComposeDirect(false);
|
|
660
762
|
return;
|
|
661
763
|
}
|
|
764
|
+
if (composeGroup) {
|
|
765
|
+
setComposeGroup(false);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (joinGroupOpen) {
|
|
769
|
+
setJoinGroupOpen(false);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
662
772
|
if (menuOpen) {
|
|
663
773
|
setMenuOpen(false);
|
|
664
774
|
return;
|
|
@@ -672,7 +782,7 @@ export function AwikiOverlay(props) {
|
|
|
672
782
|
};
|
|
673
783
|
document.addEventListener('keydown', onKeyDown);
|
|
674
784
|
return () => { document.removeEventListener('keydown', onKeyDown); };
|
|
675
|
-
}, [open, accountMenuOpen, composeDirect, logoutOpen, logoutPending, menuOpen, props.actions]);
|
|
785
|
+
}, [open, accountMenuOpen, composeDirect, composeGroup, joinGroupOpen, logoutOpen, logoutPending, menuOpen, props.actions]);
|
|
676
786
|
useEffect(() => {
|
|
677
787
|
const onResize = () => {
|
|
678
788
|
setLauncherPosition((current) => {
|
|
@@ -680,6 +790,13 @@ export function AwikiOverlay(props) {
|
|
|
680
790
|
saveLauncherPosition(next);
|
|
681
791
|
return next;
|
|
682
792
|
});
|
|
793
|
+
setDrawerFrame((current) => {
|
|
794
|
+
if (current === null)
|
|
795
|
+
return null;
|
|
796
|
+
const next = clampAwikiDrawerFrame(current, window.innerWidth, window.innerHeight);
|
|
797
|
+
saveDrawerFrame(next);
|
|
798
|
+
return next;
|
|
799
|
+
});
|
|
683
800
|
};
|
|
684
801
|
window.addEventListener('resize', onResize);
|
|
685
802
|
return () => { window.removeEventListener('resize', onResize); };
|
|
@@ -694,6 +811,8 @@ export function AwikiOverlay(props) {
|
|
|
694
811
|
origin: launcherPosition,
|
|
695
812
|
moved: false,
|
|
696
813
|
current: launcherPosition,
|
|
814
|
+
originFrame: drawerFrame,
|
|
815
|
+
currentFrame: drawerFrame,
|
|
697
816
|
};
|
|
698
817
|
callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
|
|
699
818
|
};
|
|
@@ -712,6 +831,14 @@ export function AwikiOverlay(props) {
|
|
|
712
831
|
top: drag.origin.top + deltaY,
|
|
713
832
|
}, window.innerWidth, window.innerHeight);
|
|
714
833
|
setLauncherPosition(drag.current);
|
|
834
|
+
if (drag.originFrame !== null) {
|
|
835
|
+
drag.currentFrame = clampAwikiDrawerFrame({
|
|
836
|
+
...drag.originFrame,
|
|
837
|
+
left: drag.originFrame.left + deltaX,
|
|
838
|
+
top: drag.originFrame.top + deltaY,
|
|
839
|
+
}, window.innerWidth, window.innerHeight);
|
|
840
|
+
setDrawerFrame(drag.currentFrame);
|
|
841
|
+
}
|
|
715
842
|
};
|
|
716
843
|
const finishLauncherDrag = (event) => {
|
|
717
844
|
const drag = launcherDrag.current;
|
|
@@ -721,6 +848,8 @@ export function AwikiOverlay(props) {
|
|
|
721
848
|
suppressLauncherClick.current = true;
|
|
722
849
|
saveLauncherPosition(drag.current);
|
|
723
850
|
}
|
|
851
|
+
if (drag.currentFrame !== null)
|
|
852
|
+
saveDrawerFrame(drag.currentFrame);
|
|
724
853
|
launcherDrag.current = null;
|
|
725
854
|
setLauncherDragging(false);
|
|
726
855
|
callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
|
|
@@ -738,6 +867,8 @@ export function AwikiOverlay(props) {
|
|
|
738
867
|
armed: false,
|
|
739
868
|
moved: false,
|
|
740
869
|
current: launcherPosition,
|
|
870
|
+
originFrame: drawerFrame,
|
|
871
|
+
currentFrame: drawerFrame,
|
|
741
872
|
};
|
|
742
873
|
drag.timer = setTimeout(() => {
|
|
743
874
|
if (drawerDrag.current !== drag)
|
|
@@ -770,6 +901,14 @@ export function AwikiOverlay(props) {
|
|
|
770
901
|
top: drag.origin.top + deltaY,
|
|
771
902
|
}, window.innerWidth, window.innerHeight);
|
|
772
903
|
setLauncherPosition(drag.current);
|
|
904
|
+
if (drag.originFrame !== null) {
|
|
905
|
+
drag.currentFrame = clampAwikiDrawerFrame({
|
|
906
|
+
...drag.originFrame,
|
|
907
|
+
left: drag.originFrame.left + deltaX,
|
|
908
|
+
top: drag.originFrame.top + deltaY,
|
|
909
|
+
}, window.innerWidth, window.innerHeight);
|
|
910
|
+
setDrawerFrame(drag.currentFrame);
|
|
911
|
+
}
|
|
773
912
|
};
|
|
774
913
|
const finishDrawerDrag = (event) => {
|
|
775
914
|
const drag = drawerDrag.current;
|
|
@@ -778,11 +917,76 @@ export function AwikiOverlay(props) {
|
|
|
778
917
|
clearTimeout(drag.timer);
|
|
779
918
|
if (drag.moved)
|
|
780
919
|
saveLauncherPosition(drag.current);
|
|
920
|
+
if (drag.currentFrame !== null)
|
|
921
|
+
saveDrawerFrame(drag.currentFrame);
|
|
781
922
|
drawerDrag.current = null;
|
|
782
923
|
setDrawerDragging(false);
|
|
783
924
|
setDrawerDragDirection(null);
|
|
784
925
|
callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
|
|
785
926
|
};
|
|
927
|
+
const onDrawerResizePointerDown = (event, direction) => {
|
|
928
|
+
if (event.button !== 0)
|
|
929
|
+
return;
|
|
930
|
+
event.preventDefault();
|
|
931
|
+
event.stopPropagation();
|
|
932
|
+
const origin = clampAwikiDrawerFrame(renderedDrawerFrame, window.innerWidth, window.innerHeight);
|
|
933
|
+
drawerResize.current = {
|
|
934
|
+
pointerId: event.pointerId,
|
|
935
|
+
direction,
|
|
936
|
+
startX: event.clientX,
|
|
937
|
+
startY: event.clientY,
|
|
938
|
+
origin,
|
|
939
|
+
current: origin,
|
|
940
|
+
};
|
|
941
|
+
setDrawerFrame(origin);
|
|
942
|
+
setDrawerResizing(direction);
|
|
943
|
+
callPointerCapture(event.currentTarget, 'setPointerCapture', event.pointerId);
|
|
944
|
+
};
|
|
945
|
+
const onDrawerResizePointerMove = (event) => {
|
|
946
|
+
const resize = drawerResize.current;
|
|
947
|
+
if (resize === null || resize.pointerId !== event.pointerId)
|
|
948
|
+
return;
|
|
949
|
+
event.preventDefault();
|
|
950
|
+
resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
|
|
951
|
+
setDrawerFrame(resize.current);
|
|
952
|
+
};
|
|
953
|
+
const finishDrawerResize = (event) => {
|
|
954
|
+
const resize = drawerResize.current;
|
|
955
|
+
if (resize === null || resize.pointerId !== event.pointerId)
|
|
956
|
+
return;
|
|
957
|
+
saveDrawerFrame(resize.current);
|
|
958
|
+
drawerResize.current = null;
|
|
959
|
+
setDrawerResizing(null);
|
|
960
|
+
callPointerCapture(event.currentTarget, 'releasePointerCapture', event.pointerId);
|
|
961
|
+
};
|
|
962
|
+
useEffect(() => {
|
|
963
|
+
if (drawerResizing === null)
|
|
964
|
+
return;
|
|
965
|
+
const onPointerMove = (event) => {
|
|
966
|
+
const resize = drawerResize.current;
|
|
967
|
+
if (resize === null || resize.pointerId !== event.pointerId)
|
|
968
|
+
return;
|
|
969
|
+
event.preventDefault();
|
|
970
|
+
resize.current = resizeAwikiDrawerFrame(resize.origin, resize.direction, event.clientX - resize.startX, event.clientY - resize.startY, window.innerWidth, window.innerHeight);
|
|
971
|
+
setDrawerFrame(resize.current);
|
|
972
|
+
};
|
|
973
|
+
const onPointerEnd = (event) => {
|
|
974
|
+
const resize = drawerResize.current;
|
|
975
|
+
if (resize === null || resize.pointerId !== event.pointerId)
|
|
976
|
+
return;
|
|
977
|
+
saveDrawerFrame(resize.current);
|
|
978
|
+
drawerResize.current = null;
|
|
979
|
+
setDrawerResizing(null);
|
|
980
|
+
};
|
|
981
|
+
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
|
982
|
+
window.addEventListener('pointerup', onPointerEnd);
|
|
983
|
+
window.addEventListener('pointercancel', onPointerEnd);
|
|
984
|
+
return () => {
|
|
985
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
986
|
+
window.removeEventListener('pointerup', onPointerEnd);
|
|
987
|
+
window.removeEventListener('pointercancel', onPointerEnd);
|
|
988
|
+
};
|
|
989
|
+
}, [drawerResizing]);
|
|
786
990
|
const toggleLauncher = () => {
|
|
787
991
|
if (suppressLauncherClick.current) {
|
|
788
992
|
suppressLauncherClick.current = false;
|
|
@@ -800,6 +1004,31 @@ export function AwikiOverlay(props) {
|
|
|
800
1004
|
setComposeDirect(false);
|
|
801
1005
|
setPeerHandle('');
|
|
802
1006
|
};
|
|
1007
|
+
const createGroup = async () => {
|
|
1008
|
+
setGroupComposeError(null);
|
|
1009
|
+
const members = groupMembers
|
|
1010
|
+
.split(/[\n,,]+/u)
|
|
1011
|
+
.map(member => member.trim())
|
|
1012
|
+
.filter(member => member !== '');
|
|
1013
|
+
const result = await props.createGroup(groupName, members);
|
|
1014
|
+
if (!result.ok) {
|
|
1015
|
+
setGroupComposeError(result.error);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
setComposeGroup(false);
|
|
1019
|
+
setGroupName('');
|
|
1020
|
+
setGroupMembers('');
|
|
1021
|
+
};
|
|
1022
|
+
const joinGroup = async () => {
|
|
1023
|
+
setJoinGroupError(null);
|
|
1024
|
+
const result = await props.joinGroup(joinGroupDid);
|
|
1025
|
+
if (!result.ok) {
|
|
1026
|
+
setJoinGroupError(result.error);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
setJoinGroupOpen(false);
|
|
1030
|
+
setJoinGroupDid('');
|
|
1031
|
+
};
|
|
803
1032
|
const logout = async () => {
|
|
804
1033
|
setLogoutPending(true);
|
|
805
1034
|
setLogoutError(null);
|
|
@@ -814,14 +1043,28 @@ export function AwikiOverlay(props) {
|
|
|
814
1043
|
};
|
|
815
1044
|
return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: launcherRef, type: "button", className: css.trigger, style: { left: launcherPosition.left, top: launcherPosition.top }, "data-dragging": launcherDragging || undefined, "aria-label": open
|
|
816
1045
|
? '收起 AWiki'
|
|
817
|
-
: unreadCount > 0 ? `打开 AWiki,${unreadCount} 条未读消息` : '打开 AWiki', "aria-expanded": open, "aria-haspopup": "dialog", title: "AWiki", onClick: toggleLauncher, onPointerDown: onLauncherPointerDown, onPointerMove: onLauncherPointerMove, onPointerUp: finishLauncherDrag, onPointerCancel: finishLauncherDrag, children: [_jsx("img", { className: css.launcherIcon, src: AWIKI_ME_APP_ICON_DATA_URL, alt: "", "aria-hidden": "true", draggable: "false" }), unreadCount > 0 && _jsx("span", { className: css.unreadBadge, "aria-hidden": "true", children: unreadLabel })] }), open && (_jsxs("div", { className: css.drawer, style: {
|
|
1046
|
+
: unreadCount > 0 ? `打开 AWiki,${unreadCount} 条未读消息` : '打开 AWiki', "aria-expanded": open, "aria-haspopup": "dialog", title: "AWiki", onClick: toggleLauncher, onPointerDown: onLauncherPointerDown, onPointerMove: onLauncherPointerMove, onPointerUp: finishLauncherDrag, onPointerCancel: finishLauncherDrag, children: [_jsx("img", { className: css.launcherIcon, src: AWIKI_ME_APP_ICON_DATA_URL, alt: "", "aria-hidden": "true", draggable: "false" }), unreadCount > 0 && _jsx("span", { className: css.unreadBadge, "aria-hidden": "true", children: unreadLabel })] }), open && (_jsxs("div", { className: css.drawer, style: {
|
|
1047
|
+
left: renderedDrawerFrame.left,
|
|
1048
|
+
top: renderedDrawerFrame.top,
|
|
1049
|
+
width: renderedDrawerFrame.width,
|
|
1050
|
+
height: renderedDrawerFrame.height,
|
|
1051
|
+
}, "data-placement": drawerPlacement.direction, "data-mode": mode, "data-resizing": drawerResizing ?? undefined, role: "dialog", "aria-modal": "false", "aria-labelledby": titleId, children: [_jsxs("header", { className: css.drawerHeader, "data-dragging": drawerDragging || undefined, title: "\u957F\u6309\u62D6\u52A8 AWiki", onPointerDown: onDrawerPointerDown, onPointerMove: onDrawerPointerMove, onPointerUp: finishDrawerDrag, onPointerCancel: finishDrawerDrag, children: [_jsxs("div", { children: [registered ? (_jsx(Menu, { open: accountMenuOpen, onClose: () => { setAccountMenuOpen(false); }, align: "start", portal: true, compact: true, items: [{ id: 'logout', label: '退出登录', danger: true }], onSelect: () => {
|
|
818
1052
|
setAccountMenuOpen(false);
|
|
819
1053
|
setLogoutError(null);
|
|
820
1054
|
setLogoutOpen(true);
|
|
821
|
-
}, anchor: (_jsx("button", { type: "button", "aria-label": "AWiki \u8D26\u6237\u83DC\u5355", "aria-expanded": accountMenuOpen, "aria-haspopup": "menu", onClick: () => { setAccountMenuOpen(value => !value); }, children: _jsx(IconGlobeOutline14, { size: 18 }) })) })) : _jsx(IconGlobeOutline14, { size: 18 }), _jsx("h2", { id: titleId, children: "AWiki" })] }),
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1055
|
+
}, anchor: (_jsx("button", { type: "button", "aria-label": "AWiki \u8D26\u6237\u83DC\u5355", "aria-expanded": accountMenuOpen, "aria-haspopup": "menu", onClick: () => { setAccountMenuOpen(value => !value); }, children: _jsx(IconGlobeOutline14, { size: 18 }) })) })) : _jsx(IconGlobeOutline14, { size: 18 }), _jsx("h2", { id: titleId, children: "AWiki" })] }), mode === 'chat' && (_jsx("button", { type: "button", "aria-label": "\u5237\u65B0 AWiki", disabled: view.pending !== null, onClick: () => { void props.open(); }, children: _jsx(IconRefreshOutline16, {}) })), _jsx("button", { type: "button", "aria-label": "\u5173\u95ED AWiki", onClick: props.actions.close, children: _jsx(IconCloseOutline16, {}) })] }), view.status === 'loading' && _jsx("div", { className: css.centerState, role: "status", children: "\u6B63\u5728\u8FDE\u63A5 AWiki\u2026" }), view.status === 'error' && _jsxs("div", { className: css.centerState, children: [_jsx("p", { children: view.error }), _jsx("button", { type: "button", className: css.primary, onClick: () => { void props.open(); }, children: "\u91CD\u8BD5" })] }), view.status === 'ready' && (view.sessionStatus === 'unregistered' || view.sessionStatus === 'signed-out') && (_jsx("div", { className: css.identityAccess, children: _jsx(AwikiIdentityAccess, { ...props, sessionStatus: view.sessionStatus, recoveryOperationId: view.recoveryOperationId, recoveryProgress: view.recoveryProgress, pending: view.pending !== null }) })), view.status === 'ready' && view.sessionStatus === 'active' && view.identity !== null && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.modePanel, "data-active": mode === 'chat' || undefined, hidden: mode !== 'chat', children: _jsx(Chat, { ...props, selectConversation: selectConversation, view: { ...view, identity: view.identity }, modeTabs: _jsx(ModeTabs, { mode: mode, mailUnreadCount: mailUnreadCount, onChange: setMode }), composeMenu: (_jsx(Menu, { open: menuOpen, onClose: () => { setMenuOpen(false); }, align: "end", portal: true, compact: true, items: [
|
|
1056
|
+
{ id: 'direct', label: '发起私聊' },
|
|
1057
|
+
{ id: 'group', label: '发起群聊' },
|
|
1058
|
+
{ id: 'join-group', label: '加入群聊' },
|
|
1059
|
+
], onSelect: (id) => {
|
|
1060
|
+
setMenuOpen(false);
|
|
1061
|
+
if (id === 'direct')
|
|
1062
|
+
setComposeDirect(true);
|
|
1063
|
+
if (id === 'group')
|
|
1064
|
+
setComposeGroup(true);
|
|
1065
|
+
if (id === 'join-group')
|
|
1066
|
+
setJoinGroupOpen(true);
|
|
1067
|
+
}, anchor: (_jsx("button", { type: "button", className: css.rosterAction, "aria-label": "\u53D1\u8D77\u4F1A\u8BDD", "aria-expanded": menuOpen, "aria-haspopup": "menu", onClick: () => { setMenuOpen(value => !value); }, children: _jsx(IconPlusOutline16, {}) })) })) }) }), _jsx("div", { className: css.modePanel, "data-active": mode === 'mail' || undefined, hidden: mode !== 'mail', children: _jsx(AwikiMail, { active: mode === 'mail', cacheOwner: view.identity.did, identityCard: mode === 'mail' ? _jsx(AwikiProfileCard, { identity: view.identity, profile: view.profile, pending: view.pending !== null, updateProfile: props.updateProfile }) : null, modeTabs: _jsx(ModeTabs, { mode: mode, mailUnreadCount: mailUnreadCount, onChange: setMode }), onUnreadCountChange: setMailUnreadCount, getMailAccount: props.getMailAccount, listMailInbox: props.listMailInbox, readMail: props.readMail, markMailRead: props.markMailRead, sendMail: props.sendMail }, view.identity.did) })] })), composeDirect && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": composeTitleId, onSubmit: (event) => { event.preventDefault(); void startDirect(); }, children: [_jsx("h3", { id: composeTitleId, children: "\u53D1\u8D77\u79C1\u804A" }), _jsx("p", { children: "\u8F93\u5165\u5BF9\u65B9 Handle\u3002\u6253\u5F00\u4F1A\u8BDD\u524D\u4F1A\u5148\u786E\u8BA4\u8BE5\u7528\u6237\u5B58\u5728\u3002" }), _jsxs("label", { children: ["Handle", _jsx("input", { value: peerHandle, onChange: (event) => { setPeerHandle(event.target.value); setComposeError(null); }, autoComplete: "off", placeholder: "\u4F8B\u5982 alice", autoFocus: true })] }), view.pending === '查找用户' && _jsx("p", { role: "status", children: "\u6B63\u5728\u67E5\u627E\u7528\u6237\u2026" }), composeError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: composeError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setComposeDirect(false); setComposeError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || peerHandle.trim() === '', children: "\u6253\u5F00\u4F1A\u8BDD" })] })] }) })), composeGroup && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": groupComposeTitleId, onSubmit: (event) => { event.preventDefault(); void createGroup(); }, children: [_jsx("h3", { id: groupComposeTitleId, children: "\u53D1\u8D77\u7FA4\u804A" }), _jsx("p", { children: "\u586B\u5199\u7FA4\u540D\u5373\u53EF\u521B\u5EFA\u3002\u4E5F\u53EF\u4EE5\u73B0\u5728\u9080\u8BF7\u9996\u6279\u6210\u5458\uFF0C\u652F\u6301 Handle \u6216 DID\u3002" }), _jsxs("label", { children: ["\u7FA4\u804A\u540D\u79F0", _jsx("input", { value: groupName, onChange: (event) => { setGroupName(event.target.value); setGroupComposeError(null); }, autoComplete: "off", placeholder: "\u4F8B\u5982 \u53D1\u5E03\u534F\u4F5C\u7FA4", autoFocus: true })] }), _jsxs("label", { children: ["\u9996\u6279\u7FA4\u6210\u5458\uFF08\u53EF\u9009\uFF09", _jsx("textarea", { "aria-label": "\u7FA4\u6210\u5458", value: groupMembers, onChange: (event) => { setGroupMembers(event.target.value); setGroupComposeError(null); }, rows: 4, placeholder: '例如 alice.awiki.ai\nbob.awiki.ai' })] }), view.pending === '创建群聊' && _jsx("p", { role: "status", children: "\u6B63\u5728\u521B\u5EFA\u7FA4\u804A\u5E76\u9080\u8BF7\u6210\u5458\u2026" }), groupComposeError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: groupComposeError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setComposeGroup(false); setGroupComposeError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || groupName.trim() === '', children: "\u521B\u5EFA\u7FA4\u804A" })] })] }) })), joinGroupOpen && (_jsx("div", { className: css.composeBackdrop, children: _jsxs("form", { className: css.composeCard, role: "dialog", "aria-modal": "true", "aria-labelledby": groupJoinTitleId, onSubmit: (event) => { event.preventDefault(); void joinGroup(); }, children: [_jsx("h3", { id: groupJoinTitleId, children: "\u52A0\u5165\u7FA4\u804A" }), _jsx("p", { children: "\u8F93\u5165\u7FA4\u804A\u7684\u5B8C\u6574 DID\u3002\u52A0\u5165\u6210\u529F\u540E\u4F1A\u6253\u5F00\u8BE5\u7FA4\u804A\u5E76\u8BFB\u53D6\u6700\u65B0\u6210\u5458\u5217\u8868\u3002" }), _jsxs("label", { children: ["\u7FA4 DID", _jsx("input", { value: joinGroupDid, onChange: (event) => { setJoinGroupDid(event.target.value); setJoinGroupError(null); }, autoComplete: "off", placeholder: "did:wba:...", autoFocus: true })] }), joinGroupError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: joinGroupError }), _jsxs("div", { className: css.composeActions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setJoinGroupOpen(false); setJoinGroupError(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "submit", className: css.primary, disabled: view.pending !== null || joinGroupDid.trim() === '', children: "\u52A0\u5165\u7FA4\u804A" })] })] }) })), _jsx(Modal, { open: logoutOpen, onClose: () => { if (!logoutPending)
|
|
1068
|
+
setLogoutOpen(false); }, title: "\u9000\u51FA\u767B\u5F55", closeLabel: "\u53D6\u6D88", className: css.compactModal ?? '', contentClassName: css.compactModalContent ?? '', description: "\u9000\u51FA\u540E\uFF0C\u672C\u673A\u5C06\u6682\u505C\u4F7F\u7528 AWiki\uFF1B\u8EAB\u4EFD\u548C\u672C\u5730\u6570\u636E\u90FD\u4F1A\u4FDD\u7559\u3002", footer: (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "outline", disabled: logoutPending, onClick: () => { setLogoutOpen(false); }, children: "\u53D6\u6D88" }), _jsx(Button, { type: "button", variant: "outline", className: css.logoutConfirm, disabled: logoutPending, onClick: () => { void logout(); }, children: logoutPending ? '正在退出…' : '确认退出' })] })), children: _jsxs("div", { className: css.logoutWarning, children: [_jsx("p", { children: "\u9000\u51FA\u671F\u95F4\uFF0CWeb UI \u548C Agent \u90FD\u4E0D\u80FD\u8BFB\u53D6\u4F1A\u8BDD\u6216\u4F7F\u7528\u8BE5\u8EAB\u4EFD\u53D1\u9001\u6D88\u606F\u3002" }), _jsx("p", { children: "\u7A0D\u540E\u70B9\u51FB\u201C\u91CD\u65B0\u8FDB\u5165\u672C\u673A\u8EAB\u4EFD\u201D\u5373\u53EF\u7531\u672C\u673A Rust SDK \u6062\u590D\u540C\u4E00\u4E2A DID\u3001Handle \u548C\u6D88\u606F\u6570\u636E\u5E93\u3002" }), logoutError !== null && _jsx("p", { className: css.inlineError, role: "alert", children: logoutError })] }) }), view.error !== null && view.status !== 'error' && _jsx("div", { className: css.error, role: "alert", children: view.error }), view.pending !== null && view.pending !== '发送消息' && view.pending !== '发送附件' && view.pending !== '加载消息' && _jsxs("div", { className: css.pending, role: "status", children: [view.pending, "\u2026"] }), DRAWER_RESIZE_DIRECTIONS.map(direction => (_jsx("div", { className: css.resizeHandle, "data-resize-handle": direction, "aria-hidden": "true", onPointerDown: (event) => { onDrawerResizePointerDown(event, direction); }, onPointerMove: onDrawerResizePointerMove, onPointerUp: finishDrawerResize, onPointerCancel: finishDrawerResize }, direction)))] }))] }));
|
|
826
1069
|
}
|
|
827
1070
|
//# sourceMappingURL=AwikiOverlay.js.map
|