This commit is contained in:
oneao committed 2026-07-11 17:30:25 +08:00
commit 4677f6c519
2 files changed
+79 -36

No files matched your search

@@ -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({
<>
<PressableFeedback
delayLongPress={260}
onLongPress={() => canShowActions && setShowActions(true)}
onLongPress={(e: any) => {
if (!canShowActions) return;
const { pageX, pageY } = e.nativeEvent ?? {};
if (pageX != null) setMenuPos({ x: pageX, y: pageY });
setShowActions(true);
}}
>
<View className={cn("px-3 py-1.5", isSelf ? "items-end" : "items-start")}>
{!isSelf && (
@@ -345,36 +354,38 @@ const ChatMessageItem = memo(function ChatMessageItem({
<PressableFeedback.Ripple />
</PressableFeedback>
{showActions && (
<Menu presentation="bottom-sheet" isOpen={showActions} onOpenChange={setShowActions}>
<Menu.Portal>
<Menu.Overlay />
<Menu.Content presentation="bottom-sheet">
{canRecall && (
<Menu.Item
onPress={() => {
{/* 长按操作菜单 */}
<ContextMenu
visible={showActions}
position={menuPos}
onClose={() => setShowActions(false)}
items={[
...(canRecall
? [
{
icon: <RotateCcwIcon size={18} color="#374151" />,
label: "撤回",
onPress: () => {
onRecall(item.id);
setShowActions(false);
}}
>
<Menu.ItemTitle>撤回</Menu.ItemTitle>
</Menu.Item>
)}
{isAdmin && (
<Menu.Item
variant="danger"
onPress={() => {
},
},
]
: []),
...(isAdmin
? [
{
icon: <Trash2Icon size={18} color="#EF4444" />,
label: "删除",
onPress: () => {
onDelete(item.id);
setShowActions(false);
}}
>
<Menu.ItemTitle>删除</Menu.ItemTitle>
</Menu.Item>
)}
</Menu.Content>
</Menu.Portal>
</Menu>
)}
},
},
]
: []),
]}
/>
</>
);
});
@@ -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 ? (
<View style={{ transform: [{ scaleY: -1 }] }} className="py-3 items-center">
+25 -5
View File
@@ -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 (
<Modal visible={visible} transparent animationType="none" onRequestClose={onClose}>
<Pressable className="flex-1" onPress={onClose}>
<View className="absolute bg-white rounded-xl overflow-hidden shadow-lg" style={{ left, top, width: menuW }}>
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<View
style={styles.backdrop}
onStartShouldSetResponder={(e) => {
const { pageX, pageY } = e.nativeEvent;
// 只在点击菜单外部区域时接管响应
return pageX < left || pageX > left + menuW || pageY < top || pageY > top + menuH;
}}
onResponderRelease={onClose}
>
{/* 视觉遮罩 */}
<View style={StyleSheet.absoluteFillObject} className="bg-black/30" pointerEvents="none" />
{/* 菜单 */}
<View
className="absolute bg-white rounded-xl overflow-hidden shadow-lg"
style={{ left, top, width: menuW }}
>
{items.map((item, index) => (
<TouchableOpacity
key={index}
@@ -37,7 +51,13 @@ export default function ContextMenu({ visible, position, items, onClose }: Conte
</TouchableOpacity>
))}
</View>
</Pressable>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
},
});