新分支:Vite+React19+TS+Tailwind v4 重新搭建,先跑通四级响应式布局壳
不再沿用旧 im_webapp 的技术栈(webpack + class组件 + 15000行全局CSS + 手搓History路由), 按业务重新选型:Vite/React Router/Zustand预留/TanStack Query/保留tinode-sdk真实依赖 (协议客户端本身没有重写的业务理由)。新代码放在 web/ 子目录,旧代码原样保留在仓库根目录, 两者共存到重写完成。 这一步先验证布局壳本身:NavRail(消息/通讯录/收藏/文件助手/通话记录/设置)+ 会话列表 + 主区域 + 详情栏,按设计稿的四档断点(<640/640-959/960-1279/≥1280)用真实浏览器宽度逐档验证 过——两列及以下"列表/主区域二选一+返回键"、三列"详情栏抽屉化"、四列"详情栏常驻"均已用 真实点击/返回验证正确,不是只看了截图。会话列表和聊天内容目前是占位数据,业务数据接入和 Tinode连接是下一步。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4e8f45d7d8
commit
8bcd9f3a5b
@@ -0,0 +1,78 @@
|
||||
// Ported from the previous im_webapp's src/config.js. These values encode real constraints that
|
||||
// must match the backend (server/*.go) -- they are not stylistic choices, so the numbers/strings
|
||||
// themselves carry over unchanged even though this is a from-scratch rewrite.
|
||||
|
||||
/** Version-independent product name (Firebase app name, youth-mode copy, etc.). */
|
||||
export const BASE_APP_NAME = 'MeetingIM';
|
||||
|
||||
/** Name used in the Tinode client's User-Agent string. */
|
||||
export const APP_NAME = `${BASE_APP_NAME}/${__APP_VERSION__}`;
|
||||
|
||||
/**
|
||||
* Tinode API key for the WS handshake ({hi} packet) and every REST call's X-MeetingIM-APIKey
|
||||
* header. Generated with https://github.com/tinode/chat/tree/master/keygen against this
|
||||
* deployment's own API_KEY_SALT -- the fallback below only works against a backend still using
|
||||
* the upstream sample salt, same caveat as the old app.
|
||||
*/
|
||||
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 LOGGING_ENABLED = true;
|
||||
|
||||
/** 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;
|
||||
|
||||
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;
|
||||
export const MAX_TITLE_LENGTH = 60;
|
||||
export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
|
||||
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 = 3_000;
|
||||
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* `tinode-sdk` ships as a plain UMD JS bundle with no bundled TypeScript types. This is a
|
||||
* deliberately partial ambient declaration covering only the surface this app actually calls --
|
||||
* grown incrementally as later phases touch more of the SDK, not an attempt at a full port of the
|
||||
* upstream JS. Method/property names and shapes below are verified against the installed
|
||||
* `node_modules/tinode-sdk/umd/tinode.dev.js` (v0.25.3), not guessed from the upstream docs, since
|
||||
* this project's own history has repeatedly found the two disagree (e.g. this deployment's REST
|
||||
* layer uses `X-MeetingIM-APIKey`, not the upstream `X-Tinode-APIKey`).
|
||||
*/
|
||||
declare module 'tinode-sdk' {
|
||||
export interface TinodeOptions {
|
||||
appName: string;
|
||||
host: string;
|
||||
apiKey: string;
|
||||
transport?: 'ws' | 'lp' | null;
|
||||
secure?: boolean;
|
||||
persist?: boolean;
|
||||
}
|
||||
|
||||
export type AuthLevel = 'anon' | 'auth' | 'root';
|
||||
|
||||
export class AccessMode {
|
||||
mode: string;
|
||||
given: string;
|
||||
want: string;
|
||||
isOwner(): boolean;
|
||||
isAdmin(): boolean;
|
||||
isSharer(): boolean;
|
||||
isWriter(): boolean;
|
||||
isReader(): boolean;
|
||||
isMuted(): boolean;
|
||||
isDeleter(): boolean;
|
||||
isJoiner(): boolean;
|
||||
isPresencer(): boolean;
|
||||
}
|
||||
|
||||
export interface TopicDescPublic {
|
||||
fn?: string;
|
||||
name?: string;
|
||||
photo?: { data?: string; type?: string; ref?: string };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TopicDescPrivate {
|
||||
comment?: string;
|
||||
pinned?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** A row from `MeTopic.contacts()` -- a live, mutable contact/subscription snapshot. */
|
||||
export interface Contact {
|
||||
topic: string;
|
||||
user?: string;
|
||||
updated?: Date;
|
||||
touched?: Date;
|
||||
online?: boolean;
|
||||
acs?: AccessMode;
|
||||
seq?: number;
|
||||
read?: number;
|
||||
recv?: number;
|
||||
unread?: number;
|
||||
public?: TopicDescPublic | null;
|
||||
private?: TopicDescPrivate | null;
|
||||
latestMessage?: () => StoredMessage | undefined;
|
||||
}
|
||||
|
||||
export interface StoredMessage {
|
||||
seq: number;
|
||||
from?: string;
|
||||
ts?: Date;
|
||||
content?: unknown;
|
||||
head?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class Topic {
|
||||
name: string;
|
||||
subscribe(): Promise<unknown>;
|
||||
leave(unsub?: boolean): Promise<unknown>;
|
||||
getMeta(query: unknown): Promise<unknown>;
|
||||
publish(data: unknown, head?: Record<string, unknown>): Promise<unknown>;
|
||||
isSubscribed(): boolean;
|
||||
}
|
||||
|
||||
export class MeTopic extends Topic {
|
||||
contacts(callback: (contact: Contact) => void): void;
|
||||
pinnedTopicRank(topic: string): number;
|
||||
getMeta(query: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
export class Tinode {
|
||||
constructor(options: TinodeOptions, onCompletion?: () => void);
|
||||
|
||||
_connection?: { sendText: (msg: string) => unknown; __tenantPatched?: boolean };
|
||||
|
||||
setHumanLanguage(locale: string): void;
|
||||
enableLogging(enabled: boolean, verbose?: boolean): void;
|
||||
|
||||
connect(host?: string): Promise<void>;
|
||||
disconnect(): void;
|
||||
isConnected(): boolean;
|
||||
isAuthenticated(): boolean;
|
||||
getAuthenticatedUserID(): string | undefined;
|
||||
|
||||
login(scheme: string, secret: string, cred?: unknown): Promise<{ ctrl: { code: number; params?: Record<string, unknown> } }>;
|
||||
loginToken(token: string, cred?: unknown): Promise<{ ctrl: { code: number } }>;
|
||||
getAuthToken(): { token: string; expires: Date } | null;
|
||||
|
||||
getMeTopic(): MeTopic;
|
||||
getTopic(topicName: string): Topic;
|
||||
|
||||
account(userId: string, scheme: string, secret: string, login: boolean, desc?: unknown): Promise<unknown>;
|
||||
|
||||
onConnect?: () => void;
|
||||
onDisconnect?: (err?: Error) => void;
|
||||
onLogin?: (code: number, text: string) => void;
|
||||
onNetworkProbe?: () => void;
|
||||
onAutoreconnectIteration?: (seconds: number, promise?: Promise<unknown>) => void;
|
||||
}
|
||||
|
||||
export const Drafty: {
|
||||
init(text: string): unknown;
|
||||
parse(text: string): unknown;
|
||||
toPlainText(content: unknown): string;
|
||||
preview(content: unknown, length: number, cutFirst?: boolean): unknown;
|
||||
videoCall(audioOnly: boolean): unknown;
|
||||
isValid(content: unknown): boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const TheCard: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user