chore: remove unused web code and dependencies
This commit is contained in:
@@ -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.
|
||||
Generated
+9
-148
@@ -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
@@ -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"
|
||||
|
||||
Generated
-1965
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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}` : ''}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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}` : ''}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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 };
|
||||
|
||||
Reference in New Issue
Block a user