chore: remove unused web code and dependencies

This commit is contained in:
richjohnrambo
2026-10-14 14:10:41 +08:00
parent b7f214daf5
commit 440e29975a
23 changed files with 29 additions and 2289 deletions
-32
View File
@@ -1,32 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
+9 -148
View File
@@ -14,7 +14,6 @@
"qrcode": "^1.5.4",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-intl": "^7.1.6",
"react-router-dom": "^7.9.6",
"tinode-sdk": "^0.25.3",
"zustand": "^5.0.9"
@@ -86,78 +85,6 @@
"integrity": "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.16",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.8.tgz",
"integrity": "sha512-LWXgwI5zTMatvR8w8kCNh/priDTOF/ZssokMBHJ7ZWXFoYLVOYo0EJERD9Eajv+xsfQO1QkuAt77KWQ1OI4mOQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.4",
"intl-messageformat": "10.7.18",
"tslib": "^2.8.0"
},
"peerDependencies": {
"typescript": "^5.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1101,18 +1028,6 @@
"react": "^18 || ^19"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.0"
},
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
@@ -1143,6 +1058,7 @@
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1353,6 +1269,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -1381,12 +1298,6 @@
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1721,15 +1632,6 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -1743,18 +1645,6 @@
"node": ">= 6"
}
},
"node_modules/intl-messageformat": {
"version": "10.7.18",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
"integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.4",
"tslib": "^2.8.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -2353,37 +2243,6 @@
"react": "^19.2.8"
}
},
"node_modules/react-intl": {
"version": "7.1.14",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-7.1.14.tgz",
"integrity": "sha512-VE/0Wi/lHJlBC7APQpCzLUdIt3GB5B0GZrRW8Q+ACbkHI4j+Wwgg9J1TniN6zmLHmPH5gxXcMy+fkSPfw5p1WQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-messageformat-parser": "2.11.4",
"@formatjs/intl": "3.1.8",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "16 || 17 || 18 || 19",
"hoist-non-react-statics": "^3.3.2",
"intl-messageformat": "10.7.18",
"tslib": "^2.8.0"
},
"peerDependencies": {
"react": "16 || 17 || 18 || 19",
"typescript": "^5.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-router": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz",
@@ -2496,9 +2355,9 @@
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -2579,13 +2438,15 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
+1 -1
View File
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"test": "node --test tests/*.test.mjs tests/*.test.ts",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
@@ -16,7 +17,6 @@
"qrcode": "^1.5.4",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-intl": "^7.1.6",
"react-router-dom": "^7.9.6",
"tinode-sdk": "^0.25.3",
"zustand": "^5.0.9"
-1965
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ export interface MessagePolicy {
/** README §2.1's "固定置顶两项" -- topic names are fixed/well-known on this backend (confirmed:
* docs/IM-前后端对接文档.md:909, `file_assistant.topic:"slf"`, `system_messages.topic:"sys"`), not
* per-user values the client has to discover another way. */
export interface BootstrapEntry {
interface BootstrapEntry {
enabled: boolean;
route: string;
topic?: string;
-4
View File
@@ -64,7 +64,3 @@ export function getCallHistory(cursor = '', limit = 20) {
if (cursor) qs.set('cursor', cursor);
return imGet<{ items: CallHistoryItem[]; next_cursor?: string }>(`/calls?${qs.toString()}`);
}
export function getTopicCallHistory(topic: string, cursor = '') {
return imGet<{ items: CallHistoryItem[]; next_cursor?: string }>(`/calls/${encodeURIComponent(topic)}${cursor ? `?cursor=${cursor}` : ''}`);
}
+1 -1
View File
@@ -104,7 +104,7 @@ function isRetryAfter(details: unknown): details is { retry_after_seconds: numbe
return !!details && typeof details === 'object' && 'retry_after_seconds' in details;
}
export function translateReason(reason: string | undefined, details?: unknown): string | null {
function translateReason(reason: string | undefined, details?: unknown): string | null {
if (!reason) return null;
const entry = REASON_MESSAGES[reason];
return (typeof entry === 'function' ? entry(details) : entry) || null;
-4
View File
@@ -80,10 +80,6 @@ export function getFavorites(type: FavoriteType | 'all' = 'all', query = '', cur
return imGet<{ items: Favorite[]; next_cursor?: string }>(`/favorites?${qs.toString()}`);
}
export function getFavorite(favoriteId: string) {
return imGet<Favorite>(`/favorites/${encodeURIComponent(favoriteId)}`);
}
export function deleteFavorite(favoriteId: string) {
return imDelete<{ favorite_id: string; deleted: boolean }>(`/favorites/${encodeURIComponent(favoriteId)}`);
}
+4 -25
View File
@@ -176,8 +176,7 @@ export interface GroupMember {
acs?: { want?: string; given?: string; mode?: string };
}
/** `GET .../members` never returns `group_nickname` on any item (confirmed live by the old app) --
* only getGroupMemberProfile() does. Don't read nickname off this list. */
/** `GET .../members` never returns `group_nickname` on any item. */
export function getGroupMembers(
topic: string,
opts: { query?: string; cursor?: string; role?: GroupRole; eligibleRole?: 'admin' | 'owner'; mentionable?: boolean; muteEligible?: boolean } = {},
@@ -225,28 +224,8 @@ export function canRemoveMember(myUserId: string | null, myRole: GroupRole, memb
return true;
}
export interface GroupMemberProfile {
topic: string;
user_id: string;
display_name?: string;
group_nickname?: string;
role: GroupRole;
relationship?: string;
capabilities?: { can_send?: boolean; can_call?: boolean };
}
/** `topic` must be the `grp...` id, never a `usr...` id -- confirmed by the protocol doc's own warning.
* Do NOT call this with your own user_id: the backend computes a self/other relationship context
* for this generic path that errors out when both sides are the same uid (confirmed live 2026-09-09,
* stable 400 MALFORMED_REQUEST). Use `getMyGroupProfile()` for your own group nickname instead --
* the backend added a dedicated `members/me/profile` route that sidesteps this entirely. */
export function getGroupMemberProfile(topic: string, userId: string) {
return imGet<GroupMemberProfile>(`/groups/${encodeURIComponent(topic)}/members/${encodeURIComponent(userId)}/profile`);
}
/** Dedicated self-profile route -- unlike getGroupMemberProfile(topic, myUserId), this one actually
* works (see that function's own warning) and, as a bonus, returns the real optimistic-lock `version`
* on every read, so callers no longer need to remember the last save's version between edits. */
/** Dedicated self-profile route. It returns the real optimistic-lock `version` on every read, so
* callers do not need to remember the last save's version between edits. */
export function getMyGroupProfile(topic: string) {
return imGet<{ nickname: string; version: number; updated_at: string }>(`/groups/${encodeURIComponent(topic)}/members/me/profile`);
}
@@ -302,7 +281,7 @@ export function muteGroupMembers(topic: string, members: Array<{ user_id: string
);
}
export function getGroupMutes(topic: string, cursor = '') {
function getGroupMutes(topic: string, cursor = '') {
return imGet<{ items: GroupMute[]; next_cursor?: string }>(`/groups/${encodeURIComponent(topic)}/mutes${cursor ? `?cursor=${cursor}` : ''}`);
}
+2 -2
View File
@@ -27,7 +27,7 @@ export function recallMessage(topic: string, seq: number, version = 1, reason?:
return imPost<RecallResult>(`/topics/${encodeURIComponent(topic)}/messages/${seq}/recall`, { version, reason });
}
export interface DraftyEntityData {
interface DraftyEntityData {
ref?: string;
val?: string;
mime?: string;
@@ -38,7 +38,7 @@ export interface DraftyEntityData {
duration?: number;
}
export interface DraftyEntity {
interface DraftyEntity {
tp?: string;
data?: DraftyEntityData;
}
+1 -1
View File
@@ -13,7 +13,7 @@
// confirmed this exact prefix check before ever sending a scanned string to `resolve`).
import { imPost } from './client';
export interface QRCodeDisplayProfile {
interface QRCodeDisplayProfile {
user_id: string;
display_name: string;
public_account?: string;
+1 -1
View File
@@ -16,7 +16,7 @@ function stringToColorHash(value: string): number {
const AVATAR_COLOR_COUNT = 11;
/** 1-based index matching the --color-avatar-1..11 tokens in index.css. */
export function avatarColorIndex(id: string): number {
function avatarColorIndex(id: string): number {
return (Math.abs(stringToColorHash(id)) % AVATAR_COLOR_COUNT) + 1;
}
-4
View File
@@ -111,10 +111,6 @@ export async function switchCamera(): Promise<void> {
}
}
export function hasRemoteVideo(): boolean {
return !!remoteVideoTrack;
}
/** Tears down everything -- call on hang-up from either side, or on error. Safe to call more than
* once (every step is guarded by the ref still being set). */
export async function leaveCall() {
+1 -52
View File
@@ -16,51 +16,18 @@ export const APP_NAME = `MeetingIM/${__APP_VERSION__}`;
*/
export const API_KEY = import.meta.env.VITE_TINODE_API_KEY || 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
export const KNOWN_HOSTS = { hosted: 'web.118im.com', local: 'localhost:6060' } as const;
export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
export const DEFAULT_HOST = 'web.118im.com';
// Tinode's own verbose logging prints every WS packet to the console, including the login
// secret (base64 username:password) and full plaintext message content -- dev-only, never in a
// production build.
export const LOGGING_ENABLED = import.meta.env.DEV;
/** Minimum time between two keypress notifications, ms. */
export const KEYPRESS_DELAY = 3_000;
/** Delay before sending a read notification, ms. */
export const READ_DELAY = 1_500;
export const MIN_TAG_LENGTH = 2;
export const MAX_TAG_LENGTH = 96;
export const MAX_TAG_COUNT = 16;
export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
export const NEW_GRP_ACCESS_MODE = 'JRWPSAO';
export const CHANNEL_ACCESS_MODE = 'JR';
export const NO_ACCESS_MODE = 'N';
/** Design breakpoints, px. Four tiers per the new design handoff (was a single 640 cut before). */
export const BREAKPOINTS = { sm: 640, md: 960, lg: 1280 } as const;
export const AVATAR_SIZE = 384;
export const MAX_AVATAR_BYTES = 4096;
export const BROKEN_IMAGE_SIZE = 32;
export const NO_DIMENSIONS_VIDEO = 128;
export const MESSAGES_PAGE = 24;
/** Scroll-to-top threshold (px) that triggers loading an older page of history. */
export const FETCH_PAGE_TRIGGER = 40;
/** Poll interval (ms) for the throttled read-receipt queue. */
export const NOTIFICATION_EXEC_INTERVAL = 300;
export const MAX_INBAND_ATTACHMENT_SIZE = 262_144;
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
export const MAX_IMAGE_DIM = 1024;
export const IMAGE_PREVIEW_DIM = 64;
export const VIDEO_PREVIEW_DIM = 96;
export const IMAGE_THUMBNAIL_DIM = 36;
export const VIDEO_THUMBNAIL_WIDTH = 48;
export const MAX_ONLINE_IN_TOPIC = 4;
/** `im_be/server/im_http.go`'s `serveIMGroupCreate`: `len([]rune(name)) < 1 || > 32` rejects outright
* with a bare MALFORMED_REQUEST (no `details.field`, unlike the description-length check right next
* to it) -- was `60` here, a value that let CreateGroupModal's input accept and submit names the
@@ -69,25 +36,7 @@ export const MAX_ONLINE_IN_TOPIC = 4;
* at the real limit prevents the over-length case from ever reaching the backend at all (live-
* confirmed real bug, Zentao #26). */
export const MAX_TITLE_LENGTH = 32;
/** docs/IM-前后端对接文档.md:1445 -- "description 可省略或传空字符串,最长 200 字符". */
export const MAX_TOPIC_DESCRIPTION_LENGTH = 200;
export const MAX_PEER_TITLE_LENGTH = 24;
export const MESSAGE_PREVIEW_LENGTH = 80;
export const QUOTED_REPLY_LENGTH = 30;
export const FORWARDED_PREVIEW_LENGTH = 84;
export const EDIT_PREVIEW_LENGTH = 48;
export const MIN_DURATION = 2_000;
export const MAX_DURATION = 600_000;
export const IMMEDIATE_P2P_SUBSCRIPTION = false;
export const CLICKABLE_URL_SCHEMES = ['http', 'https', 'ftp', 'ftps'];
export const QRCODE_SIZE = 128;
export const WAKE_UP_TIMEOUT = 80_000;
export const WAKE_UP_TICK = 1_000;
export const TOAST_DURATION = 2_500;
+1 -1
View File
@@ -55,7 +55,7 @@ const LEGACY_TYPE_MAP: Record<string, string> = {
/** The effective product type of a message, read from head (`x-im-type` preferred over legacy
* `type`). Falls back to "text" -- a message without an explicit type tag is a plain text body. */
export function messageReplyType(msg: StoredMessage): string {
function messageReplyType(msg: StoredMessage): string {
const head = msg.head;
const product = head?.['x-im-type'];
if (typeof product === 'string' && product.trim()) return product.trim();
+1 -1
View File
@@ -12,7 +12,7 @@
*/
const UID_SHAPE = /^usr[A-Za-z0-9_-]+$/;
export const UNKNOWN_SENDER_LABEL = '未知用户';
const UNKNOWN_SENDER_LABEL = '未知用户';
/**
* Accepts multiple name candidates in priority order (e.g. the current member list's
+2 -26
View File
@@ -13,11 +13,7 @@ import { API_KEY, APP_NAME, DEFAULT_HOST, LOGGING_ENABLED } from '@/lib/config';
import { isTenantMismatch } from '@/lib/tinode/errors';
import { useSessionStore } from '@/stores/session';
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'authenticated';
let tinode: Tinode | null = null;
let status: ConnectionStatus = 'disconnected';
const listeners = new Set<() => void>();
// Whether this connection has EVER reached 'authenticated' (survives the 'disconnected' state a
// reconnect passes through, so onConnect below can tell "first connect, nothing to restore yet"
// apart from "was logged in, the socket just dropped and came back"). Reset in resetTinode() only.
@@ -57,13 +53,6 @@ export function onTinodeReset(callback: () => void): () => void {
return () => resetListeners.delete(callback);
}
function setStatus(next: ConnectionStatus) {
if (status === next) return;
if (next === 'authenticated') wasAuthenticated = true;
status = next;
listeners.forEach((cb) => cb());
}
/**
* Reconnect/network state driving the top banners (README §3.7). The SDK's own exponential backoff
* (base 2s, +/-30% jitter, up to 10 iterations -- internal to tinode-sdk, confirmed by reading its
@@ -231,7 +220,6 @@ function createTinode(): Tinode {
t.onConnect = () => {
const wasReconnecting = reconnectState.retryInSeconds > 0 || reconnectState.attemptInFlight;
setStatus('connected');
stopReconnectRetry();
// The SDK's own auto-backoff (and the manual "立即重试") only re-establish the WebSocket --
// confirmed by reading the SDK source that reconnecting never resends {login}. The server
@@ -263,9 +251,8 @@ function createTinode(): Tinode {
justReconnectedTimer = setTimeout(() => setReconnectState({ justReconnected: false }), 4000);
}
};
t.onDisconnect = () => setStatus('disconnected');
t.onLogin = (code: number) => {
if (code === 200) setStatus('authenticated');
if (code === 200) wasAuthenticated = true;
};
t.onAutoreconnectIteration = (timeoutMs: number, promise?: Promise<unknown>) => {
clearCountdown();
@@ -333,7 +320,6 @@ export function resetTinode() {
// first login_type of the rebuilt connection (which sends hi before any store read matters).
sessionTenantCode = null;
forcedLoginType = null;
setStatus('disconnected');
}
/**
@@ -346,11 +332,10 @@ export function resetTinode() {
*/
let connectPromise: Promise<void> | null = null;
export function connect(): Promise<void> {
function connect(): Promise<void> {
const t = getTinode();
if (t.isConnected()) return Promise.resolve();
if (connectPromise) return connectPromise;
setStatus('connecting');
connectPromise = t.connect().finally(() => {
connectPromise = null;
});
@@ -418,12 +403,3 @@ export function loginBasic(login: string, password: string): Promise<LoginCtrl>
});
return loginPromise;
}
export function getConnectionStatus(): ConnectionStatus {
return status;
}
export function subscribeConnectionStatus(callback: () => void): () => void {
listeners.add(callback);
return () => listeners.delete(callback);
}
+1 -6
View File
@@ -13,7 +13,7 @@ const CTRL_ERROR_TEXT_CN: Record<string, string> = {
'internal error': '服务器繁忙,请稍后重试',
// Confirmed live 2026-09-09: registering with a username or phone number that's already taken
// returns this exact raw SDK text ("duplicate credential (409)") -- without this entry,
// registerErrorMessage()'s fallback showed the raw English straight to a Chinese-language user.
// Keep the server reason translated instead of leaking raw English to a Chinese-language user.
'duplicate credential': '该用户名或手机号已被注册,请更换后重试',
// docs/IM-前后端对接文档.md's own worked example (`{"login":...,"secret":"其他租户 token"}` ->
// `403 tenant mismatch`) -- without this entry, loginErrorMessage's generic `code===403` fallback
@@ -73,11 +73,6 @@ export function loginErrorMessage(err: unknown): string {
return (err as Error)?.message || '登录失败,请重试';
}
export function registerErrorMessage(err: unknown): string {
const known = translateCtrlError(err);
return known || (err as Error)?.message || '注册失败,请稍后重试';
}
/**
* Only INVITE_CODE_REQUIRED/INVALID were confirmed against a live server; REVOKED/EXPIRED are
* inferred siblings under the same naming pattern (not individually exercised).
-7
View File
@@ -3,22 +3,15 @@ import { useCallback, useSyncExternalStore } from 'react';
import type { Contact, StoredMessage } from 'tinode-sdk';
import {
getConnectionStatus,
getKickedInfo,
getReconnectState,
getTinode,
subscribeConnectionStatus,
subscribeKicked,
subscribeReconnectState,
type ReconnectState,
} from './connection';
import { getChatListSnapshot, getMessagesSnapshot, subscribeAnyTopicChange, subscribeTopicChanges } from './topics';
/** The current WS connection status, reactive via useSyncExternalStore. */
export function useConnectionStatus() {
return useSyncExternalStore(subscribeConnectionStatus, getConnectionStatus);
}
/** Reconnect countdown / offline / just-reconnected state driving the top banners (README §3.7). */
export function useReconnectState(): ReconnectState {
return useSyncExternalStore(subscribeReconnectState, getReconnectState);
-4
View File
@@ -97,10 +97,6 @@ export function retryMessage(topic: Topic, seq: number): Promise<unknown> | unde
return publishWithReattach(topic, msg);
}
export function messageIsMine(msg: StoredMessage, myUserId: string | null): boolean {
return !!myUserId && msg.from === myUserId;
}
/**
* Quoted reply (cross-client `x-im-reply` contract, aligned with iOS 2026-09-23): the quote is a
* lightweight SNAPSHOT carried in `head["x-im-reply"]` (`{seq,type,text,from}`), NOT a Drafty QQ
+1 -1
View File
@@ -483,7 +483,7 @@ const inFlightPreview = new Map<string, Promise<unknown>>();
* Deliberately never left afterward (matches the old app) -- simpler than tracking teardown for a
* subscription that's cheap to leave attached and likely to be reopened soon anyway.
*/
export function prefetchPreview(topicName: string): void {
function prefetchPreview(topicName: string): void {
if (previewPrefetched.has(topicName)) return;
previewPrefetched.add(topicName);
const topic = getTinode().getTopic(topicName);
+1 -1
View File
@@ -13,7 +13,7 @@ import { getTinode } from '@/lib/tinode/connection';
import { getChatListSnapshot, getMessagesSnapshot, subscribeAnyTopicChange, subscribeTopicInfo } from '@/lib/tinode/topics';
import { useSessionStore } from '@/stores/session';
export type CallPhase = 'idle' | 'outgoing' | 'incoming' | 'connected' | 'ended';
type CallPhase = 'idle' | 'outgoing' | 'incoming' | 'connected' | 'ended';
/** Generous local ring-timeout fallback -- see startCall's own comment on why this exists. */
const RING_TIMEOUT_MS = 60_000;
+1 -1
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface TenantContext {
interface TenantContext {
code: string;
name: string;
endpoints: { api: string; file: string };