chore: remove unused web code and dependencies
This commit is contained in:
@@ -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