@yiarce/dsh-chat 0.1.1 → 0.1.2
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/context-menu.js +1 -1
- package/package.json +1 -1
- package/page.js +47 -27
package/context-menu.js
CHANGED
|
@@ -87,7 +87,7 @@ export function mountChatContextMenu(root, api) {
|
|
|
87
87
|
} else if (relation === "blocked") {
|
|
88
88
|
entries.push({ label: "解除屏蔽", run: () => api.toggleBlock(entry.message) });
|
|
89
89
|
} else {
|
|
90
|
-
entries.push({ label:
|
|
90
|
+
entries.push({ label: "加为好友", disabled: !id || api.isSelf(id), run: () => api.addFriend(entry.message) });
|
|
91
91
|
entries.push({ label: "屏蔽用户", danger: true, disabled: !id || api.isSelf(id), run: () => api.toggleBlock(entry.message) });
|
|
92
92
|
}
|
|
93
93
|
entries.push({ label: "复制 ID", disabled: !id, run: () => copy(id) });
|
package/package.json
CHANGED
package/page.js
CHANGED
|
@@ -209,7 +209,7 @@ const timeStr = ms => ms ? new Date(Number(ms)).toLocaleTimeString('zh-CN', { ho
|
|
|
209
209
|
function session(kind, id, name, avatar = '') {
|
|
210
210
|
const key = kind + ':' + id;
|
|
211
211
|
if (kind === 'room' && Number(id) === 1) name = '广场';
|
|
212
|
-
if (!state.sessions.has(key)) state.sessions.set(key, { key, kind, id, name: name || (kind === 'room' ? id + ' 号房' : id), avatar, entries: [], loaded: false, loading: null, loadVersion: 0, displayFloor: 0, clearedRevision: -1, detachedEntries: new Map(), messageCount: 0, localNew: true, unread: 0, draft: '', draftSticker: null, draftVersion: 0, preview: '', updated: 0, count: null, members: null });
|
|
212
|
+
if (!state.sessions.has(key)) state.sessions.set(key, { key, kind, id, name: name || (kind === 'room' ? id + ' 号房' : id), avatar, entries: [], loaded: false, loading: null, loadVersion: 0, displayFloor: 0, clearedRevision: -1, detachedEntries: new Map(), cards: new Map(), messageCount: 0, localNew: true, unread: 0, draft: '', draftSticker: null, draftVersion: 0, preview: '', updated: 0, count: null, members: null });
|
|
213
213
|
const item = state.sessions.get(key);
|
|
214
214
|
if (name) item.name = name;
|
|
215
215
|
if (avatar) item.avatar = avatar;
|
|
@@ -238,11 +238,11 @@ function sendControl(peerId, type, fields = {}) {
|
|
|
238
238
|
return sendClientFrame({ privateChat: { toUserId: String(peerId), clientMessageId: crypto.randomUUID(), content: controlMessage(type, { ...fields, toUserId: String(peerId) }) } });
|
|
239
239
|
}
|
|
240
240
|
function addRequestCard(request) {
|
|
241
|
+
if (!request?.requestId) return;
|
|
241
242
|
const item = session('private', request.peerId, request.nickname, request.avatar);
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
if (current() === item) renderMessages(); renderSessions();
|
|
243
|
+
item.cards.set(request.requestId, { ...request });
|
|
244
|
+
if (current() === item) renderMessages();
|
|
245
|
+
renderSessions();
|
|
246
246
|
}
|
|
247
247
|
async function handleControlMessage(message, control) {
|
|
248
248
|
const mine = String(message.fromUserId) === String(state.me?.id || lastUserID);
|
|
@@ -251,12 +251,17 @@ async function handleControlMessage(message, control) {
|
|
|
251
251
|
const profile = { nickname: message.fromNickname || peerId, avatar: message.fromAvatar || '' };
|
|
252
252
|
const base = { requestId: String(control.requestId || ''), peerId, nickname: profile.nickname, avatar: profile.avatar, created: Number(control.createdAt) || Date.now() };
|
|
253
253
|
if (control.type === 'friend-request' && !mine) {
|
|
254
|
+
// The sender may retry freely; the receiver dedupes. Already a friend,
|
|
255
|
+
// an untouched pending request and an unreleased rejection all drop it.
|
|
254
256
|
if (!base.requestId || isFriend(peerId)) return;
|
|
257
|
+
const existing = requestOf(peerId, 'incoming');
|
|
258
|
+
if (existing && (existing.status === 'pending' || existing.status === 'rejected')) return;
|
|
255
259
|
const request = { ...base, direction: 'incoming', status: 'pending' }; await saveRequest(request); addRequestCard(request); return;
|
|
256
260
|
}
|
|
257
|
-
|
|
258
|
-
if (control.type === 'friend-
|
|
259
|
-
if (control.type === 'friend-
|
|
261
|
+
// Senders keep no local request record, so acceptance is applied directly.
|
|
262
|
+
if (control.type === 'friend-accept' && !mine) { await saveContact(peerId, 'friend', profile); return; }
|
|
263
|
+
if (control.type === 'friend-reject' && !mine) { log('对方拒绝了你的好友申请', 'err'); return; }
|
|
264
|
+
if (control.type === 'friend-release' && !mine) return;
|
|
260
265
|
}
|
|
261
266
|
async function handleRequestAction(request, action) {
|
|
262
267
|
if (action === 'accept') { await saveRequest({ ...request, status: 'accepted' }); await saveContact(request.peerId, 'friend', request); sendControl(request.peerId, 'friend-accept', { requestId: request.requestId }); }
|
|
@@ -1011,18 +1016,27 @@ function scrollLatest() {
|
|
|
1011
1016
|
const box = $('messageViewport'); box.scrollTop = box.scrollHeight; $('btnLatest').hidden = true;
|
|
1012
1017
|
}
|
|
1013
1018
|
function nearBottom() { const box = $('messageViewport'); return box.scrollHeight - box.scrollTop - box.clientHeight < 80; }
|
|
1019
|
+
function requestCardNode(request) {
|
|
1020
|
+
const status = request.status;
|
|
1021
|
+
const title = status === 'accepted' ? '已成为好友'
|
|
1022
|
+
: status === 'rejected' ? '申请已拒绝'
|
|
1023
|
+
: status === 'released' ? '已解除拒绝'
|
|
1024
|
+
: request.direction === 'incoming' ? '好友申请' : '好友申请中';
|
|
1025
|
+
const card = make('article', 'yc-friend-request-card');
|
|
1026
|
+
card.append(make('strong', '', title));
|
|
1027
|
+
card.append(make('p', '', request.nickname + (request.direction === 'incoming' ? ' 请求添加你为好友' : ' 的好友申请')));
|
|
1028
|
+
const actions = make('div', 'yc-friend-request-actions');
|
|
1029
|
+
const add = (label, action) => {
|
|
1030
|
+
const button = make('button', '', label); button.type = 'button';
|
|
1031
|
+
button.onclick = () => void handleRequestAction(request, action).catch(error => log(error.message || '操作失败', 'err'));
|
|
1032
|
+
actions.append(button);
|
|
1033
|
+
};
|
|
1034
|
+
if (request.direction === 'incoming' && status === 'pending') { add('同意', 'accept'); add('拒绝', 'reject'); }
|
|
1035
|
+
else if (request.direction === 'incoming' && status === 'rejected') add('解除拒绝', 'release');
|
|
1036
|
+
if (actions.childNodes.length) card.append(actions);
|
|
1037
|
+
return card;
|
|
1038
|
+
}
|
|
1014
1039
|
function messageNode(entry) {
|
|
1015
|
-
if (entry.friendRequest) {
|
|
1016
|
-
const card = make('article', 'yc-friend-request-card');
|
|
1017
|
-
card.append(make('strong', '', entry.friendRequest.direction === 'incoming' ? '好友申请' : entry.friendRequest.status === 'accepted' ? '已成为好友' : entry.friendRequest.status === 'rejected' ? '申请已拒绝' : '好友申请中'));
|
|
1018
|
-
card.append(make('p', '', entry.friendRequest.nickname + ' 请求添加你为好友'));
|
|
1019
|
-
if (entry.friendRequest.direction === 'incoming' && entry.friendRequest.status === 'pending') {
|
|
1020
|
-
const actions = make('div', 'yc-friend-request-actions');
|
|
1021
|
-
for (const [label, action] of [['同意','accept'],['拒绝','reject'],['稍后处理',null]]) { const button = make('button', '', label); button.type = 'button'; if (action) button.onclick = () => void handleRequestAction(entry.friendRequest, action).catch(error => log(error.message || '操作失败', 'err')); actions.append(button); }
|
|
1022
|
-
card.append(actions);
|
|
1023
|
-
}
|
|
1024
|
-
return card;
|
|
1025
|
-
}
|
|
1026
1040
|
if (entry.system) return make('p', 'yc-system-message', entry.text);
|
|
1027
1041
|
const m = entry.message;
|
|
1028
1042
|
const attachment = m.sticker || (entry.localImage ? { url: entry.localImage.url } : null);
|
|
@@ -1067,7 +1081,9 @@ function renderMessages(force = false) {
|
|
|
1067
1081
|
const entries = item?.entries || [];
|
|
1068
1082
|
resetMessageImages();
|
|
1069
1083
|
for (const row of $('messages').children) { const entry = contextEntries.get(row); if (entry) detachMessageRow(entry); }
|
|
1070
|
-
|
|
1084
|
+
const nodes = entries.map(messageNode);
|
|
1085
|
+
for (const request of item?.cards?.values() || []) nodes.push(requestCardNode(request));
|
|
1086
|
+
$('messages').replaceChildren(...nodes);
|
|
1071
1087
|
if (bottom) scrollLatest(); else box.scrollTop = top;
|
|
1072
1088
|
}
|
|
1073
1089
|
function selectSession(key) {
|
|
@@ -1667,7 +1683,9 @@ function applyHistoryEvent(event) {
|
|
|
1667
1683
|
if (!alive) return;
|
|
1668
1684
|
if (event.type === 'snapshot') {
|
|
1669
1685
|
contacts.clear(); for (const contact of event.contacts || []) contacts.set(String(contact.userId), contact);
|
|
1670
|
-
friendRequests.clear(); for (const request of event.requests || [])
|
|
1686
|
+
friendRequests.clear(); for (const request of event.requests || []) setRequestLocal(request);
|
|
1687
|
+
for (const item of state.sessions.values()) item.cards?.clear();
|
|
1688
|
+
for (const request of event.requests || []) if (request.status === 'pending' || request.status === 'accepted' || request.status === 'rejected') addRequestCard(request);
|
|
1671
1689
|
const known = new Set(event.sessions.map(item => item.key));
|
|
1672
1690
|
for (const item of [...state.sessions.values()]) if (!known.has(item.key) && !item.localNew) removeStoredSession(item);
|
|
1673
1691
|
for (const meta of event.sessions) {
|
|
@@ -1678,7 +1696,9 @@ function applyHistoryEvent(event) {
|
|
|
1678
1696
|
if (current() && !document.hidden && document.hasFocus()) void recordStore.read(current().key).catch(historyError);
|
|
1679
1697
|
} else if (event.type === 'contact' || event.type === 'request') {
|
|
1680
1698
|
contacts.clear(); for (const contact of event.contacts || []) contacts.set(String(contact.userId), contact);
|
|
1681
|
-
friendRequests.clear(); for (const request of event.requests || [])
|
|
1699
|
+
friendRequests.clear(); for (const request of event.requests || []) setRequestLocal(request);
|
|
1700
|
+
for (const item of state.sessions.values()) item.cards?.clear();
|
|
1701
|
+
for (const request of event.requests || []) if (request.status === 'pending' || request.status === 'accepted' || request.status === 'rejected') addRequestCard(request);
|
|
1682
1702
|
} else if (event.type === 'remove') {
|
|
1683
1703
|
const item = state.sessions.get(event.key);
|
|
1684
1704
|
if (event.revision < (sessionRevisions.get(event.key) ?? -1)) { if (item) forgetSessionMessages(item, event.revision, event.pendingIds); }
|
|
@@ -1729,10 +1749,11 @@ function discardLocalEntry(item, entry) {
|
|
|
1729
1749
|
}
|
|
1730
1750
|
async function addFriend(message) {
|
|
1731
1751
|
const id = String(message.fromUserId); if (!connected || isFriend(id) || isBlocked(id)) return;
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1752
|
+
// No local pending record: a retry is always allowed so a lost request or a
|
|
1753
|
+
// removed conversation can be recovered. The receiver dedupes.
|
|
1754
|
+
const requestId = crypto.randomUUID(), createdAt = Date.now();
|
|
1755
|
+
if (!sendControl(id, 'friend-request', { requestId, createdAt })) { log('好友申请发送失败,请重试', 'err'); return; }
|
|
1756
|
+
log('好友申请已发送');
|
|
1736
1757
|
}
|
|
1737
1758
|
async function deleteFriend(message) { await saveContact(message.fromUserId, 'none', message); log('已删除好友'); }
|
|
1738
1759
|
async function toggleBlock(message) {
|
|
@@ -1752,7 +1773,6 @@ const contextActions = mountChatContextMenu(root, {
|
|
|
1752
1773
|
},
|
|
1753
1774
|
isSelf: id => id === (state.me?.id || lastUserID),
|
|
1754
1775
|
relation: id => contactOf(id),
|
|
1755
|
-
pendingFriend: id => Boolean(requestOf(id, 'outgoing', 'pending')),
|
|
1756
1776
|
addFriend,
|
|
1757
1777
|
deleteFriend,
|
|
1758
1778
|
toggleBlock,
|