diff --git a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
index 75f80a1a..f75b4a52 100644
--- a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
+++ b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
@@ -5,15 +5,18 @@ import YifudapeiIcon from "@/static/images/subapps/yifudapei.webp";
import { Image, ImageSourcePropType, Text, TextInput, TouchableOpacity, View } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { useRouter } from "expo-router";
-import { Menu, PressableFeedback, Spinner } from "heroui-native";
+import { PressableFeedback, Spinner } from "heroui-native";
import {
AlertCircleIcon,
PaperclipIcon,
PlayIcon,
+ RotateCcwIcon,
SearchIcon,
SendHorizonalIcon,
+ Trash2Icon,
} from "lucide-react-native";
import { toast } from "sonner-native";
+import ContextMenu from "@/components/ContextMenu";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
import { loadDataApi, loadDataPageApi } from "@/request/api";
@@ -150,6 +153,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
const member = members[item.user_id];
const nickname = item.nickname || member?.nickname || "未知";
const [showActions, setShowActions] = useState(false);
+ const [menuPos, setMenuPos] = useState({ x: 0, y: 0 });
const msgTime = new Date(item.create_time).getTime();
const within2Min = !isNaN(msgTime) && Date.now() - msgTime < 2 * 60 * 1000;
const canRecall = isSelf && within2Min;
@@ -189,7 +193,12 @@ const ChatMessageItem = memo(function ChatMessageItem({
<>
canShowActions && setShowActions(true)}
+ onLongPress={(e: any) => {
+ if (!canShowActions) return;
+ const { pageX, pageY } = e.nativeEvent ?? {};
+ if (pageX != null) setMenuPos({ x: pageX, y: pageY });
+ setShowActions(true);
+ }}
>
{!isSelf && (
@@ -345,36 +354,38 @@ const ChatMessageItem = memo(function ChatMessageItem({
- {showActions && (
-
- )}
+ },
+ },
+ ]
+ : []),
+ ]}
+ />
>
);
});
@@ -640,6 +651,17 @@ export default function ChatPage() {
return p ? p.localId : String(item.id);
}, []);
+ // 删除 & 撤回时同步更新本地 history,确保页面即时移除
+ const handleDeleteLocal = useCallback((id: number) => {
+ ws.deleteMessage(id);
+ setHistory((prev) => prev.filter((m) => m.id !== id));
+ }, [ws]);
+
+ const handleRecallLocal = useCallback((id: number) => {
+ ws.recall(id);
+ setHistory((prev) => prev.map((m) => (m.id === id ? { ...m, recalled: 1 } : m)));
+ }, [ws]);
+
const renderItem = useCallback(({ item }: { item: any }) => {
const pending = item._pending as PendingMsg | undefined;
if (pending) {
@@ -671,11 +693,11 @@ export default function ChatPage() {
isSelf={item.user_id === userId}
members={members}
isAdmin={adminRole === 0}
- onRecall={ws.recall}
- onDelete={ws.deleteMessage}
+ onRecall={handleRecallLocal}
+ onDelete={handleDeleteLocal}
/>
);
- }, [userId, members, adminRole, ws.recall, ws.deleteMessage, handleRetry]);
+ }, [userId, members, adminRole, handleRecallLocal, handleDeleteLocal, handleRetry]);
if (loading) {
return (
@@ -734,6 +756,7 @@ export default function ChatPage() {
if (hasMore && !loadingMore) handleLoadMore();
}}
onEndReachedThreshold={0.3}
+ windowSize={5}
ListFooterComponent={
hasMore ? (
diff --git a/code/app/app-rn/src/components/ContextMenu.tsx b/code/app/app-rn/src/components/ContextMenu.tsx
index 73ab1e6f..b21c5ce4 100644
--- a/code/app/app-rn/src/components/ContextMenu.tsx
+++ b/code/app/app-rn/src/components/ContextMenu.tsx
@@ -1,4 +1,4 @@
-import { Dimensions, Modal, Pressable, Text, TouchableOpacity, View } from "react-native";
+import { Dimensions, Modal, StyleSheet, Text, TouchableOpacity, View } from "react-native";
export interface ContextMenuItem {
icon: React.ReactNode;
@@ -22,9 +22,23 @@ export default function ContextMenu({ visible, position, items, onClose }: Conte
const top = Math.max(8, Math.min(position.y - menuH + 20, sh - menuH - 8));
return (
-
-
-
+
+ {
+ const { pageX, pageY } = e.nativeEvent;
+ // 只在点击菜单外部区域时接管响应
+ return pageX < left || pageX > left + menuW || pageY < top || pageY > top + menuH;
+ }}
+ onResponderRelease={onClose}
+ >
+ {/* 视觉遮罩 */}
+
+ {/* 菜单 */}
+
{items.map((item, index) => (
))}
-
+
);
}
+
+const styles = StyleSheet.create({
+ backdrop: {
+ flex: 1,
+ },
+});