This commit is contained in:
oneao committed 2026-06-27 16:53:21 +08:00
commit 51622a4b1a
12 files changed
+283 -69

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# 2026-06-26
## chat/index.tsx 乱码修复
- 文件中所有中文字符(注释、placeholder、UI 文案)因编码问题出现 mojibake(乱码)
- 文件是 UTF-8 with BOM(EF BB BF),BOM 可能导致了编码识别问题
- 修复了所有 14 处乱码字符串,同时清除了文件头的 BOM
- 修复内容:在线栏、消息气泡、未知、来自、撤回、删除、页面、成员映射、加载历史、加载更多、合并消息(旧→新)、加载中...、说点什么...、暂无消息,开始聊天吧
## chat/index.tsx 输入框获焦滚动优化
- 将 `keyboardWillShow` 改为 `keyboardDidShow`,确保键盘动画完成后再滚动
- 添加 `InteractionManager.runAfterInteractions` 用于无延迟的滚动调用
- 为 FlatList 添加 `maintainVisibleContentPosition={{ minIndexForVisible: 0, autoscrollToTopThreshold: 10 }}`,让 FlatList 在 viewport 变化时(键盘弹起/收起)自动保持可见内容位置
+4 -3
View File
@@ -2,6 +2,7 @@ package ws
import (
"allapp-go/internal/httpx"
"allapp-go/internal/middleware"
"allapp-go/pkg/db"
"allapp-go/pkg/jwtx"
"allapp-go/pkg/uniqueid"
@@ -195,7 +196,7 @@ func (c *Client) readPump() {
// handleSend 处理发送消息
func (c *Client) handleSend(msg clientSendMsg) {
ctx := context.Background()
ctx := context.WithValue(context.Background(), middleware.CtxUserIDKey, c.userID)
dbClient := db.New()
msgID := uniqueid.NextId()
@@ -293,7 +294,7 @@ func (c *Client) handleSend(msg clientSendMsg) {
// handleRecall 处理撤回(发送者 2 分钟内)
func (c *Client) handleRecall(msg clientSendMsg) {
ctx := context.Background()
ctx := context.WithValue(context.Background(), middleware.CtxUserIDKey, c.userID)
dbClient := db.New()
rows, err := dbClient.LoadData(ctx, "b_chat",
@@ -337,7 +338,7 @@ func (c *Client) handleDelete(msg clientSendMsg) {
return
}
ctx := context.Background()
ctx := context.WithValue(context.Background(), middleware.CtxUserIDKey, c.userID)
dbClient := db.New()
if err := dbClient.Update(ctx, "b_chat", "id", map[string]any{
+2 -2
View File
@@ -19,7 +19,7 @@
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"backgroundColor": "#F0FDF4",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
@@ -38,7 +38,7 @@
[
"expo-splash-screen",
{
"backgroundColor": "#208AEF",
"backgroundColor": "#15803D",
"android": {
"image": "./assets/images/splash-icon.png",
"imageWidth": 76
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useRef, useState, memo } from "react";
import { Image, Text, TextInput, View, Keyboard } from "react-native";
import { useCallback, useEffect, useRef, useState, memo, useMemo } from "react";
import { Image, Text, TextInput, View, Keyboard, Animated } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { useRouter } from "expo-router";
import { Menu, PressableFeedback, Spinner } from "heroui-native";
import { SendHorizonalIcon, SearchIcon } from "lucide-react-native";
import { AlertCircleIcon, PaperclipIcon, PlayIcon, SearchIcon, SendHorizonalIcon } from "lucide-react-native";
import { toast } from "sonner-native";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
import { loadDataApi, loadDataPageApi } from "@/request/api";
@@ -27,20 +28,20 @@ function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: ()
const router = useRouter();
return (
<View className="flex-row items-center justify-between px-4 h-12 bg-white/90 rounded-2xl shadow-sm">
<View className="flex-row items-center flex-1">
{users.slice(0, 6).map((u, i) => (
<View className="flex-row items-center flex-1 gap-2">
{users.slice(0, 6).map((u) => (
<PressableFeedback
key={u.user_id}
onPress={() => router.navigate(`/family/member/${u.user_id}` as any)}
>
<View className={cn(i > 0 && "-ml-2")}>
<View>
{u.avatar ? (
<Image
source={{ uri: buildFileUrl(u.avatar) }}
className="w-8 h-8 rounded-full bg-gray-200 border-2 border-white"
className="w-8 h-8 rounded-full bg-gray-200"
/>
) : (
<View className="w-8 h-8 rounded-full bg-accent/20 items-center justify-center border-2 border-white">
<View className="w-8 h-8 rounded-full bg-accent/20 items-center justify-center">
<Text className="text-xs font-medium text-accent">{u.nickname?.charAt(0) || "?"}</Text>
</View>
)}
@@ -50,7 +51,7 @@ function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: ()
</PressableFeedback>
))}
{users.length > 6 && (
<View className="-ml-2 w-8 h-8 rounded-full bg-gray-100 items-center justify-center border-2 border-white">
<View className="w-8 h-8 rounded-full bg-gray-100 items-center justify-center">
<Text className="text-[10px] font-medium text-gray-500">+{users.length - 6}</Text>
</View>
)}
@@ -133,7 +134,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
isSelf ? "bg-accent rounded-br-md" : "bg-white rounded-bl-md shadow-sm",
)}
>
<Text className={cn("text-[15px] leading-[22px]", isSelf ? "text-white text-right" : "text-gray-900 text-left")}>
<Text className={cn("text-[15px] leading-[22px] text-left", isSelf ? "text-white" : "text-gray-900")}>
{item.content}
</Text>
</View>
@@ -155,7 +156,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
{item.msg_type === ChatMsgType.Video && item.attachments?.length && (
<View className="w-48 h-32 rounded-xl bg-gray-800/90 items-center justify-center overflow-hidden">
<View className="w-10 h-10 rounded-full bg-white/20 items-center justify-center">
<Text className="text-white text-lg ml-0.5">▶</Text>
<PlayIcon size={20} color="white" fill="white" />
</View>
{item.attachments[0].duration ? (
<Text className="text-white/80 text-[10px] absolute bottom-2 right-2">
@@ -173,16 +174,16 @@ const ChatMessageItem = memo(function ChatMessageItem({
)}
>
<View className={cn("w-9 h-9 rounded-lg items-center justify-center", isSelf ? "bg-white/20" : "bg-gray-100")}>
<Text className={cn("text-sm", isSelf ? "text-white" : "text-gray-500")}>📎</Text>
<PaperclipIcon size={17} color={isSelf ? "white" : "#6B7280"} />
</View>
<View className="flex-1">
<Text
className={cn("text-sm font-medium", isSelf ? "text-white" : "text-gray-900")}
className={cn("text-sm font-medium text-left", isSelf ? "text-white" : "text-gray-900")}
numberOfLines={1}
>
{item.attachments[0].file_name}
</Text>
<Text className={cn("text-[11px]", isSelf ? "text-white/70" : "text-gray-400")}>
<Text className={cn("text-[11px] text-left", isSelf ? "text-white/70" : "text-gray-400")}>
{item.attachments[0].file_size
? (item.attachments[0].file_size / 1024).toFixed(0) + " KB"
: ""}
@@ -195,7 +196,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
<View className="max-w-[60%] rounded-xl overflow-hidden bg-white shadow-sm border border-gray-100">
<View className="px-3 py-2">
<Text className="text-[11px] text-gray-400">来自 {item.source_table}</Text>
<Text className="text-sm text-gray-700 mt-1" numberOfLines={3}>
<Text className="text-sm text-gray-700 mt-1 text-left" numberOfLines={3}>
{(() => {
try {
const d = JSON.parse(item.source_data);
@@ -212,7 +213,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
</View>
{item.content ? (
<View className="px-3 pb-2 border-t border-gray-50">
<Text className="text-sm text-gray-900">{item.content}</Text>
<Text className="text-sm text-gray-900 text-left">{item.content}</Text>
</View>
) : null}
</View>
@@ -274,15 +275,142 @@ export default function ChatPage() {
const [inputText, setInputText] = useState("");
const [adminRole, setAdminRole] = useState(0);
const listRef = useRef<any>(null);
const opacity = useRef(new Animated.Value(0)).current;
const revealed = useRef(false);
// 键盘弹起时自动滚到底部
useEffect(() => {
const show = Keyboard.addListener("keyboardWillShow", () => {
setTimeout(() => listRef.current?.scrollToEnd?.({ animated: true }), 100);
});
return () => show.remove();
// ---- optimistic send ----
type PendingMsg = { localId: string; content: string; status: "sending" | "failed"; createTime: string };
const [pending, setPending] = useState<PendingMsg[]>([]);
const pendingTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
const removePending = useCallback((localId: string) => {
setPending((prev) => prev.filter((p) => p.localId !== localId));
const t = pendingTimers.current[localId];
if (t) { clearTimeout(t); delete pendingTimers.current[localId]; }
}, []);
// Watch ws.messages — when a new message from self appears, resolve matching pending
const resolvedRef = useRef<Set<string>>(new Set());
useEffect(() => {
for (const m of ws.messages) {
if (m.user_id !== userId) continue;
const key = `${m.user_id}:${m.content}`;
if (resolvedRef.current.has(key)) continue;
resolvedRef.current.add(key);
const match = pending.find((p) => p.content === m.content && p.status === "sending");
if (match) {
removePending(match.localId);
toast.success("发送成功", { id: match.localId });
}
}
}, [ws.messages, userId, pending, removePending]);
// ---- toast: online / offline ----
const onlineIdsRef = useRef<Set<number>>(new Set());
useEffect(() => {
const curr = new Set(ws.onlineUsers.map((u) => u.user_id));
const prev = onlineIdsRef.current;
// joined
for (const id of curr) {
if (!prev.has(id) && prev.size > 0) {
const u = ws.onlineUsers.find((x) => x.user_id === id);
if (u && u.user_id !== userId) toast(`${u.nickname} 上线了`);
}
}
// left
for (const id of prev) {
if (!curr.has(id)) {
// look up nickname from members (offline event only gives user_id)
const m = members[id];
const name = m?.nickname || `用户${id}`;
toast(`${name} 下线了`);
}
}
onlineIdsRef.current = curr;
}, [ws.onlineUsers, userId, members]);
// ---- toast: new message from others ----
const seenMsgIds = useRef<Set<number>>(new Set());
// Sync history IDs into seen set so we don't toast on initial load
useEffect(() => {
for (const m of history) seenMsgIds.current.add(m.id);
}, [history]);
// Watch ws.messages for new arrivals from others
useEffect(() => {
if (loading) return; // skip during initial load
for (const m of ws.messages) {
if (seenMsgIds.current.has(m.id)) continue;
seenMsgIds.current.add(m.id);
if (m.user_id !== userId) {
const name = m.nickname || members[m.user_id]?.nickname || "用户";
const preview = m.content.length > 20 ? m.content.slice(0, 20) + "…" : m.content;
toast(`${name}: ${preview}`);
}
}
}, [ws.messages, userId, members, loading]);
// Merge pending messages into the display list
const allMessages = useMemo(() => {
const ids = new Set(history.map((m) => m.id));
const merged: (ChatMessage & { _pending?: PendingMsg })[] = [...history];
for (const m of ws.messages) {
if (!ids.has(m.id)) { merged.push(m); ids.add(m.id); }
}
// Append pending (sending/failed) at the end
for (const p of pending) {
merged.push({
id: -(Date.now() % 100000), // negative ID for local-only items
family_id: familyId ?? 0,
user_id: userId,
nickname: "",
avatar: "",
content: p.content,
msg_type: ChatMsgType.Text,
source_table: null,
source_id: null,
source_data: null,
attachments: null,
recalled: 0,
deleted: 0,
create_time: p.createTime,
_pending: p,
} as any);
}
return merged;
}, [history, ws.messages, pending, familyId, userId]);
// Non-inverted: scrollToEnd = latest messages
const scrollToBottom = useCallback(
(animated = true) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
listRef.current?.scrollToEnd?.({ animated });
});
});
},
[],
);
const revealAtBottom = useCallback(() => {
requestAnimationFrame(() => {
listRef.current?.scrollToEnd?.({ animated: false });
requestAnimationFrame(() => {
listRef.current?.scrollToEnd?.({ animated: false });
opacity.setValue(1);
});
});
}, [opacity]);
// Keyboard appears → double-tap scroll: first to catch the new viewport,
// second as a safety net after FlatList internal measurements settle.
useEffect(() => {
const sub = Keyboard.addListener("keyboardDidShow", () => {
setTimeout(() => scrollToBottom(true), 200);
setTimeout(() => scrollToBottom(true), 400);
});
return () => sub.remove();
}, [scrollToBottom]);
// 成员映射
useEffect(() => {
if (!familyId) return;
@@ -364,27 +492,54 @@ export default function ChatPage() {
});
}, [loadingMore, hasMore, page, loadHistory, runLoadMore]);
// 合并消息 (旧→新)
const allMessages = (() => {
const ids = new Set(history.map((m) => m.id));
const merged = [...history];
for (const m of ws.messages) {
if (!ids.has(m.id)) {
merged.push(m);
ids.add(m.id);
}
}
return merged;
})();
// 发送
const handleSend = useCallback(() => {
// Send the current message draft (optimistic).
function handleSend() {
const text = inputText.trim();
if (!text) return;
setInputText("");
Keyboard.dismiss();
const localId = `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const now = new Date().toISOString();
// Show immediately in the list
setPending((prev) => [...prev, { localId, content: text, status: "sending", createTime: now }]);
toast.loading("正在发送", { id: localId });
// Send via WebSocket
ws.send({ type: "send", content: text });
}, [inputText, ws]);
// 10-second timeout → mark as failed
pendingTimers.current[localId] = setTimeout(() => {
setPending((prev) => prev.map((p) => (p.localId === localId ? { ...p, status: "failed" } : p)));
toast.error("发送失败", { id: localId });
}, 10_000);
}
// Retry a failed message
function handleRetry(p: PendingMsg) {
setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "sending" } : x)));
toast.loading("正在发送", { id: p.localId });
ws.send({ type: "send", content: p.content });
pendingTimers.current[p.localId] = setTimeout(() => {
setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "failed" } : x)));
toast.error("发送失败", { id: p.localId });
}, 10_000);
}
// Auto-scroll when a new message arrives (WebSocket push)
const msgCountRef = useRef(0);
useEffect(() => {
if (msgCountRef.current === 0 && allMessages.length > 0) {
// Initial data just arrived — seed count, don't scroll
msgCountRef.current = allMessages.length;
return;
}
if (allMessages.length > msgCountRef.current) {
scrollToBottom(true);
}
msgCountRef.current = allMessages.length;
}, [allMessages.length, scrollToBottom]);
if (loading) {
return (
@@ -400,6 +555,7 @@ export default function ChatPage() {
behavior="padding"
keyboardVerticalOffset={-60}
>
<Animated.View style={{ flex: 1, opacity }}>
<LayoutView>
<LayoutView.FlatList
ref={listRef}
@@ -410,24 +566,32 @@ export default function ChatPage() {
/>
}
footer={
<View className="flex-row items-center gap-2 px-3 py-2.5 bg-white/95 rounded-2xl shadow-sm">
<View className="flex-row items-center gap-2 px-2.5 py-1.5 bg-white/95 rounded-2xl shadow-sm">
<View className="flex-1 min-h-9 max-h-20 rounded-xl bg-background px-3 justify-center">
<TextInput
className="flex-1 h-10 bg-gray-100 rounded-full px-4 text-[15px]"
placeholder="输入消息..."
placeholderTextColor="#9CA3AF"
className="min-h-8 max-h-20 text-[15px] leading-[20px] text-foreground"
placeholder="说点什么..."
placeholderTextColor="#86A19C"
value={inputText}
onChangeText={setInputText}
onFocus={() => scrollToBottom(true)}
onSubmitEditing={handleSend}
returnKeyType="send"
multiline
maxLength={800}
textAlignVertical="center"
/>
</View>
<PressableFeedback onPress={handleSend} isDisabled={!inputText.trim()}>
<View
className={cn(
"w-10 h-10 rounded-full items-center justify-center",
inputText.trim() ? "bg-accent" : "bg-gray-200",
"w-9 h-9 rounded-full items-center justify-center",
inputText.trim()
? "bg-accent shadow-sm"
: "bg-background",
)}
>
<SendHorizonalIcon size={18} color={inputText.trim() ? "white" : "#9CA3AF"} />
<SendHorizonalIcon size={17} color={inputText.trim() ? "white" : "#86A19C"} />
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
@@ -435,6 +599,7 @@ export default function ChatPage() {
}
data={allMessages}
keyExtractor={(item) => String(item.id)}
contentContainerStyle={{ paddingBottom: 12 }}
onScroll={({ nativeEvent }) => {
if (nativeEvent.contentOffset.y < 30 && hasMore && !loadingMore) {
handleLoadMore();
@@ -442,8 +607,12 @@ export default function ChatPage() {
}}
scrollEventThrottle={200}
onContentSizeChange={() => {
if (loadingMore) return;
listRef.current?.scrollToEnd({ animated: false });
if (!revealed.current) {
revealed.current = true;
revealAtBottom();
return;
}
if (!loadingMore) scrollToBottom(false);
}}
ListHeaderComponent={
loadingMore ? (
@@ -457,7 +626,32 @@ export default function ChatPage() {
<Text className="text-gray-400 text-sm">暂无消息,开始聊天吧</Text>
</View>
}
renderItem={({ item }) => (
renderItem={({ item }: { item: any }) => {
const pending = item._pending as PendingMsg | undefined;
if (pending) {
return (
<View className="flex-row items-center px-3 py-1.5 justify-end">
{pending.status === "sending" ? (
<View className="mr-1">
<Spinner size="sm" color="#9CA3AF" />
</View>
) : (
<PressableFeedback onPress={() => handleRetry(pending)}>
<View className="mr-1 p-1">
<AlertCircleIcon size={16} color="#EF4444" />
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
)}
<View className="max-w-[60%] rounded-2xl rounded-br-md px-3.5 py-2 bg-accent">
<Text className="text-[15px] leading-[22px] text-white">
{pending.content}
</Text>
</View>
</View>
);
}
return (
<ChatMessageItem
item={item}
isSelf={item.user_id === userId}
@@ -466,9 +660,11 @@ export default function ChatPage() {
onRecall={ws.recall}
onDelete={ws.deleteMessage}
/>
)}
);
}}
/>
</LayoutView>
</Animated.View>
</KeyboardAvoidingView>
);
}
@@ -1,5 +1,5 @@
import LayoutView from "@/components/layout/view";
import { SearchField, CloseButton, PressableFeedback } from "heroui-native";
import { SearchField, CloseButton, PressableFeedback, useThemeColor } from "heroui-native";
import { toast } from "sonner-native";
import { View, Text } from "react-native";
import { Stack, useRouter } from "expo-router";
@@ -14,6 +14,7 @@ import { Heart } from "lucide-react-native";
export default function Tools() {
const router = useRouter();
const safeAreaInsets = useSafeAreaInsets();
const accentColor = useThemeColor("accent");
const scrollRef = useRef<any>(null);
const headerPositions = useRef<Record<string, number>>({});
@@ -120,7 +121,7 @@ export default function Tools() {
{/* 收藏分组特殊标记 */}
{item.title === "收藏" && (
<View className="w-5 h-5 rounded-full bg-accent/15 items-center justify-center mr-1.5">
<Heart size={10} fill="#0d9488" color="#0d9488" />
<Heart size={10} fill={accentColor} color={accentColor} />
</View>
)}
{/* 普通分组标记 */}
@@ -175,8 +176,8 @@ export default function Tools() {
>
<Heart
size={18}
fill={isFav ? "#0d9488" : "none"}
color={isFav ? "#0d9488" : "#9ca3af"}
fill={isFav ? accentColor : "none"}
color={isFav ? accentColor : "#9ca3af"}
/>
<PressableFeedback.Ripple />
</PressableFeedback>
+3 -2
View File
@@ -1,6 +1,6 @@
import DateTimePicker from "react-native-dates-picker";
import dayjs from "dayjs";
import { BottomSheet, Button, Spinner } from "heroui-native";
import { BottomSheet, Button, Spinner, useThemeColor } from "heroui-native";
import { useState, useEffect, useCallback, useRef } from "react";
import { View } from "react-native";
import { CheckIcon } from "lucide-react-native";
@@ -49,6 +49,7 @@ export default function DateTime({
format,
}: DatePickerProps) {
const finalFormat = format ?? defaultFormatMap[mode];
const accentColor = useThemeColor("accent");
const [open, setOpen] = useState(false);
const [tempDate, setTempDate] = useState<dayjs.Dayjs | undefined>(dayjs());
@@ -100,7 +101,7 @@ export default function DateTime({
date={tempDate}
initialView={modeViewMap[mode]}
timePicker={mode === "time" || mode === "datetime"}
selectedItemColor="#0d9488"
selectedItemColor={accentColor}
onChange={({ date }) => {
if (date) setTempDate(dayjs(date));
}}
@@ -1,6 +1,6 @@
import DateTimePicker, { DateType } from "react-native-dates-picker";
import dayjs from "dayjs";
import { BottomSheet, Button, Spinner } from "heroui-native";
import { BottomSheet, Button, Spinner, useThemeColor } from "heroui-native";
import { View } from "react-native";
import { useState, useEffect, useCallback, useRef } from "react";
import { CheckIcon } from "lucide-react-native";
@@ -21,6 +21,8 @@ export default function DateTimeRange({
onClose,
onConfirm,
}: DateRangePickerProps) {
const accentColor = useThemeColor("accent");
const accentSoftColor = useThemeColor("accent-soft");
const [open, setOpen] = useState(false);
const [range, setRange] = useState<{
startDate: DateType;
@@ -75,8 +77,8 @@ export default function DateTimeRange({
mode="range"
startDate={range.startDate}
endDate={range.endDate}
selectedItemColor="#0d9488"
selectedRangeBackgroundColor="rgba(13,148,136,0.15)"
selectedItemColor={accentColor}
selectedRangeBackgroundColor={accentSoftColor}
onChange={(params) => setRange(params)}
/>
@@ -7,13 +7,13 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { useEffect } from "react";
import { useThemeColor } from "heroui-native";
// ── 视觉常量 ──────────────────────────────────────
const ICON_SIZE = 20;
const LABEL_SIZE = 11;
const BAR_HEIGHT = 56;
const BAR_RADIUS = 16;
const COLOR_ACTIVE = "#10B981";
const COLOR_IDLE = "#9CA3AF";
const BAR_BG = "#FFFFFF";
@@ -62,6 +62,7 @@ function TabItem({
onPress: () => void;
}) {
const Icon = tab.icon;
const activeColor = useThemeColor("accent");
const scale = useSharedValue(1);
useEffect(() => {
@@ -86,7 +87,7 @@ function TabItem({
<Animated.View style={[s.itemInner, animatedStyle]}>
<Icon
size={ICON_SIZE}
color={active ? COLOR_ACTIVE : COLOR_IDLE}
color={active ? activeColor : COLOR_IDLE}
strokeWidth={active ? 2.2 : 1.8}
/>
{tab.title ? (
@@ -94,7 +95,7 @@ function TabItem({
style={[
s.label,
{
color: active ? COLOR_ACTIVE : COLOR_IDLE,
color: active ? activeColor : COLOR_IDLE,
fontWeight: active ? "600" : "400",
},
]}
@@ -8,7 +8,6 @@ import {
ViewStyle,
} from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { ScrollShadow } from "heroui-native";
import { LinearGradient } from "expo-linear-gradient";
import { LegendList } from "@legendapp/list";
@@ -167,14 +166,13 @@ LayoutView.FlatList = forwardRef(
paddingTop: padding.paddingTop,
paddingHorizontal: padding.paddingHorizontal,
}}
className="z-[999]"
>
{header}
</View>
) : null}
<Wrapper>
<Shadow>
<FlatList ref={ref} {...props} contentContainerStyle={contentContainerStyle} />
</Shadow>
</Wrapper>
{footer ? (
<View
+2
View File
@@ -6,6 +6,7 @@
@layer theme {
:root {
@variant light {
--background: oklch(0.98 0 0);
--foreground: oklch(0.15 0 0);
@@ -13,3 +14,4 @@
--accent-foreground: oklch(0.98 0 0);
}
}
}