feat: 更新文章功能和相关依赖
- 新增文章详情页面,支持根据文章 ID 加载和展示文章内容 - 添加文章卡片组件,展示推荐文章的标题、封面和阅读量 - 更新文章服务,支持获取文章列表和根据 ID 获取文章详情 - 集成腾讯云 COS SDK,支持文件上传功能 - 优化打卡功能,支持按日期加载和展示打卡记录 - 更新相关依赖,确保项目兼容性和功能完整性 - 调整样式以适应新功能的展示和交互
This commit is contained in:
@@ -3,12 +3,13 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import type { CheckinExercise } from '@/store/checkinSlice';
|
||||
import { getDailyCheckins, loadMonthCheckins, removeExercise, setCurrentDate, syncCheckin, toggleExerciseCompleted } from '@/store/checkinSlice';
|
||||
import { getDailyCheckins, removeExercise, replaceExercises, setCurrentDate, syncCheckin, toggleExerciseCompleted } from '@/store/checkinSlice';
|
||||
import { buildClassicalSession } from '@/utils/classicalSession';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Alert, FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, FlatList, Modal, SafeAreaView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
function formatDate(d: Date) {
|
||||
const y = d.getFullYear();
|
||||
@@ -20,33 +21,65 @@ function formatDate(d: Date) {
|
||||
export default function CheckinHome() {
|
||||
const dispatch = useAppDispatch();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams<{ date?: string }>();
|
||||
const today = useMemo(() => formatDate(new Date()), []);
|
||||
const checkin = useAppSelector((s) => (s as any).checkin);
|
||||
const record = checkin?.byDate?.[today];
|
||||
const routeDateParam = typeof params?.date === 'string' && params.date ? params.date : undefined;
|
||||
const currentDate: string = routeDateParam || (checkin?.currentDate as string) || today;
|
||||
const record = checkin?.byDate?.[currentDate] as (undefined | { items?: CheckinExercise[]; note?: string; raw?: any[] });
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setCurrentDate(today));
|
||||
// 进入页面立即从后端获取当天打卡列表,回填本地
|
||||
dispatch(getDailyCheckins(today)).unwrap().catch((err: any) => {
|
||||
Alert.alert('获取打卡失败', err?.message || '请稍后重试');
|
||||
});
|
||||
// 预取本月数据(用于日历视图点亮)
|
||||
const now = new Date();
|
||||
dispatch(loadMonthCheckins({ year: now.getFullYear(), month1Based: now.getMonth() + 1 }));
|
||||
}, [dispatch, today]);
|
||||
console.log('CheckinHome render', {
|
||||
currentDate,
|
||||
routeDateParam,
|
||||
itemsCount: record?.items?.length || 0,
|
||||
rawCount: (record as any)?.raw?.length || 0,
|
||||
});
|
||||
|
||||
const lastFetchedRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
// 初始化当前日期:路由参数优先,其次 store,最后今天
|
||||
if (currentDate && checkin?.currentDate !== currentDate) {
|
||||
dispatch(setCurrentDate(currentDate));
|
||||
}
|
||||
// 仅当切换日期时获取一次,避免重复请求
|
||||
if (currentDate && lastFetchedRef.current !== currentDate) {
|
||||
lastFetchedRef.current = currentDate;
|
||||
dispatch(getDailyCheckins(currentDate)).unwrap().catch((err: any) => {
|
||||
Alert.alert('获取打卡失败', err?.message || '请稍后重试');
|
||||
});
|
||||
}
|
||||
}, [dispatch, currentDate]);
|
||||
|
||||
const lastSyncSigRef = useRef<string>('');
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
// 返回本页时确保与后端同步(若本地有内容则上报,后台 upsert)
|
||||
if (record?.items && Array.isArray(record.items)) {
|
||||
dispatch(syncCheckin({ date: today, items: record.items as CheckinExercise[], note: record?.note }));
|
||||
// 仅当本地条目发生变更时才上报,避免反复刷写
|
||||
const sig = JSON.stringify(record?.items || []);
|
||||
if (record?.items && Array.isArray(record.items) && sig !== lastSyncSigRef.current) {
|
||||
lastSyncSigRef.current = sig;
|
||||
dispatch(syncCheckin({ date: currentDate, items: record.items as CheckinExercise[], note: record?.note }));
|
||||
}
|
||||
return () => { };
|
||||
}, [dispatch, today, record?.items])
|
||||
}, [dispatch, currentDate, record?.items, record?.note])
|
||||
);
|
||||
|
||||
const [genVisible, setGenVisible] = useState(false);
|
||||
const [genLevel, setGenLevel] = useState<'beginner' | 'intermediate' | 'advanced'>('beginner');
|
||||
const [genWithRests, setGenWithRests] = useState(true);
|
||||
const [genWithNotes, setGenWithNotes] = useState(true);
|
||||
const [genRest, setGenRest] = useState('30');
|
||||
|
||||
const onGenerate = () => {
|
||||
const restSec = Math.max(10, Math.min(120, parseInt(genRest || '30', 10)));
|
||||
const { items, note } = buildClassicalSession({ withSectionRests: genWithRests, restSeconds: restSec, withNotes: genWithNotes, level: genLevel });
|
||||
dispatch(replaceExercises({ date: currentDate, items, note }));
|
||||
dispatch(syncCheckin({ date: currentDate, items, note }));
|
||||
setGenVisible(false);
|
||||
Alert.alert('排课已生成', '已为你生成经典普拉提序列,可继续调整。');
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||
@@ -57,73 +90,185 @@ export default function CheckinHome() {
|
||||
|
||||
<HeaderBar title="每日打卡" onBack={() => router.back()} withSafeTop={false} transparent />
|
||||
<View style={[styles.hero, { backgroundColor: colorTokens.heroSurfaceTint }]}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>{today}</Text>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>{currentDate}</Text>
|
||||
<Text style={[styles.subtitle, { color: colorTokens.textMuted }]}>请选择动作并记录完成情况</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]} onPress={() => router.push('/checkin/select')}>
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]}
|
||||
onPress={() => router.push({ pathname: '/checkin/select', params: { date: currentDate } })}
|
||||
>
|
||||
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}>新增动作</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{ height: 10 }} />
|
||||
<TouchableOpacity
|
||||
style={[styles.secondaryBtn, { borderColor: colorTokens.primary }]}
|
||||
onPress={() => setGenVisible(true)}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colorTokens.primary }]}>一键排课(经典序列)</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={record?.items || []}
|
||||
keyExtractor={(item) => item.key}
|
||||
data={(record?.items && record.items.length > 0)
|
||||
? record.items
|
||||
: ((record as any)?.raw || [])}
|
||||
keyExtractor={(item, index) => (item?.key || item?.id || `${currentDate}_${index}`)}
|
||||
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }}
|
||||
ListEmptyComponent={
|
||||
<View style={[styles.emptyBox, { backgroundColor: colorTokens.card }]}>
|
||||
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}>还没有选择任何动作,点击“新增动作”开始吧。</Text>
|
||||
</View>
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<View style={[styles.card, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{item.name}</Text>
|
||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{item.category}</Text>
|
||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>组数 {item.sets}{item.reps ? ` · 每组 ${item.reps} 次` : ''}{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={item.completed ? '已完成,点击取消完成' : '未完成,点击标记完成'}
|
||||
style={styles.doneIconBtn}
|
||||
onPress={() => {
|
||||
dispatch(toggleExerciseCompleted({ date: today, key: item.key }));
|
||||
const nextItems: CheckinExercise[] = (record?.items || []).map((it: CheckinExercise) =>
|
||||
it.key === item.key ? { ...it, completed: !it.completed } : it
|
||||
);
|
||||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
||||
}}
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
|
||||
size={24}
|
||||
color={item.completed ? colorTokens.primary : colorTokens.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.removeBtn, { backgroundColor: colorTokens.border }]}
|
||||
onPress={() =>
|
||||
Alert.alert('确认移除', '确定要移除该动作吗?', [
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '移除',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
dispatch(removeExercise({ date: today, key: item.key }));
|
||||
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== item.key);
|
||||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
||||
renderItem={({ item }) => {
|
||||
// 若为后端原始项(无 key),以标题/时间为卡片,禁用交互
|
||||
const isRaw = !item?.key;
|
||||
if (isRaw) {
|
||||
const title = item?.title || '每日训练打卡';
|
||||
const status = item?.status || '';
|
||||
const startedAt = item?.startedAt ? new Date(item.startedAt).toLocaleString() : '';
|
||||
return (
|
||||
<View style={[styles.card, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{title}</Text>
|
||||
{!!status && <Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{status}</Text>}
|
||||
{!!startedAt && <Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{startedAt}</Text>}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const exercise = item as CheckinExercise;
|
||||
const type = exercise.itemType ?? 'exercise';
|
||||
const isRest = type === 'rest';
|
||||
const isNote = type === 'note';
|
||||
const cardStyle = [styles.card, { backgroundColor: colorTokens.card }];
|
||||
if (isRest || isNote) {
|
||||
return (
|
||||
<View style={styles.inlineRow}>
|
||||
<Ionicons name={isRest ? 'time-outline' : 'information-circle-outline'} size={14} color={colorTokens.textMuted} />
|
||||
<View style={[styles.inlineBadge, isRest ? styles.inlineBadgeRest : styles.inlineBadgeNote, { borderColor: colorTokens.border }]}>
|
||||
<Text style={[isNote ? styles.inlineTextItalic : styles.inlineText, { color: colorTokens.textMuted }]}>
|
||||
{isRest ? `间隔休息 ${exercise.restSec ?? 30}s` : (exercise.note || '提示')}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.inlineRemoveBtn}
|
||||
onPress={() =>
|
||||
Alert.alert('确认移除', '确定要移除该条目吗?', [
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '移除',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
dispatch(removeExercise({ date: currentDate, key: exercise.key }));
|
||||
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== exercise.key);
|
||||
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Ionicons name="close-outline" size={16} color={colorTokens.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={cardStyle as any}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{exercise.name}</Text>
|
||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{exercise.category}</Text>
|
||||
{isNote && (
|
||||
<Text style={[styles.cardMetaItalic, { color: colorTokens.textMuted }]}>{exercise.note || '提示'}</Text>
|
||||
)}
|
||||
{!isNote && (
|
||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>
|
||||
{isRest
|
||||
? `建议休息 ${exercise.restSec ?? 30}s`
|
||||
: `组数 ${exercise.sets}${exercise.reps ? ` · 每组 ${exercise.reps} 次` : ''}${exercise.durationSec ? ` · 每组 ${exercise.durationSec}s` : ''}`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{type === 'exercise' && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={exercise.completed ? '已完成,点击取消完成' : '未完成,点击标记完成'}
|
||||
style={styles.doneIconBtn}
|
||||
onPress={() => {
|
||||
dispatch(toggleExerciseCompleted({ date: currentDate, key: exercise.key }));
|
||||
const nextItems: CheckinExercise[] = (record?.items || []).map((it: CheckinExercise) =>
|
||||
it.key === exercise.key ? { ...it, completed: !it.completed } : it
|
||||
);
|
||||
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||
}}
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={exercise.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
|
||||
size={24}
|
||||
color={exercise.completed ? colorTokens.primary : colorTokens.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.removeBtn, { backgroundColor: colorTokens.border }]}
|
||||
onPress={() =>
|
||||
Alert.alert('确认移除', '确定要移除该动作吗?', [
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '移除',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
dispatch(removeExercise({ date: currentDate, key: exercise.key }));
|
||||
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== exercise.key);
|
||||
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
<Text style={[styles.removeBtnText, { color: colorTokens.text }]}>移除</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
])
|
||||
}
|
||||
>
|
||||
<Text style={[styles.removeBtnText, { color: colorTokens.text }]}>移除</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* 生成配置弹窗 */}
|
||||
<Modal visible={genVisible} transparent animationType="fade" onRequestClose={() => setGenVisible(false)}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setGenVisible(false)}>
|
||||
<TouchableOpacity activeOpacity={1} style={[styles.modalSheet, { backgroundColor: colorTokens.card }]} onPress={(e) => e.stopPropagation() as any}>
|
||||
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>经典排课配置</Text>
|
||||
<Text style={[styles.modalLabel, { color: colorTokens.textMuted }]}>强度水平</Text>
|
||||
<View style={styles.segmentedRow}>
|
||||
{(['beginner', 'intermediate', 'advanced'] as const).map((lv) => (
|
||||
<TouchableOpacity key={lv} style={[styles.segment, genLevel === lv && { backgroundColor: colorTokens.primary }]} onPress={() => setGenLevel(lv)}>
|
||||
<Text style={[styles.segmentText, genLevel === lv && { color: colorTokens.onPrimary }]}>
|
||||
{lv === 'beginner' ? '入门' : lv === 'intermediate' ? '进阶' : '高级'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.switchRow}>
|
||||
<Text style={[styles.switchLabel, { color: colorTokens.text }]}>段间休息</Text>
|
||||
<Switch value={genWithRests} onValueChange={setGenWithRests} />
|
||||
</View>
|
||||
<View style={styles.switchRow}>
|
||||
<Text style={[styles.switchLabel, { color: colorTokens.text }]}>插入操作提示</Text>
|
||||
<Switch value={genWithNotes} onValueChange={setGenWithNotes} />
|
||||
</View>
|
||||
<View style={styles.inputRow}>
|
||||
<Text style={[styles.switchLabel, { color: colorTokens.textMuted }]}>休息秒数</Text>
|
||||
<TextInput value={genRest} onChangeText={setGenRest} keyboardType="number-pad" style={[styles.input, { borderColor: colorTokens.border, color: colorTokens.text }]} />
|
||||
</View>
|
||||
<View style={{ height: 8 }} />
|
||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]} onPress={onGenerate}>
|
||||
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}>生成今日计划</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -145,14 +290,35 @@ const styles = StyleSheet.create({
|
||||
actionRow: { paddingHorizontal: 20, marginTop: 8 },
|
||||
primaryBtn: { backgroundColor: '#111827', paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
|
||||
primaryBtnText: { color: '#FFFFFF', fontWeight: '800' },
|
||||
secondaryBtn: { borderWidth: 2, paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
|
||||
secondaryBtnText: { fontWeight: '800' },
|
||||
emptyBox: { marginTop: 16, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginHorizontal: 0 },
|
||||
emptyText: { color: '#6B7280' },
|
||||
card: { marginTop: 12, marginHorizontal: 0, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3 },
|
||||
cardTitle: { fontSize: 16, fontWeight: '800', color: '#111827' },
|
||||
cardMeta: { marginTop: 4, fontSize: 12, color: '#6B7280' },
|
||||
cardMetaItalic: { marginTop: 4, fontSize: 12, color: '#6B7280', fontStyle: 'italic' },
|
||||
removeBtn: { backgroundColor: '#F3F4F6', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 },
|
||||
removeBtnText: { color: '#111827', fontWeight: '700' },
|
||||
doneIconBtn: { paddingHorizontal: 4, paddingVertical: 4, borderRadius: 16, marginRight: 8 },
|
||||
inlineRow: { marginTop: 10, marginHorizontal: 20, flexDirection: 'row', alignItems: 'center' },
|
||||
inlineBadge: { marginLeft: 6, borderWidth: 1, borderRadius: 999, paddingVertical: 6, paddingHorizontal: 10 },
|
||||
inlineBadgeRest: { backgroundColor: '#F8FAFC' },
|
||||
inlineBadgeNote: { backgroundColor: '#F9FAFB' },
|
||||
inlineText: { fontSize: 12, fontWeight: '700' },
|
||||
inlineTextItalic: { fontSize: 12, fontStyle: 'italic' },
|
||||
inlineRemoveBtn: { marginLeft: 6, padding: 4, borderRadius: 999 },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.35)', alignItems: 'center', justifyContent: 'flex-end' },
|
||||
modalSheet: { width: '100%', borderTopLeftRadius: 16, borderTopRightRadius: 16, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 24 },
|
||||
modalTitle: { fontSize: 16, fontWeight: '800', marginBottom: 8 },
|
||||
modalLabel: { fontSize: 12, marginBottom: 6 },
|
||||
segmentedRow: { flexDirection: 'row', gap: 8, marginBottom: 8 },
|
||||
segment: { flex: 1, borderRadius: 999, borderWidth: 1, borderColor: '#E5E7EB', paddingVertical: 8, alignItems: 'center' },
|
||||
segmentText: { fontWeight: '700' },
|
||||
switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 8 },
|
||||
switchLabel: { fontWeight: '700' },
|
||||
inputRow: { marginTop: 8 },
|
||||
input: { height: 40, borderWidth: 1, borderRadius: 10, paddingHorizontal: 12 },
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user