+
消息免打扰
updateSettings.mutate({ mute_notifications: !settingsQuery.data?.mute_notifications })}
/>
-
+
加入黑名单
blockMutation.mutate(!isBlocked)} />
-
+
+
+
+
diff --git a/web/src/features/messages/EmojiPanel.tsx b/web/src/features/messages/EmojiPanel.tsx
index 0f04eab8..27829554 100644
--- a/web/src/features/messages/EmojiPanel.tsx
+++ b/web/src/features/messages/EmojiPanel.tsx
@@ -1,12 +1,29 @@
import { useState } from 'react';
-// README §3.3: "表情图位(使用系统 Emoji 或品牌表情包)" -- the design mockup itself has no
+// README §3.3: "分类 Tab(最近/表情/手势/物品),12 列网格" -- the design mockup itself has no
// specific emoji asset list, just gray placeholder tiles captioned "use system emoji or a branded
// pack." This is a plain Unicode emoji grid (no custom art), same call the old app made (per its own
// comment: no WeChat/QQ-style sticker assets available, system emoji is the fallback).
-const EMOJI = [
+const EMOJI_FACES = [
'😀', '😁', '😂', '🤣', '😊', '😍', '😘', '😜', '🤔', '😐', '😴', '😭', '😡', '🥳', '😱', '🤯', '🥰', '😇', '🙃', '😉',
- '👍', '👎', '👏', '🙏', '💪', '🤝', '✌️', '👌', '🤞', '👋', '❤️', '💔', '💯', '🔥', '✨', '🎉', '🎂', '☕', '🍺', '🌹',
+ '😆', '😅', '☺️', '😋', '😝', '🤗', '🤩', '🥺', '😢', '😤', '🤬', '🥵', '🥶', '😵', '🤢', '😷', '🤒', '🥴', '😪', '🙄',
+];
+const EMOJI_GESTURES = [
+ '👍', '👎', '👏', '🙏', '💪', '🤝', '✌️', '👌', '🤞', '👋', '👊', '✊', '🤛', '🤜', '🤟', '🤘', '👆', '👇', '👈', '👉',
+ '☝️', '✋', '🤚', '🖐️', '🖖', '🤙', '💅', '🤳', '👐', '🙌', '🤲', '🫶', '🤌', '🫰', '🫵', '👋', '💃', '🕺', '🙋', '🙆',
+];
+const EMOJI_OBJECTS = [
+ '❤️', '💔', '💯', '🔥', '✨', '🎉', '🎂', '☕', '🍺', '🌹', '🎁', '🎈', '⭐', '🌈', '☀️', '🌙', '⚡', '💧', '🎵', '🎮',
+ '📷', '📱', '💻', '📌', '📎', '🔔', '💰', '⏰', '🔑', '💡', '📖', '✏️', '🍎', '🍕', '🍔', '🍜', '🎄', '🎃', '⚽', '🏆',
+];
+
+type EmojiTab = 'recent' | 'faces' | 'gestures' | 'objects';
+
+const TABS: { key: EmojiTab; label: string; list: string[] }[] = [
+ { key: 'recent', label: '最近', list: [] },
+ { key: 'faces', label: '表情', list: EMOJI_FACES },
+ { key: 'gestures', label: '手势', list: EMOJI_GESTURES },
+ { key: 'objects', label: '物品', list: EMOJI_OBJECTS },
];
interface EmojiPanelProps {
@@ -14,7 +31,7 @@ interface EmojiPanelProps {
}
export function EmojiPanel({ onPick }: EmojiPanelProps) {
- const [tab, setTab] = useState<'recent' | 'all'>('all');
+ const [tab, setTab] = useState
('faces');
const recent = (() => {
try {
return JSON.parse(localStorage.getItem('im-recent-emoji') || '[]') as string[];
@@ -24,7 +41,7 @@ export function EmojiPanel({ onPick }: EmojiPanelProps) {
})();
const pick = (emoji: string) => {
- const next = [emoji, ...recent.filter((e) => e !== emoji)].slice(0, 30);
+ const next = [emoji, ...recent.filter((e) => e !== emoji)].slice(0, 36);
try {
localStorage.setItem('im-recent-emoji', JSON.stringify(next));
} catch {
@@ -33,36 +50,32 @@ export function EmojiPanel({ onPick }: EmojiPanelProps) {
onPick(emoji);
};
- const list = tab === 'recent' ? recent : EMOJI;
+ const list = tab === 'recent' ? recent : (TABS.find((t) => t.key === tab)?.list ?? []);
return (
-
+
-
-
+ {TABS.map((t) => (
+
+ ))}
{list.length === 0 ? (
暂无最近使用
) : (
-
+
{list.map((emoji, i) => (
diff --git a/web/src/features/messages/MessageBubble.tsx b/web/src/features/messages/MessageBubble.tsx
index 2dd93d78..b51eb61c 100644
--- a/web/src/features/messages/MessageBubble.tsx
+++ b/web/src/features/messages/MessageBubble.tsx
@@ -17,11 +17,26 @@ interface MessageRowProps {
myUserId: string | null;
selectMode?: boolean;
selected?: boolean;
+ /** README §3.5: "选中气泡加 outline:2px solid #1677ff" -- true while this message is the open
+ * context menu's target. */
+ menuActive?: boolean;
onToggleSelect?: (seq: number) => void;
onContextMenu?: (e: React.MouseEvent, msg: StoredMessage) => void;
}
-export function MessageRow({ topic, msg, mine, isGroup, peerName, myUserId, selectMode, selected, onToggleSelect, onContextMenu }: MessageRowProps) {
+export function MessageRow({
+ topic,
+ msg,
+ mine,
+ isGroup,
+ peerName,
+ myUserId,
+ selectMode,
+ selected,
+ menuActive,
+ onToggleSelect,
+ onContextMenu,
+}: MessageRowProps) {
if (isServerDeleted(msg)) {
return
此消息已被删除
;
}
@@ -77,7 +92,7 @@ export function MessageRow({ topic, msg, mine, isGroup, peerName, myUserId, sele
{isRecalled(msg) ? '⚠ 消息已删除' : renderDrafty(msg.content)}
diff --git a/web/src/lib/api/bootstrap.ts b/web/src/lib/api/bootstrap.ts
index fd756b65..20e3bc07 100644
--- a/web/src/lib/api/bootstrap.ts
+++ b/web/src/lib/api/bootstrap.ts
@@ -13,10 +13,34 @@ export interface RealnameStatus {
status: 'unverified' | 'pending' | 'verified' | string;
}
+/** 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 {
+ enabled: boolean;
+ route: string;
+ topic?: string;
+ unread_count: number;
+}
+
export interface BootstrapConfig {
message_policy?: MessagePolicy;
limits?: { max_forward_targets?: number; [key: string]: unknown };
security?: { realname?: RealnameStatus };
+ entries?: {
+ new_friends?: BootstrapEntry;
+ groups?: BootstrapEntry;
+ file_assistant?: BootstrapEntry;
+ system_messages?: BootstrapEntry;
+ };
+ entry_badges?: {
+ new_friends_pending_count?: number;
+ groups_unread_count?: number;
+ file_assistant_unread_count?: number;
+ system_unread_count?: number;
+ workbench_total_unread?: number;
+ };
+ system_topic?: { topic: string; type: string };
[key: string]: unknown;
}
diff --git a/web/src/lib/api/friends.ts b/web/src/lib/api/friends.ts
index 1bc8852f..78ff2a8d 100644
--- a/web/src/lib/api/friends.ts
+++ b/web/src/lib/api/friends.ts
@@ -72,6 +72,25 @@ export function rejectFriendRequest(requestId: string, version?: number) {
return imPost
(`/friend-requests/${encodeURIComponent(requestId)}/reject`, version !== undefined ? { version } : {});
}
+export interface UserProfile {
+ user_id: string;
+ display_name: string;
+ remark?: string;
+ avatar_file_id?: string;
+ gender?: string;
+ region?: string;
+ public_account?: string;
+ signature?: string;
+ relationship: string;
+ topic?: string;
+}
+
+/** `identifier` accepts `user_id`, `username`, or `public_account` -- P2P topic names are the peer's
+ * own user_id (Tinode convention), so DetailColumn can pass `topicId` straight through. */
+export function getUserProfile(identifier: string) {
+ return imGet(`/users/${encodeURIComponent(identifier)}/profile`);
+}
+
export interface ConversationSettings {
pinned: boolean;
mute_notifications: boolean;