认证域:
- 找回密码验证码请求存在竞态:请求发出后如果用户改了手机号,旧请求
返回时仍会用旧手机号的challenge�covers新手机号的输入框,现在按
发起请求时的手机号快照判断,作废过期响应
- 切换企业码时"退出登录"确认框可被绕过:判断依据只有内存里的
myUserId,直接刷新在/tenant页会让这个值变回null,导致确认框不弹
就静默销毁一个其实还有效的会话;现在同时检查sessionStorage里的
token
- 企业码不存在错误文案缺后半句",请核实后重试",和其它两处文案对齐
- 营业执照上传补上前端体积/类型校验(仿照问题反馈页截图校验的写法),
不再无条件发一次注定被拒的上传请求
- 找回密码页移动端"重置密码"标题重复渲染两次,桌面版标题补上
min-[900px]:block hidden
- 企业码输入框补maxLength=64
图标CSS:Material Icons的.im-icon规则漏抄了Google原始样式表的两行
(-webkit-font-feature-settings/-webkit-font-smoothing),补齐让
"1:1 copy"真正成立
顶号踢人 vs 已有401登出的竞态:两条路径会同时清空同一个token,如果
401先触发,用户会被直接甩回登录页看不到"账号已在其他设备登录"的
提示——forceReauth()现在先等一小段时间看kickedInfo是否被顶号流程
设置,设置了就让顶号流程自己的按钮触发跳转,避免抢跑
通话信令健壮性:sendCallRinging/Accept/Hangup底层用的{note}包没有id
字段,连接断开时会同步抛异常而不是走Promise reject——之前hangUp()
在这类异常后直接中断,导致后续的leaveCall()清理和UI收尾全部被跳过。
统一加了try/catch兜底
消息核心:
- 名片消息/通话结果消息(非Drafty的扁平自定义对象)之前可以被引用/
复制/转发,会把这些对象错误地喂给Drafty的解析函数,产生格式错误
的内容——现在这两类消息只保留收藏/撤回/删除/多选
- MessagesLayout.tsx自己独立计算的peerName没有对slf(自聊天)做
isSelf特殊处理,会显示裸topic id"slf"而不是昵称,和已经修过的
ConversationView头部不一致,现在补齐同样的处理
- 发送失败图标/文案错用了danger-text(撤回删除等破坏性操作的语义
token),改成语义正确的unread token;录音条文字被连带染红,只保留
指示点是红色
- 群成员数徽标形状(应为4px圆角矩形不是胶囊)和颜色(应为text-tertiary-4
不是text-tertiary-3)都和设计稿不符,已修正
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
3.3 KiB
TypeScript
53 lines
3.3 KiB
TypeScript
import type { StoredMessage } from 'tinode-sdk';
|
|
|
|
import type { MessagePolicy } from '@/lib/api/bootstrap';
|
|
import { callResultDisplay, contactCardContentOf, isRecalled, isServerDeleted } from '@/lib/messages/dispatch';
|
|
|
|
/** SDK-internal placeholder seq threshold for a not-yet-server-confirmed message (tinode.dev.js's
|
|
* own `LOCAL_SEQID`, not exported as a Tinode static -- verified against source, not guessed). */
|
|
const LOCAL_SEQID = 0xfffffff;
|
|
|
|
export type MessageAction = 'copy' | 'quote' | 'favorite' | 'forward' | 'recall' | 'delete' | 'select';
|
|
|
|
/**
|
|
* Seconds left in which `msg` can still be recalled, or `null` if it can't be recalled at all
|
|
* (not the sender, not yet server-confirmed, no window configured, or the window already lapsed).
|
|
* Split out from availableActions() so the context menu can also render the "1:42 内" countdown
|
|
* README §3.5 calls for, not just decide whether to show the action.
|
|
*/
|
|
export function recallRemainingSeconds(msg: StoredMessage, myUserId: string | null, messagePolicy: MessagePolicy | undefined): number | null {
|
|
const mine = !!myUserId && msg.from === myUserId;
|
|
const isConfirmed = typeof msg.seq === 'number' && msg.seq < LOCAL_SEQID;
|
|
const recallWindow = messagePolicy?.recall_window_seconds ?? 0;
|
|
if (!mine || !isConfirmed || recallWindow <= 0 || !msg.ts) return null;
|
|
const remaining = recallWindow - Math.floor((Date.now() - msg.ts.getTime()) / 1000);
|
|
return remaining > 0 ? remaining : null;
|
|
}
|
|
|
|
/**
|
|
* Which context-menu actions apply to one message. Scoped to P2P for now (no group-admin recall,
|
|
* no self-topic hard-delete-always -- those need group role/topic-type knowledge Phase 3 will add).
|
|
* 翻译 is deliberately never in this list -- confirmed via research that the old app has zero
|
|
* backend call or handler for it anywhere, only a decorative id in its mock; there's nothing real
|
|
* to wire up.
|
|
*/
|
|
export function availableActions(msg: StoredMessage, myUserId: string | null, messagePolicy: MessagePolicy | undefined): MessageAction[] {
|
|
if (isServerDeleted(msg) || isRecalled(msg)) return [];
|
|
// 名片消息和通话结果消息are both flat, non-Drafty custom objects (dispatch.ts's own comments on
|
|
// contactCardContentOf/callResultDisplay), not something Drafty.toPlainText/replyContent/
|
|
// forwardedContent know how to handle -- copy/quote/forward would all feed a Drafty function an
|
|
// object it doesn't recognize, producing empty/malformed output rather than an error a user would
|
|
// notice. Favorite/delete/select don't re-parse content, so they stay safe for these two types.
|
|
const isNonDrafty = contactCardContentOf(msg) !== null || callResultDisplay(msg, !!myUserId && msg.from === myUserId) !== null;
|
|
const actions: MessageAction[] = isNonDrafty ? ['favorite'] : ['quote', 'copy', 'favorite', 'forward'];
|
|
// Defer to the server's own supported_actions list when it's present, so a server-side policy
|
|
// change (recall disabled tenant-wide, say) doesn't leave the client offering an action that
|
|
// will just 400 when tapped -- the time-window check below still applies on top of this.
|
|
const serverAllowsRecall = !messagePolicy?.supported_actions || messagePolicy.supported_actions.includes('recall');
|
|
if (serverAllowsRecall && recallRemainingSeconds(msg, myUserId, messagePolicy) !== null) {
|
|
actions.push('recall');
|
|
}
|
|
actions.push('delete', 'select');
|
|
return actions;
|
|
}
|