541 lines
18 KiB
TypeScript
541 lines
18 KiB
TypeScript
|
|
/**
|
|||
|
|
* 通知页 NotificationsScreen
|
|||
|
|
* 胡萝卜BBS - 系统消息列表
|
|||
|
|
* 使用新的系统消息API
|
|||
|
|
* 支持响应式布局
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|||
|
|
import {
|
|||
|
|
View,
|
|||
|
|
FlatList,
|
|||
|
|
StyleSheet,
|
|||
|
|
TouchableOpacity,
|
|||
|
|
RefreshControl,
|
|||
|
|
ActivityIndicator,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|||
|
|
import { useIsFocused } from '@react-navigation/native';
|
|||
|
|
import { useNavigation } from '@react-navigation/native';
|
|||
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|||
|
|
import { colors, spacing } from '../../theme';
|
|||
|
|
import { SystemMessageResponse } from '../../types/dto';
|
|||
|
|
import { messageService } from '../../services/messageService';
|
|||
|
|
import { commentService } from '../../services/commentService';
|
|||
|
|
import { SystemMessageItem } from '../../components/business';
|
|||
|
|
import { Text, EmptyState, ResponsiveContainer } from '../../components/common';
|
|||
|
|
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
|
|||
|
|
import { RootStackParamList } from '../../navigation/types';
|
|||
|
|
import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores';
|
|||
|
|
|
|||
|
|
const MESSAGE_TYPES = [
|
|||
|
|
{ key: 'all', title: '全部' },
|
|||
|
|
{ key: 'like_post', title: '点赞' },
|
|||
|
|
{ key: 'comment', title: '评论' },
|
|||
|
|
{ key: 'follow', title: '关注' },
|
|||
|
|
{ key: 'group', title: '群通知' },
|
|||
|
|
{ key: 'system', title: '系统' },
|
|||
|
|
{ key: 'announcement', title: '公告' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
|
|||
|
|
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
|
|||
|
|
|
|||
|
|
export const NotificationsScreen: React.FC = () => {
|
|||
|
|
const isFocused = useIsFocused();
|
|||
|
|
const fetchMessageUnreadCount = useUserStore(state => state.fetchMessageUnreadCount);
|
|||
|
|
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
|||
|
|
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
|||
|
|
|
|||
|
|
// 响应式布局
|
|||
|
|
const { isDesktop, isTablet } = useResponsive();
|
|||
|
|
const isWideScreen = useBreakpointGTE('lg');
|
|||
|
|
|
|||
|
|
const [messages, setMessages] = useState<SystemMessageResponse[]>([]);
|
|||
|
|
const [activeType, setActiveType] = useState('all');
|
|||
|
|
const [refreshing, setRefreshing] = useState(false);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [hasMore, setHasMore] = useState(true);
|
|||
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|||
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|||
|
|
|
|||
|
|
// 同一 flag 只要有人审批过,就将待处理消息同步展示为已处理状态
|
|||
|
|
const displayMessages = useMemo(() => {
|
|||
|
|
const reviewedByFlag = new Map<string, SystemMessageResponse>();
|
|||
|
|
messages.forEach((msg) => {
|
|||
|
|
if (msg.system_type !== 'group_join_apply') return;
|
|||
|
|
const flag = msg.extra_data?.flag;
|
|||
|
|
const status = msg.extra_data?.request_status;
|
|||
|
|
if (!flag) return;
|
|||
|
|
if (status === 'accepted' || status === 'rejected') {
|
|||
|
|
reviewedByFlag.set(flag, msg);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (reviewedByFlag.size === 0) {
|
|||
|
|
return messages;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return messages.map((msg) => {
|
|||
|
|
if (msg.system_type !== 'group_join_apply') return msg;
|
|||
|
|
const flag = msg.extra_data?.flag;
|
|||
|
|
if (!flag) return msg;
|
|||
|
|
const reviewedMsg = reviewedByFlag.get(flag);
|
|||
|
|
if (!reviewedMsg) return msg;
|
|||
|
|
const reviewedStatus = reviewedMsg.extra_data?.request_status;
|
|||
|
|
if (reviewedStatus !== 'accepted' && reviewedStatus !== 'rejected') return msg;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
...msg,
|
|||
|
|
extra_data: {
|
|||
|
|
...msg.extra_data,
|
|||
|
|
request_status: reviewedStatus,
|
|||
|
|
actor_name: reviewedMsg.extra_data?.actor_name || reviewedMsg.extra_data?.operator_name || msg.extra_data?.actor_name,
|
|||
|
|
avatar_url: reviewedMsg.extra_data?.avatar_url || reviewedMsg.extra_data?.operator_avatar || msg.extra_data?.avatar_url,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}, [messages]);
|
|||
|
|
|
|||
|
|
// 获取系统消息数据
|
|||
|
|
const fetchMessages = useCallback(async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const response = await messageService.getSystemMessages(50, 1);
|
|||
|
|
// 添加防御性检查,确保 messages 数组存在
|
|||
|
|
setMessages(response.messages || []);
|
|||
|
|
setHasMore(response.has_more ?? false);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取系统消息失败:', error);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 获取未读数
|
|||
|
|
const fetchUnreadCount = useCallback(async () => {
|
|||
|
|
try {
|
|||
|
|
const response = await messageService.getSystemUnreadCount();
|
|||
|
|
setUnreadCount(response.unread_count);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取未读数失败:', error);
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 一键已读
|
|||
|
|
const handleMarkAllRead = useCallback(async () => {
|
|||
|
|
try {
|
|||
|
|
await messageService.markAllSystemMessagesRead();
|
|||
|
|
setMessages(prev => prev.map(m => ({ ...m, is_read: true })));
|
|||
|
|
setUnreadCount(0);
|
|||
|
|
setSystemUnreadCount(0);
|
|||
|
|
// 同步更新全局 TabBar 红点
|
|||
|
|
fetchMessageUnreadCount();
|
|||
|
|
// 刷新消息列表
|
|||
|
|
fetchMessages();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('一键已读失败:', error);
|
|||
|
|
}
|
|||
|
|
}, [fetchMessages, fetchMessageUnreadCount, setSystemUnreadCount]);
|
|||
|
|
|
|||
|
|
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (isFocused) {
|
|||
|
|
fetchMessages();
|
|||
|
|
fetchUnreadCount();
|
|||
|
|
// 进入界面自动标记所有消息为已读
|
|||
|
|
handleMarkAllRead();
|
|||
|
|
}
|
|||
|
|
}, [isFocused, fetchMessages, fetchUnreadCount, handleMarkAllRead]);
|
|||
|
|
|
|||
|
|
// 屏幕失去焦点时返回消息列表
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!isFocused) {
|
|||
|
|
// 使用 setTimeout 确保在导航状态稳定后再执行
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
navigation.goBack();
|
|||
|
|
}, 0);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
}
|
|||
|
|
}, [isFocused, navigation]);
|
|||
|
|
|
|||
|
|
// 筛选消息
|
|||
|
|
const filteredMessages = activeType === 'all'
|
|||
|
|
? displayMessages
|
|||
|
|
: displayMessages.filter((m) => {
|
|||
|
|
if (activeType === 'like_post') {
|
|||
|
|
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
if (activeType === 'group') {
|
|||
|
|
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
return m.system_type === activeType;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 下拉刷新
|
|||
|
|
const onRefresh = useCallback(async () => {
|
|||
|
|
setRefreshing(true);
|
|||
|
|
await Promise.all([fetchMessages(), fetchUnreadCount()]);
|
|||
|
|
setRefreshing(false);
|
|||
|
|
}, [fetchMessages, fetchUnreadCount]);
|
|||
|
|
|
|||
|
|
// 加载更多
|
|||
|
|
const loadMore = useCallback(async () => {
|
|||
|
|
if (loadingMore || !hasMore || messages.length === 0) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setLoadingMore(true);
|
|||
|
|
// 使用时间戳或seq作为游标分页(后端使用page分页)
|
|||
|
|
const nextPage = Math.floor((messages.length / 20)) + 1;
|
|||
|
|
const response = await messageService.getSystemMessages(20, nextPage);
|
|||
|
|
// 添加防御性检查
|
|||
|
|
const newMessages = response.messages || [];
|
|||
|
|
setMessages(prev => [...prev, ...newMessages]);
|
|||
|
|
setHasMore(response.has_more ?? false);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载更多失败:', error);
|
|||
|
|
} finally {
|
|||
|
|
setLoadingMore(false);
|
|||
|
|
}
|
|||
|
|
}, [loadingMore, hasMore, messages]);
|
|||
|
|
|
|||
|
|
// 标记单条消息已读并处理导航
|
|||
|
|
const extractPostIdFromActionUrl = (actionUrl?: string): string | null => {
|
|||
|
|
if (!actionUrl) return null;
|
|||
|
|
|
|||
|
|
const postPathMatch = actionUrl.match(/\/posts\/([^/?#]+)/);
|
|||
|
|
if (postPathMatch?.[1]) {
|
|||
|
|
return decodeURIComponent(postPathMatch[1]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const query = actionUrl.split('?')[1];
|
|||
|
|
if (!query) return null;
|
|||
|
|
const params = new URLSearchParams(query);
|
|||
|
|
return params.get('post') || params.get('post_id');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const resolvePostId = async (message: SystemMessageResponse): Promise<string | null> => {
|
|||
|
|
const { extra_data, system_type } = message;
|
|||
|
|
|
|||
|
|
// 后端已统一将 target_id 设为帖子ID(like_post/comment/mention/favorite_post/like_reply/like_comment)
|
|||
|
|
if (
|
|||
|
|
extra_data?.target_id &&
|
|||
|
|
['like_post', 'like_comment', 'like_reply', 'comment', 'mention', 'favorite_post'].includes(system_type)
|
|||
|
|
) {
|
|||
|
|
return extra_data.target_id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// reply 通知:target_id 是 replyID,从 action_url 解析帖子ID
|
|||
|
|
const postIdFromUrl = extractPostIdFromActionUrl(extra_data?.action_url);
|
|||
|
|
if (postIdFromUrl) {
|
|||
|
|
return postIdFromUrl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兜底:通过评论详情反查(仅旧数据兜底)
|
|||
|
|
if (extra_data?.target_id && system_type === 'reply') {
|
|||
|
|
const comment = await commentService.getComment(extra_data.target_id);
|
|||
|
|
if (comment?.post_id) {
|
|||
|
|
return comment.post_id;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleMessagePress = async (message: SystemMessageResponse) => {
|
|||
|
|
try {
|
|||
|
|
const messageId = String(message.id);
|
|||
|
|
const wasUnread = message.is_read !== true;
|
|||
|
|
await messageService.markSystemMessageRead(messageId);
|
|||
|
|
setMessages(prev =>
|
|||
|
|
prev.map(m => (String(m.id) === messageId ? { ...m, is_read: true } : m))
|
|||
|
|
);
|
|||
|
|
if (wasUnread) {
|
|||
|
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
|||
|
|
decrementSystemUnreadCount(1);
|
|||
|
|
}
|
|||
|
|
// 更新本地未读数以及全局 TabBar 红点
|
|||
|
|
fetchUnreadCount();
|
|||
|
|
fetchMessageUnreadCount();
|
|||
|
|
|
|||
|
|
// 根据消息类型处理导航
|
|||
|
|
const { system_type, extra_data } = message;
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
['like_post', 'comment', 'mention', 'reply', 'like_comment', 'like_reply', 'favorite_post'].includes(system_type)
|
|||
|
|
) {
|
|||
|
|
const postId = await resolvePostId(message);
|
|||
|
|
if (postId) {
|
|||
|
|
navigation.navigate('PostDetail', { postId });
|
|||
|
|
}
|
|||
|
|
} else if (system_type === 'follow') {
|
|||
|
|
// 关注 - 跳转到用户主页
|
|||
|
|
if (extra_data?.actor_id_str) {
|
|||
|
|
navigation.navigate('UserProfile', { userId: extra_data.actor_id_str });
|
|||
|
|
}
|
|||
|
|
} else if (system_type === 'group_join_apply') {
|
|||
|
|
navigation.navigate('GroupRequestDetail', { message });
|
|||
|
|
} else if (system_type === 'group_invite') {
|
|||
|
|
navigation.navigate('GroupInviteDetail', { message });
|
|||
|
|
}
|
|||
|
|
// 其他类型暂不处理跳转
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('标记已读失败:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理头像点击 - 跳转到用户主页
|
|||
|
|
const handleAvatarPress = (message: SystemMessageResponse) => {
|
|||
|
|
const { extra_data } = message;
|
|||
|
|
// 优先使用 actor_id_str (UUID格式),兼容 actor_id (数字格式)
|
|||
|
|
const actorId = extra_data?.actor_id_str || (extra_data?.actor_id ? String(extra_data.actor_id) : null);
|
|||
|
|
|
|||
|
|
if (actorId) {
|
|||
|
|
navigation.navigate('UserProfile', { userId: actorId });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染消息项
|
|||
|
|
const renderMessage = ({ item }: { item: SystemMessageResponse }) => (
|
|||
|
|
<SystemMessageItem
|
|||
|
|
message={item}
|
|||
|
|
onPress={() => handleMessagePress(item)}
|
|||
|
|
onAvatarPress={() => handleAvatarPress(item)}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 渲染底部加载指示器
|
|||
|
|
const renderFooter = () => {
|
|||
|
|
if (!loadingMore) return null;
|
|||
|
|
return (
|
|||
|
|
<View style={styles.loadingMore}>
|
|||
|
|
<ActivityIndicator size="small" color={colors.primary.main} />
|
|||
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.loadingMoreText}>
|
|||
|
|
加载中...
|
|||
|
|
</Text>
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染空状态
|
|||
|
|
const renderEmpty = () => (
|
|||
|
|
<EmptyState
|
|||
|
|
title="暂无通知"
|
|||
|
|
description="关注一些用户来获取通知吧"
|
|||
|
|
icon="bell-outline"
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView style={styles.container} edges={[]}>
|
|||
|
|
{isWideScreen ? (
|
|||
|
|
<ResponsiveContainer maxWidth={900}>
|
|||
|
|
{/* 分类筛选 */}
|
|||
|
|
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWide]}>
|
|||
|
|
{MESSAGE_TYPES.map(type => {
|
|||
|
|
const count = type.key === 'all'
|
|||
|
|
? displayMessages.length
|
|||
|
|
: displayMessages.filter((m) => {
|
|||
|
|
if (type.key === 'like_post') {
|
|||
|
|
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
if (type.key === 'group') {
|
|||
|
|
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
return m.system_type === type.key;
|
|||
|
|
}).length;
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={type.key}
|
|||
|
|
style={[
|
|||
|
|
styles.filterTag,
|
|||
|
|
activeType === type.key && styles.filterTagActive,
|
|||
|
|
isWideScreen && styles.filterTagWide,
|
|||
|
|
]}
|
|||
|
|
onPress={() => setActiveType(type.key)}
|
|||
|
|
>
|
|||
|
|
<Text
|
|||
|
|
variant="body"
|
|||
|
|
color={activeType === type.key ? colors.primary.main : colors.text.secondary}
|
|||
|
|
>
|
|||
|
|
{type.title}
|
|||
|
|
</Text>
|
|||
|
|
{count > 0 && (
|
|||
|
|
<Text
|
|||
|
|
variant="caption"
|
|||
|
|
color={activeType === type.key ? colors.primary.main : colors.text.hint}
|
|||
|
|
style={styles.filterCount}
|
|||
|
|
>
|
|||
|
|
{count}
|
|||
|
|
</Text>
|
|||
|
|
)}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 消息列表 */}
|
|||
|
|
{loading ? (
|
|||
|
|
<View style={styles.loadingContainer}>
|
|||
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
) : (
|
|||
|
|
<FlatList
|
|||
|
|
data={filteredMessages}
|
|||
|
|
renderItem={renderMessage}
|
|||
|
|
keyExtractor={item => item.id.toString()}
|
|||
|
|
contentContainerStyle={[styles.listContent, isWideScreen && styles.listContentWide]}
|
|||
|
|
showsVerticalScrollIndicator={false}
|
|||
|
|
ListEmptyComponent={renderEmpty}
|
|||
|
|
ListFooterComponent={renderFooter}
|
|||
|
|
onEndReached={loadMore}
|
|||
|
|
onEndReachedThreshold={0.3}
|
|||
|
|
refreshControl={
|
|||
|
|
<RefreshControl
|
|||
|
|
refreshing={refreshing}
|
|||
|
|
onRefresh={onRefresh}
|
|||
|
|
colors={[colors.primary.main]}
|
|||
|
|
tintColor={colors.primary.main}
|
|||
|
|
/>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</ResponsiveContainer>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
{/* 分类筛选 */}
|
|||
|
|
<View style={styles.filterContainer}>
|
|||
|
|
{MESSAGE_TYPES.map(type => {
|
|||
|
|
const count = type.key === 'all'
|
|||
|
|
? displayMessages.length
|
|||
|
|
: displayMessages.filter((m) => {
|
|||
|
|
if (type.key === 'like_post') {
|
|||
|
|
return LIKE_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
if (type.key === 'group') {
|
|||
|
|
return GROUP_SYSTEM_TYPES.has(m.system_type);
|
|||
|
|
}
|
|||
|
|
return m.system_type === type.key;
|
|||
|
|
}).length;
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={type.key}
|
|||
|
|
style={[
|
|||
|
|
styles.filterTag,
|
|||
|
|
activeType === type.key && styles.filterTagActive,
|
|||
|
|
]}
|
|||
|
|
onPress={() => setActiveType(type.key)}
|
|||
|
|
>
|
|||
|
|
<Text
|
|||
|
|
variant="body"
|
|||
|
|
color={activeType === type.key ? colors.primary.main : colors.text.secondary}
|
|||
|
|
>
|
|||
|
|
{type.title}
|
|||
|
|
</Text>
|
|||
|
|
{count > 0 && (
|
|||
|
|
<Text
|
|||
|
|
variant="caption"
|
|||
|
|
color={activeType === type.key ? colors.primary.main : colors.text.hint}
|
|||
|
|
style={styles.filterCount}
|
|||
|
|
>
|
|||
|
|
{count}
|
|||
|
|
</Text>
|
|||
|
|
)}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{/* 消息列表 */}
|
|||
|
|
{loading ? (
|
|||
|
|
<View style={styles.loadingContainer}>
|
|||
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
) : (
|
|||
|
|
<FlatList
|
|||
|
|
data={filteredMessages}
|
|||
|
|
renderItem={renderMessage}
|
|||
|
|
keyExtractor={item => item.id.toString()}
|
|||
|
|
contentContainerStyle={styles.listContent}
|
|||
|
|
showsVerticalScrollIndicator={false}
|
|||
|
|
ListEmptyComponent={renderEmpty}
|
|||
|
|
ListFooterComponent={renderFooter}
|
|||
|
|
onEndReached={loadMore}
|
|||
|
|
onEndReachedThreshold={0.3}
|
|||
|
|
refreshControl={
|
|||
|
|
<RefreshControl
|
|||
|
|
refreshing={refreshing}
|
|||
|
|
onRefresh={onRefresh}
|
|||
|
|
colors={[colors.primary.main]}
|
|||
|
|
tintColor={colors.primary.main}
|
|||
|
|
/>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
container: {
|
|||
|
|
flex: 1,
|
|||
|
|
backgroundColor: colors.background.default,
|
|||
|
|
},
|
|||
|
|
filterContainer: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
paddingHorizontal: spacing.lg,
|
|||
|
|
paddingVertical: spacing.sm,
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
borderBottomWidth: 1,
|
|||
|
|
borderBottomColor: colors.divider,
|
|||
|
|
},
|
|||
|
|
filterContainerWide: {
|
|||
|
|
paddingHorizontal: spacing.xl,
|
|||
|
|
paddingVertical: spacing.md,
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
},
|
|||
|
|
filterTag: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
paddingHorizontal: spacing.md,
|
|||
|
|
paddingVertical: spacing.sm,
|
|||
|
|
marginRight: spacing.sm,
|
|||
|
|
borderRadius: 16,
|
|||
|
|
backgroundColor: colors.background.default,
|
|||
|
|
},
|
|||
|
|
filterTagWide: {
|
|||
|
|
paddingHorizontal: spacing.lg,
|
|||
|
|
paddingVertical: spacing.md,
|
|||
|
|
marginRight: spacing.md,
|
|||
|
|
},
|
|||
|
|
filterTagActive: {
|
|||
|
|
backgroundColor: colors.primary.light + '30',
|
|||
|
|
},
|
|||
|
|
filterCount: {
|
|||
|
|
marginLeft: spacing.xs,
|
|||
|
|
},
|
|||
|
|
listContent: {
|
|||
|
|
flexGrow: 1,
|
|||
|
|
},
|
|||
|
|
listContentWide: {
|
|||
|
|
paddingHorizontal: spacing.xl,
|
|||
|
|
},
|
|||
|
|
loadingContainer: {
|
|||
|
|
flex: 1,
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
paddingVertical: spacing.xl * 2,
|
|||
|
|
},
|
|||
|
|
loadingMore: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
paddingVertical: spacing.lg,
|
|||
|
|
},
|
|||
|
|
loadingMoreText: {
|
|||
|
|
marginLeft: spacing.sm,
|
|||
|
|
},
|
|||
|
|
});
|