20260709212224

This commit is contained in:
oneao committed 2026-07-09 21:22:24 +08:00
1 parent d080a2a48d
commit dadbc304da
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 { Image, ImageSourcePropType, Text, TextInput, TouchableOpacity, View } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller"; import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Menu, PressableFeedback, Spinner } from "heroui-native"; import { PressableFeedback, Spinner } from "heroui-native";
import { import {
AlertCircleIcon, AlertCircleIcon,
PaperclipIcon, PaperclipIcon,
PlayIcon, PlayIcon,
RotateCcwIcon,
SearchIcon, SearchIcon,
SendHorizonalIcon, SendHorizonalIcon,
Trash2Icon,
} from "lucide-react-native"; } from "lucide-react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import ContextMenu from "@/components/ContextMenu";
import LayoutView from "@/components/layout/view"; import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus"; import EmptyState from "@/components/EmptyStatus";
import { loadDataApi, loadDataPageApi } from "@/request/api"; import { loadDataApi, loadDataPageApi } from "@/request/api";
@@ -150,6 +153,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
const member = members[item.user_id]; const member = members[item.user_id];
const nickname = item.nickname || member?.nickname || "未知"; const nickname = item.nickname || member?.nickname || "未知";
const [showActions, setShowActions] = useState(false); const [showActions, setShowActions] = useState(false);
const [menuPos, setMenuPos] = useState({ x: 0, y: 0 });
const msgTime = new Date(item.create_time).getTime(); const msgTime = new Date(item.create_time).getTime();
const within2Min = !isNaN(msgTime) && Date.now() - msgTime < 2 * 60 * 1000; const within2Min = !isNaN(msgTime) && Date.now() - msgTime < 2 * 60 * 1000;
const canRecall = isSelf && within2Min; const canRecall = isSelf && within2Min;
@@ -189,7 +193,12 @@ const ChatMessageItem = memo(function ChatMessageItem({
<> <>
<PressableFeedback <PressableFeedback
delayLongPress={260} 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")}> <View className={cn("px-3 py-1.5", isSelf ? "items-end" : "items-start")}>
{!isSelf && ( {!isSelf && (
@@ -345,36 +354,38 @@ const ChatMessageItem = memo(function ChatMessageItem({
<PressableFeedback.Ripple /> <PressableFeedback.Ripple />
</PressableFeedback> </PressableFeedback>
{showActions && ( {/* 长按操作菜单 */}
<Menu presentation="bottom-sheet" isOpen={showActions} onOpenChange={setShowActions}> <ContextMenu
<Menu.Portal> visible={showActions}
<Menu.Overlay /> position={menuPos}
<Menu.Content presentation="bottom-sheet"> onClose={() => setShowActions(false)}
{canRecall && ( items={[
<Menu.Item ...(canRecall
onPress={() => { ? [
{
icon: <RotateCcwIcon size={18} color="#374151" />,
label: "撤回",
onPress: () => {
onRecall(item.id); onRecall(item.id);
setShowActions(false); setShowActions(false);
}} },
> },
<Menu.ItemTitle>撤回</Menu.ItemTitle> ]
</Menu.Item> : []),
)} ...(isAdmin
{isAdmin && ( ? [
<Menu.Item {
variant="danger" icon: <Trash2Icon size={18} color="#EF4444" />,
onPress={() => { label: "删除",
onPress: () => {
onDelete(item.id); onDelete(item.id);
setShowActions(false); 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); 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 renderItem = useCallback(({ item }: { item: any }) => {
const pending = item._pending as PendingMsg | undefined; const pending = item._pending as PendingMsg | undefined;
if (pending) { if (pending) {
@@ -671,11 +693,11 @@ export default function ChatPage() {
isSelf={item.user_id === userId} isSelf={item.user_id === userId}
members={members} members={members}
isAdmin={adminRole === 0} isAdmin={adminRole === 0}
onRecall={ws.recall} onRecall={handleRecallLocal}
onDelete={ws.deleteMessage} onDelete={handleDeleteLocal}
/> />
); );
}, [userId, members, adminRole, ws.recall, ws.deleteMessage, handleRetry]); }, [userId, members, adminRole, handleRecallLocal, handleDeleteLocal, handleRetry]);
if (loading) { if (loading) {
return ( return (
@@ -734,6 +756,7 @@ export default function ChatPage() {
if (hasMore && !loadingMore) handleLoadMore(); if (hasMore && !loadingMore) handleLoadMore();
}} }}
onEndReachedThreshold={0.3} onEndReachedThreshold={0.3}
windowSize={5}
ListFooterComponent={ ListFooterComponent={
hasMore ? ( hasMore ? (
<View style={{ transform: [{ scaleY: -1 }] }} className="py-3 items-center"> <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 { export interface ContextMenuItem {
icon: React.ReactNode; 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)); const top = Math.max(8, Math.min(position.y - menuH + 20, sh - menuH - 8));
return ( return (
<Modal visible={visible} transparent animationType="none" onRequestClose={onClose}> <Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<Pressable className="flex-1" onPress={onClose}> <View
<View className="absolute bg-white rounded-xl overflow-hidden shadow-lg" style={{ left, top, width: menuW }}> 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) => ( {items.map((item, index) => (
<TouchableOpacity <TouchableOpacity
key={index} key={index}
@@ -37,7 +51,13 @@ export default function ContextMenu({ visible, position, items, onClose }: Conte
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
</Pressable> </View>
</Modal> </Modal>
); );
} }
const styles = StyleSheet.create({
backdrop: {
flex: 1,
},
});