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
+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 };