@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.
Files changed (3) hide show
  1. package/context-menu.js +1 -1
  2. package/package.json +1 -1
  3. 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: api.pendingFriend(id) ? "好友申请中" : "加为好友", disabled: !id || api.isSelf(id) || api.pendingFriend(id), run: () => api.addFriend(entry.message) });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yiarce/dsh-chat",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "独立聊天插件:共享聊天页面、图片上传、表情搜索;可选桌面独立窗口。",
5
5
  "type": "module",
6
6
  "main": "./host.js",
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
- const existing = item.entries.find(entry => entry.friendRequest?.requestId === request.requestId);
243
- if (existing) existing.friendRequest = request;
244
- else addEntry(item, { id: 'request:' + request.requestId, friendRequest: request, message: { fromUserId: request.peerId, fromNickname: request.nickname, fromAvatar: request.avatar, sentAtMs: request.created }, sendStatus: 'sent' });
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
- if (control.type === 'friend-accept' && !mine) { const request = requestOf(peerId, 'outgoing'); if (request) { request.status = 'accepted'; await saveRequest(request); await saveContact(peerId, 'friend', profile); } return; }
258
- if (control.type === 'friend-reject' && !mine) { const request = requestOf(peerId, 'outgoing'); if (request) { request.status = 'rejected'; await saveRequest(request); } return; }
259
- if (control.type === 'friend-release' && !mine) { const request = requestOf(peerId, 'outgoing', 'rejected'); if (request) { request.status = 'released'; await saveRequest(request); } }
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
- $('messages').replaceChildren(...entries.map(messageNode));
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 || []) { setRequestLocal(request); if (request.status === 'pending' || request.status === 'accepted' || request.status === 'rejected') addRequestCard(request); }
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 || []) { setRequestLocal(request); if (request.status === 'pending' || request.status === 'accepted' || request.status === 'rejected') addRequestCard(request); }
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
- if (requestOf(id, 'outgoing', 'pending')) { log('好友申请已发送,正在等待对方处理'); return; }
1733
- const request = { requestId: crypto.randomUUID(), peerId: id, direction: 'outgoing', status: 'pending', nickname: message.fromNickname || id, avatar: message.fromAvatar || '', created: Date.now() };
1734
- if (!sendControl(id, 'friend-request', { requestId: request.requestId, createdAt: request.created })) { log('好友申请发送失败', 'err'); return; }
1735
- await saveRequest(request); addRequestCard(request); log('好友申请已发送');
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,