refactor(core): optimize state management and component rendering performance
Improve application stability and performance by optimizing Zustand store usage, implementing memoization patterns, and introducing persistent authentication. - **State Management**: - Refactor Zustand selectors to use `useShallow` and `getState()` to prevent unnecessary re-renders and infinite loops in hooks. - Implement `persist` middleware for `authStore` to maintain user sessions across restarts. - Introduce `buildStateCached` in `themeStore` to reduce redundant theme object computations. - **Performance & Rendering**: - Implement `useMemo` for stable object/array references in `ImageGallery` and list components to prevent expensive re-renders. - Replace inline arrow functions with `useCallback` in complex screens like `ChatScreen` and `MessageListScreen`. - Optimize `FlashList` usage by providing stable `key` and `extraData` props. - **Architecture**: - Decouple unread count fetching by introducing `useUnreadCountQuery` (React Query) for better caching and synchronization. - Add `useChannels` hook to centralize channel data fetching. - Refine `SessionGate` logic to allow immediate rendering of authenticated users while verifying in the background.
This commit is contained in:
@@ -101,15 +101,18 @@ function SystemChrome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SessionGate({ children }: { children: React.ReactNode }) {
|
function SessionGate({ children }: { children: React.ReactNode }) {
|
||||||
const [ready, setReady] = useState(false);
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCurrentUser().finally(() => setReady(true));
|
fetchCurrentUser().finally(() => setVerified(true));
|
||||||
}, [fetchCurrentUser]);
|
}, [fetchCurrentUser]);
|
||||||
|
|
||||||
if (!ready) {
|
// If persisted state shows authenticated, render immediately (background verify)
|
||||||
|
// If not authenticated and not yet verified, show loading
|
||||||
|
if (!isAuthenticated && !verified) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -123,6 +126,7 @@ function SessionGate({ children }: { children: React.ReactNode }) {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
src/hooks/useChannels.ts
Normal file
14
src/hooks/useChannels.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { channelService, type ChannelItem } from '@/services/post/channelService';
|
||||||
|
|
||||||
|
export const channelKeys = {
|
||||||
|
all: ['channels'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useChannels() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: channelKeys.all,
|
||||||
|
queryFn: () => channelService.list(),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
15
src/hooks/useUnreadCount.ts
Normal file
15
src/hooks/useUnreadCount.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { messageManager } from '@/stores/message/MessageManager';
|
||||||
|
|
||||||
|
export const messageKeys = {
|
||||||
|
unread: ['messages', 'unread'] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useUnreadCountQuery() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: messageKeys.unread,
|
||||||
|
queryFn: () => messageManager.fetchUnreadCount(),
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
refetchInterval: 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
resetRegisterData,
|
resetRegisterData,
|
||||||
goToNextStep,
|
goToNextStep,
|
||||||
goToPrevStep,
|
goToPrevStep,
|
||||||
} = useRegisterStore();
|
} = useRegisterStore.getState();
|
||||||
|
|
||||||
// 本地状态
|
// 本地状态
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { Post } from '../../types';
|
|||||||
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
||||||
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
|
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
|
||||||
import { channelService, postService } from '../../services';
|
import { channelService, postService } from '../../services';
|
||||||
|
import { useChannels } from '../../hooks/useChannels';
|
||||||
import { PostCard, SearchBar, ShareSheet } from '../../components/business';
|
import { PostCard, SearchBar, ShareSheet } from '../../components/business';
|
||||||
import type { PostCardAction } from '../../components/business/PostCard';
|
import type { PostCardAction } from '../../components/business/PostCard';
|
||||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
|
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||||
@@ -215,7 +216,7 @@ function createHomeStyles(colors: AppColors, responsivePadding: number) {
|
|||||||
export const HomeScreen: React.FC = () => {
|
export const HomeScreen: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { posts: storePosts } = useUserStore();
|
const storePosts = useUserStore((s) => s.posts);
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
const isVerified = useIsVerified();
|
const isVerified = useIsVerified();
|
||||||
@@ -242,13 +243,23 @@ export const HomeScreen: React.FC = () => {
|
|||||||
|
|
||||||
const [sortIndex, setSortIndex] = useState(2);
|
const [sortIndex, setSortIndex] = useState(2);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||||
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
|
const { data: channelList = [] } = useChannels();
|
||||||
|
const latestCapsules = useMemo<LatestCapsule[]>(
|
||||||
|
() => [{ id: '', name: '全部' }, ...channelList.map(item => ({ id: item.id, name: item.name }))],
|
||||||
|
[channelList]
|
||||||
|
);
|
||||||
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
||||||
|
|
||||||
// 图片查看器状态
|
// 图片查看器状态
|
||||||
const [showImageViewer, setShowImageViewer] = useState(false);
|
const [showImageViewer, setShowImageViewer] = useState(false);
|
||||||
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
||||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||||
|
|
||||||
|
const stableCloseImageViewer = useCallback(() => setShowImageViewer(false), []);
|
||||||
|
const stableGalleryImages = useMemo(() => postImages.map((img, i) => ({
|
||||||
|
id: img.id || img.url || `img-${i}`,
|
||||||
|
url: img.url || img.uri || ''
|
||||||
|
})), [postImages]);
|
||||||
|
|
||||||
// 搜索显示状态(用于内嵌搜索页面)
|
// 搜索显示状态(用于内嵌搜索页面)
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
@@ -587,18 +598,6 @@ export const HomeScreen: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadChannels = async () => {
|
|
||||||
const list = await channelService.list();
|
|
||||||
const capsules: LatestCapsule[] = [
|
|
||||||
{ id: '', name: '全部' },
|
|
||||||
...list.map(item => ({ id: item.id, name: item.name })),
|
|
||||||
];
|
|
||||||
setLatestCapsules(capsules);
|
|
||||||
};
|
|
||||||
loadChannels();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 跳转到搜索页(使用内嵌模式,不再依赖导航)
|
// 跳转到搜索页(使用内嵌模式,不再依赖导航)
|
||||||
const handleSearchPress = () => {
|
const handleSearchPress = () => {
|
||||||
setShowSearch(true);
|
setShowSearch(true);
|
||||||
@@ -811,9 +810,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
const renderResponsiveGrid = () => {
|
const renderResponsiveGrid = () => {
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
key={`${listKey}_grid`}
|
key="home-grid"
|
||||||
ref={scrollViewRef as any}
|
ref={scrollViewRef as any}
|
||||||
data={displayPosts}
|
data={displayPosts}
|
||||||
|
extraData={listKey}
|
||||||
renderItem={renderGridItem}
|
renderItem={renderGridItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
numColumns={gridColumns}
|
numColumns={gridColumns}
|
||||||
@@ -865,9 +865,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 移动端和宽屏都使用单列 FlashList,宽屏下居中显示
|
// 移动端和宽屏都使用单列 FlashList,宽屏下居中显示
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
key={listKey}
|
key="home-list"
|
||||||
ref={flashListRef}
|
ref={flashListRef}
|
||||||
data={displayPosts}
|
data={displayPosts}
|
||||||
|
extraData={listKey}
|
||||||
renderItem={renderPostList}
|
renderItem={renderPostList}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
@@ -1062,12 +1063,9 @@ export const HomeScreen: React.FC = () => {
|
|||||||
{/* 图片查看器 */}
|
{/* 图片查看器 */}
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
visible={showImageViewer}
|
visible={showImageViewer}
|
||||||
images={postImages.map(img => ({
|
images={stableGalleryImages}
|
||||||
id: img.id || img.url || String(Math.random()),
|
|
||||||
url: img.url || img.uri || ''
|
|
||||||
}))}
|
|
||||||
initialIndex={selectedImageIndex}
|
initialIndex={selectedImageIndex}
|
||||||
onClose={() => setShowImageViewer(false)}
|
onClose={stableCloseImageViewer}
|
||||||
enableSave
|
enableSave
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,8 @@ export function MarketView({
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
numColumns={numColumns}
|
numColumns={numColumns}
|
||||||
key={`market_${numColumns}`}
|
key="market-list"
|
||||||
|
extraData={numColumns}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
|
|||||||
@@ -1084,6 +1084,12 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
}, [post?.images]);
|
}, [post?.images]);
|
||||||
|
|
||||||
|
// 为 ImageGallery 提供稳定的图片列表,避免 Math.random() 在每次渲染时生成新 ID
|
||||||
|
const stableGalleryImages = useMemo(() => allImages.map((img, i) => ({
|
||||||
|
id: img.id || img.url || `img-${i}`,
|
||||||
|
url: img.url || img.uri || ''
|
||||||
|
})), [allImages]);
|
||||||
|
|
||||||
// 渲染帖子头部 - 小红书/微博风格
|
// 渲染帖子头部 - 小红书/微博风格
|
||||||
const renderPostHeader = useCallback(() => {
|
const renderPostHeader = useCallback(() => {
|
||||||
if (!post) return null;
|
if (!post) return null;
|
||||||
@@ -1740,10 +1746,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
{/* 图片预览 ImageGallery */}
|
{/* 图片预览 ImageGallery */}
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
visible={showImageModal}
|
visible={showImageModal}
|
||||||
images={allImages.map(img => ({
|
images={stableGalleryImages}
|
||||||
id: img.id || img.url || String(Math.random()),
|
|
||||||
url: img.url || img.uri || ''
|
|
||||||
}))}
|
|
||||||
initialIndex={selectedImageIndex}
|
initialIndex={selectedImageIndex}
|
||||||
onClose={() => setShowImageModal(false)}
|
onClose={() => setShowImageModal(false)}
|
||||||
enableSave
|
enableSave
|
||||||
@@ -1815,10 +1818,7 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
{/* 图片预览 ImageGallery */}
|
{/* 图片预览 ImageGallery */}
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
visible={showImageModal}
|
visible={showImageModal}
|
||||||
images={allImages.map(img => ({
|
images={stableGalleryImages}
|
||||||
id: img.id || img.url || String(Math.random()),
|
|
||||||
url: img.url || img.uri || ''
|
|
||||||
}))}
|
|
||||||
initialIndex={selectedImageIndex}
|
initialIndex={selectedImageIndex}
|
||||||
onClose={() => setShowImageModal(false)}
|
onClose={() => setShowImageModal(false)}
|
||||||
enableSave
|
enableSave
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
|||||||
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
|
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { searchHistory: history, addSearchHistory, clearSearchHistory } = useUserStore();
|
const history = useUserStore((state) => state.searchHistory);
|
||||||
|
const { addSearchHistory, clearSearchHistory } = useUserStore.getState();
|
||||||
|
|
||||||
// 使用响应式 hook
|
// 使用响应式 hook
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
const [chatImages, setChatImages] = useState<ImageGridItem[]>([]);
|
const [chatImages, setChatImages] = useState<ImageGridItem[]>([]);
|
||||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||||
|
|
||||||
|
const stableGalleryImages = useMemo(() => chatImages.map((img, i) => ({
|
||||||
|
id: img.id || img.url || `img-${i}`,
|
||||||
|
url: img.url || img.uri || ''
|
||||||
|
})), [chatImages]);
|
||||||
|
|
||||||
// 图片点击处理函数
|
// 图片点击处理函数
|
||||||
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
||||||
setChatImages(images);
|
setChatImages(images);
|
||||||
@@ -212,6 +217,28 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
jumpToLatestMessages,
|
jumpToLatestMessages,
|
||||||
setBrowsingHistory,
|
setBrowsingHistory,
|
||||||
} = useChatScreen(props);
|
} = useChatScreen(props);
|
||||||
|
|
||||||
|
const stableOnBack = useCallback(() => router.back(), [router]);
|
||||||
|
const stableOnFocusInput = useCallback(() => {
|
||||||
|
if (activePanel !== 'none' && activePanel !== 'mention') {
|
||||||
|
closePanel();
|
||||||
|
}
|
||||||
|
}, [activePanel, closePanel]);
|
||||||
|
const stableFocusTextInput = useCallback(() => textInputRef.current?.focus(), []);
|
||||||
|
const stableOnLayout = useCallback((e: any) => {
|
||||||
|
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
|
||||||
|
}, []);
|
||||||
|
const stableOnScrollBeginDrag = useCallback(() => {
|
||||||
|
isUserDraggingRef.current = true;
|
||||||
|
handleDismiss();
|
||||||
|
}, [handleDismiss]);
|
||||||
|
const stableOnScrollEndDrag = useCallback(() => {
|
||||||
|
isUserDraggingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
const stableOnMomentumScrollEnd = useCallback(() => {
|
||||||
|
isUserDraggingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||||
|
|
||||||
const longPressMenuMemberMap = useMemo(() => {
|
const longPressMenuMemberMap = useMemo(() => {
|
||||||
@@ -460,7 +487,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
otherUser={otherUser}
|
otherUser={otherUser}
|
||||||
routeGroupName={effectiveGroupName ?? undefined}
|
routeGroupName={effectiveGroupName ?? undefined}
|
||||||
typingHint={typingHint}
|
typingHint={typingHint}
|
||||||
onBack={props.onEmbeddedBack ? props.onEmbeddedBack : () => router.back()}
|
onBack={props.onEmbeddedBack || stableOnBack}
|
||||||
onTitlePress={navigateToInfo}
|
onTitlePress={navigateToInfo}
|
||||||
onMorePress={navigateToChatSettings}
|
onMorePress={navigateToChatSettings}
|
||||||
onGroupInfoPress={handleGroupInfoPress}
|
onGroupInfoPress={handleGroupInfoPress}
|
||||||
@@ -470,9 +497,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
<View
|
<View
|
||||||
style={styles.messageListContainer}
|
style={styles.messageListContainer}
|
||||||
onLayout={e => {
|
onLayout={stableOnLayout}
|
||||||
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
|
|
||||||
}}
|
|
||||||
onTouchEnd={handleDismiss}
|
onTouchEnd={handleDismiss}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -493,16 +518,9 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
scrollEnabled={true}
|
scrollEnabled={true}
|
||||||
drawDistance={250}
|
drawDistance={250}
|
||||||
onScroll={handleMessageListScroll}
|
onScroll={handleMessageListScroll}
|
||||||
onScrollBeginDrag={() => {
|
onScrollBeginDrag={stableOnScrollBeginDrag}
|
||||||
isUserDraggingRef.current = true;
|
onScrollEndDrag={stableOnScrollEndDrag}
|
||||||
handleDismiss();
|
onMomentumScrollEnd={stableOnMomentumScrollEnd}
|
||||||
}}
|
|
||||||
onScrollEndDrag={() => {
|
|
||||||
isUserDraggingRef.current = false;
|
|
||||||
}}
|
|
||||||
onMomentumScrollEnd={() => {
|
|
||||||
isUserDraggingRef.current = false;
|
|
||||||
}}
|
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onContentSizeChange={handleContentSizeChange}
|
onContentSizeChange={handleContentSizeChange}
|
||||||
/>
|
/>
|
||||||
@@ -568,12 +586,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
pendingAttachments={pendingAttachments}
|
pendingAttachments={pendingAttachments}
|
||||||
onRemovePendingAttachment={removePendingAttachment}
|
onRemovePendingAttachment={removePendingAttachment}
|
||||||
onCancelReply={handleCancelReply}
|
onCancelReply={handleCancelReply}
|
||||||
onFocus={() => {
|
onFocus={stableOnFocusInput}
|
||||||
// 输入框获得焦点时,关闭其他面板(但不要关闭键盘)
|
|
||||||
if (activePanel !== 'none' && activePanel !== 'mention') {
|
|
||||||
closePanel();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
otherUser={otherUser}
|
otherUser={otherUser}
|
||||||
getSenderInfo={getSenderInfo}
|
getSenderInfo={getSenderInfo}
|
||||||
@@ -587,7 +600,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
onInsertEmoji={handleInsertEmoji}
|
onInsertEmoji={handleInsertEmoji}
|
||||||
onInsertSticker={handleSendSticker}
|
onInsertSticker={handleSendSticker}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
onFocusInput={() => textInputRef.current?.focus()}
|
onFocusInput={stableFocusTextInput}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -657,10 +670,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
|||||||
{/* 图片查看器 */}
|
{/* 图片查看器 */}
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
visible={showImageViewer}
|
visible={showImageViewer}
|
||||||
images={chatImages.map(img => ({
|
images={stableGalleryImages}
|
||||||
id: img.id || img.url || String(Math.random()),
|
|
||||||
url: img.url || img.uri || ''
|
|
||||||
}))}
|
|
||||||
initialIndex={selectedImageIndex}
|
initialIndex={selectedImageIndex}
|
||||||
onClose={handleCloseImageViewer}
|
onClose={handleCloseImageViewer}
|
||||||
enableSave
|
enableSave
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
// 系统通知显示状态 - 用于在移动端显示通知页面
|
// 系统通知显示状态 - 用于在移动端显示通知页面
|
||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
|
|
||||||
|
// Stable callbacks to avoid inline arrows passed to child components
|
||||||
|
const stableCloseNotifications = useCallback(() => setShowNotifications(false), []);
|
||||||
|
const stableClearSelectedConversation = useCallback(() => setSelectedConversation(null), []);
|
||||||
|
const stableCloseScanner = useCallback(() => setScannerVisible(false), []);
|
||||||
|
|
||||||
// 系统消息会话对象(用于选中状态)
|
// 系统消息会话对象(用于选中状态)
|
||||||
const systemMessageConversation: ConversationResponse = useMemo(() => ({
|
const systemMessageConversation: ConversationResponse = useMemo(() => ({
|
||||||
id: SYSTEM_MESSAGE_CHANNEL_ID,
|
id: SYSTEM_MESSAGE_CHANNEL_ID,
|
||||||
@@ -197,14 +202,11 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
|
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
|
||||||
}, [isWideScreen, insets.bottom]);
|
}, [isWideScreen, insets.bottom]);
|
||||||
|
|
||||||
// 【新架构】页面获得焦点时初始化MessageManager
|
// 初始化MessageManager(仅首次),焦点时轻量刷新会话列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFocused) {
|
messageManager.initialize();
|
||||||
messageManager.initialize();
|
}, []);
|
||||||
}
|
|
||||||
}, [isFocused]);
|
|
||||||
|
|
||||||
// 【新架构】使用focus刷新hook,从ChatScreen返回时自动刷新未读数
|
|
||||||
useMessageListRefresh();
|
useMessageListRefresh();
|
||||||
|
|
||||||
// 同步未读数到userStore(用于TabBar角标显示)
|
// 同步未读数到userStore(用于TabBar角标显示)
|
||||||
@@ -840,7 +842,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
<View style={styles.chatArea}>
|
<View style={styles.chatArea}>
|
||||||
{showNotifications ? (
|
{showNotifications ? (
|
||||||
// 显示系统通知页面
|
// 显示系统通知页面
|
||||||
<NotificationsScreen onBack={() => setShowNotifications(false)} />
|
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||||
) : selectedConversation ? (
|
) : selectedConversation ? (
|
||||||
// 显示选中会话的聊天内容
|
// 显示选中会话的聊天内容
|
||||||
<ChatScreen
|
<ChatScreen
|
||||||
@@ -849,7 +851,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
embeddedIsGroupChat={selectedConversation.type === 'group'}
|
embeddedIsGroupChat={selectedConversation.type === 'group'}
|
||||||
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
|
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
|
||||||
embeddedGroupName={selectedConversation.group?.name}
|
embeddedGroupName={selectedConversation.group?.name}
|
||||||
onEmbeddedBack={() => setSelectedConversation(null)}
|
onEmbeddedBack={stableClearSelectedConversation}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
// 默认占位符
|
// 默认占位符
|
||||||
@@ -871,7 +873,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
{showNotifications ? (
|
{showNotifications ? (
|
||||||
// 显示系统通知页面,传入 onBack 回调
|
// 显示系统通知页面,传入 onBack 回调
|
||||||
<NotificationsScreen onBack={() => setShowNotifications(false)} />
|
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||||
) : isSearchMode ? (
|
) : isSearchMode ? (
|
||||||
renderSearchMode()
|
renderSearchMode()
|
||||||
) : isWideScreen ? (
|
) : isWideScreen ? (
|
||||||
@@ -882,7 +884,7 @@ export const MessageListScreen: React.FC = () => {
|
|||||||
renderConversationList()
|
renderConversationList()
|
||||||
)}
|
)}
|
||||||
{renderActionMenu()}
|
{renderActionMenu()}
|
||||||
<QRCodeScanner visible={scannerVisible} onClose={() => setScannerVisible(false)} />
|
<QRCodeScanner visible={scannerVisible} onClose={stableCloseScanner} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const FollowListScreen: React.FC = () => {
|
|||||||
const type = typeParam === 'followers' ? 'followers' : 'following';
|
const type = typeParam === 'followers' ? 'followers' : 'following';
|
||||||
|
|
||||||
const { currentUser } = useAuthStore();
|
const { currentUser } = useAuthStore();
|
||||||
const { followUser, unfollowUser } = useUserStore();
|
const { followUser, unfollowUser } = useUserStore.getState();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
const { isWideScreen, isDesktop, width } = useResponsive();
|
const { isWideScreen, isDesktop, width } = useResponsive();
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { followUser, unfollowUser, posts: storePosts } = useUserStore();
|
const storePosts = useUserStore((state) => state.posts);
|
||||||
|
const { followUser, unfollowUser } = useUserStore.getState();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -634,6 +634,11 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
|
|||||||
}));
|
}));
|
||||||
}, [images]);
|
}, [images]);
|
||||||
|
|
||||||
|
const stableTradeGalleryImages = useMemo(() => tradeImages.map((img, i) => ({
|
||||||
|
id: img.id || img.url || `trade-img-${i}`,
|
||||||
|
url: img.url || '',
|
||||||
|
})), [tradeImages]);
|
||||||
|
|
||||||
const handleImagePress = useCallback((allImages: any[], index: number) => {
|
const handleImagePress = useCallback((allImages: any[], index: number) => {
|
||||||
setSelectedImageIndex(index);
|
setSelectedImageIndex(index);
|
||||||
setShowImageModal(true);
|
setShowImageModal(true);
|
||||||
@@ -965,10 +970,7 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
|
|||||||
{/* ─── 图片预览 ─── */}
|
{/* ─── 图片预览 ─── */}
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
visible={showImageModal}
|
visible={showImageModal}
|
||||||
images={tradeImages.map(img => ({
|
images={stableTradeGalleryImages}
|
||||||
id: img.id || img.url || String(Math.random()),
|
|
||||||
url: img.url || '',
|
|
||||||
}))}
|
|
||||||
initialIndex={selectedImageIndex}
|
initialIndex={selectedImageIndex}
|
||||||
onClose={() => setShowImageModal(false)}
|
onClose={() => setShowImageModal(false)}
|
||||||
enableSave
|
enableSave
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import { User } from '../../types';
|
import { User } from '../../types';
|
||||||
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services';
|
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../../services';
|
||||||
import { wsService } from '@/services/core';
|
import { wsService } from '@/services/core';
|
||||||
@@ -148,15 +149,17 @@ interface AuthState {
|
|||||||
fetchCurrentUser: () => Promise<void>;
|
fetchCurrentUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>()(
|
||||||
isAuthenticated: false,
|
persist(
|
||||||
currentUser: null,
|
(set) => ({
|
||||||
isLoading: false,
|
isAuthenticated: false,
|
||||||
error: null,
|
currentUser: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
// ── 登录 ──
|
// ── 登录 ──
|
||||||
// 流程:API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态
|
// 流程:API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态
|
||||||
login: async (credentials: LoginRequest) => {
|
login: async (credentials: LoginRequest) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -396,7 +399,17 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
||||||
|
|
||||||
setError: (error: string | null) => set({ error }),
|
setError: (error: string | null) => set({ error }),
|
||||||
}));
|
}),
|
||||||
|
{
|
||||||
|
name: 'auth-session',
|
||||||
|
storage: createJSONStorage(() => AsyncStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
isAuthenticated: state.isAuthenticated,
|
||||||
|
currentUser: state.currentUser,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// 导出 selector hooks 以优化性能
|
// 导出 selector hooks 以优化性能
|
||||||
export const useCurrentUser = () => useAuthStore((state) => state.currentUser);
|
export const useCurrentUser = () => useAuthStore((state) => state.currentUser);
|
||||||
|
|||||||
@@ -97,13 +97,14 @@ export function useMessages(
|
|||||||
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
loadMoreMessages: (conversationId: string, beforeSeq: number, limit?: number) => Promise<MessageResponse[]>
|
||||||
): UseMessagesReturn {
|
): UseMessagesReturn {
|
||||||
const normalizedConversationId = normalizeConversationId(conversationId);
|
const normalizedConversationId = normalizeConversationId(conversationId);
|
||||||
const store = useMessageStore();
|
|
||||||
|
|
||||||
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
// 使用 useShallow 避免每次返回新数组引用导致的无限循环
|
||||||
const messages = useMessageStore(
|
const messages = useMessageStore(
|
||||||
useShallow(state => state.messagesMap.get(normalizedConversationId) || [])
|
useShallow(state => state.messagesMap.get(normalizedConversationId) || [])
|
||||||
);
|
);
|
||||||
const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId));
|
const isLoadingMessages = useMessageStore(state => state.loadingMessagesSet.has(normalizedConversationId));
|
||||||
|
const getActiveConversation = useMessageStore.getState().getActiveConversation;
|
||||||
|
const setCurrentConversation = useMessageStore.getState().setCurrentConversation;
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -114,8 +115,8 @@ export function useMessages(
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// 清理活动会话
|
// 清理活动会话
|
||||||
if (store.getActiveConversation() === normalizedConversationId) {
|
if (getActiveConversation() === normalizedConversationId) {
|
||||||
store.setCurrentConversation(null);
|
setCurrentConversation(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [normalizedConversationId]);
|
}, [normalizedConversationId]);
|
||||||
@@ -428,11 +429,10 @@ interface UseGroupMutedReturn {
|
|||||||
export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
export function useGroupMuted(groupId: string): UseGroupMutedReturn {
|
||||||
// 使用 Zustand selector 直接订阅禁言状态
|
// 使用 Zustand selector 直接订阅禁言状态
|
||||||
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false);
|
const isMuted = useMessageStore(state => state.mutedStatusMap.get(groupId) || false);
|
||||||
const store = useMessageStore();
|
|
||||||
|
|
||||||
const setMutedStatus = useCallback((muted: boolean) => {
|
const setMutedStatus = useCallback((muted: boolean) => {
|
||||||
store.setMutedStatus(groupId, muted);
|
useMessageStore.getState().setMutedStatus(groupId, muted);
|
||||||
}, [groupId, store]);
|
}, [groupId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMuted,
|
isMuted,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useShallow } from 'zustand/react/shallow';
|
|||||||
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
|
import { ConversationResponse, MessageResponse, MessageSegment } from '../../types/dto';
|
||||||
import { messageManager } from './MessageManager';
|
import { messageManager } from './MessageManager';
|
||||||
import { useMessageStore } from './store';
|
import { useMessageStore } from './store';
|
||||||
|
import { useUnreadCountQuery } from '../../hooks/useUnreadCount';
|
||||||
|
|
||||||
// ==================== useConversations - 获取会话列表 ====================
|
// ==================== useConversations - 获取会话列表 ====================
|
||||||
|
|
||||||
@@ -43,16 +44,19 @@ export function useConversations(): UseConversationsReturn {
|
|||||||
console.error('[useConversations] 初始化失败:', error);
|
console.error('[useConversations] 初始化失败:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 冷启动兜底:延迟做一次强制刷新,避免首次因时序问题拿到空列表
|
// 仅在首次初始化时做一次冷启动兜底刷新,避免每次挂载都触发
|
||||||
const coldStartSyncTimer = setTimeout(() => {
|
const store = useMessageStore.getState();
|
||||||
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
|
if (!store.isInitialized) {
|
||||||
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
const coldStartSyncTimer = setTimeout(() => {
|
||||||
});
|
messageManager.refreshConversations(true, 'hooks-initial-refresh').catch(error => {
|
||||||
}, 1200);
|
console.error('[useConversations] 冷启动强制刷新失败:', error);
|
||||||
|
});
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(coldStartSyncTimer);
|
clearTimeout(coldStartSyncTimer);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听 hasMore 变化
|
// 监听 hasMore 变化
|
||||||
@@ -249,15 +253,11 @@ interface UseUnreadCountReturn {
|
|||||||
* 使用 Zustand selector 直接订阅未读数变化
|
* 使用 Zustand selector 直接订阅未读数变化
|
||||||
*/
|
*/
|
||||||
export function useUnreadCount(): UseUnreadCountReturn {
|
export function useUnreadCount(): UseUnreadCountReturn {
|
||||||
// 使用 Zustand selector 直接订阅未读数
|
// Use React Query for fetching (caching + auto-refetch), Zustand selectors for reading
|
||||||
|
useUnreadCountQuery();
|
||||||
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
||||||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 初始化时获取最新未读数
|
|
||||||
messageManager.fetchUnreadCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalUnreadCount,
|
totalUnreadCount,
|
||||||
systemUnreadCount,
|
systemUnreadCount,
|
||||||
@@ -271,14 +271,11 @@ export function useUnreadCount(): UseUnreadCountReturn {
|
|||||||
* 使用 Zustand selector 直接订阅
|
* 使用 Zustand selector 直接订阅
|
||||||
*/
|
*/
|
||||||
export function useTotalUnreadCount(): number {
|
export function useTotalUnreadCount(): number {
|
||||||
|
// Use React Query for fetching, Zustand selectors for reading
|
||||||
|
useUnreadCountQuery();
|
||||||
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
const totalUnreadCount = useMessageStore(state => state.totalUnreadCount);
|
||||||
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
const systemUnreadCount = useMessageStore(state => state.systemUnreadCount);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 初始化时获取最新未读数
|
|
||||||
messageManager.fetchUnreadCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return totalUnreadCount + systemUnreadCount;
|
return totalUnreadCount + systemUnreadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ function buildState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _cachedKey = '';
|
||||||
|
let _cachedState: ReturnType<typeof buildState>;
|
||||||
|
|
||||||
|
function buildStateCached(
|
||||||
|
preference: ThemePreference,
|
||||||
|
systemScheme: ResolvedScheme
|
||||||
|
): {
|
||||||
|
resolvedScheme: ResolvedScheme;
|
||||||
|
colors: AppColors;
|
||||||
|
paperTheme: MD3Theme;
|
||||||
|
} {
|
||||||
|
const key = `${preference}:${systemScheme}`;
|
||||||
|
if (_cachedKey === key && _cachedState) return _cachedState;
|
||||||
|
_cachedKey = key;
|
||||||
|
_cachedState = buildState(preference, systemScheme);
|
||||||
|
return _cachedState;
|
||||||
|
}
|
||||||
|
|
||||||
type ThemeState = {
|
type ThemeState = {
|
||||||
preference: ThemePreference;
|
preference: ThemePreference;
|
||||||
systemScheme: ResolvedScheme;
|
systemScheme: ResolvedScheme;
|
||||||
@@ -58,14 +76,14 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
|
|||||||
preference: 'system',
|
preference: 'system',
|
||||||
systemScheme: initialSystemScheme,
|
systemScheme: initialSystemScheme,
|
||||||
hydrated: false,
|
hydrated: false,
|
||||||
...buildState('system', initialSystemScheme),
|
...buildStateCached('system', initialSystemScheme),
|
||||||
|
|
||||||
setSystemScheme: (systemScheme) => {
|
setSystemScheme: (systemScheme) => {
|
||||||
const { preference, systemScheme: cur } = get();
|
const { preference, systemScheme: cur } = get();
|
||||||
if (cur === systemScheme) return;
|
if (cur === systemScheme) return;
|
||||||
set({
|
set({
|
||||||
systemScheme,
|
systemScheme,
|
||||||
...buildState(preference, systemScheme),
|
...buildStateCached(preference, systemScheme),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -78,7 +96,7 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
|
|||||||
const { systemScheme } = get();
|
const { systemScheme } = get();
|
||||||
set({
|
set({
|
||||||
preference,
|
preference,
|
||||||
...buildState(preference, systemScheme),
|
...buildStateCached(preference, systemScheme),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -97,7 +115,7 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
|
|||||||
preference,
|
preference,
|
||||||
hydrated: true,
|
hydrated: true,
|
||||||
systemScheme,
|
systemScheme,
|
||||||
...buildState(preference, systemScheme),
|
...buildStateCached(preference, systemScheme),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { User, Post, Notification, NotificationBadge } from '../types';
|
import { User, Post, Notification, NotificationBadge } from '../types';
|
||||||
import { PaginatedData } from '@/services/core';
|
import { PaginatedData } from '@/services/core';
|
||||||
import {
|
import {
|
||||||
@@ -320,7 +321,7 @@ export const useUserStore = create<UserState>((set, get) => {
|
|||||||
// 导出selector hooks以优化性能
|
// 导出selector hooks以优化性能
|
||||||
export const usePosts = () => useUserStore((state) => state.posts);
|
export const usePosts = () => useUserStore((state) => state.posts);
|
||||||
export const useNotifications = () => useUserStore((state) => state.notifications);
|
export const useNotifications = () => useUserStore((state) => state.notifications);
|
||||||
export const useNotificationBadge = () => useUserStore((state) => state.notificationBadge);
|
export const useNotificationBadge = () => useUserStore(useShallow((state) => state.notificationBadge));
|
||||||
export const useMessageUnreadCount = () => useUserStore((state) => state.messageUnreadCount);
|
export const useMessageUnreadCount = () => useUserStore((state) => state.messageUnreadCount);
|
||||||
export const useSearchHistory = () => useUserStore((state) => state.searchHistory);
|
export const useSearchHistory = () => useUserStore((state) => state.searchHistory);
|
||||||
export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts);
|
export const useIsLoadingPosts = () => useUserStore((state) => state.isLoadingPosts);
|
||||||
|
|||||||
Reference in New Issue
Block a user