feat: 新增健康档案模块,支持家庭邀请与个人健康数据管理
This commit is contained in:
@@ -9,6 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -20,18 +21,26 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Svg, { Circle, Path } from 'react-native-svg';
|
||||
import Svg, { Circle, Defs, Path, Stop, LinearGradient as SvgLinearGradient } from 'react-native-svg';
|
||||
import { WeightProgressBar } from './WeightProgressBar';
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const CARD_WIDTH = screenWidth - 40; // Subtract left and right margins
|
||||
const CHART_WIDTH = CARD_WIDTH - 36; // Subtract card padding
|
||||
const CHART_HEIGHT = 60;
|
||||
const CARD_WIDTH = screenWidth - 40;
|
||||
const CHART_WIDTH = CARD_WIDTH - 36;
|
||||
const CHART_HEIGHT = 70;
|
||||
const PADDING = 10;
|
||||
|
||||
// 主题色
|
||||
const THEME_PRIMARY = '#4F5BD5';
|
||||
const THEME_SECONDARY = '#6B6CFF';
|
||||
const THEME_SUCCESS = '#22C55E';
|
||||
const THEME_TEXT_PRIMARY = '#1c1f3a';
|
||||
const THEME_TEXT_SECONDARY = '#6f7ba7';
|
||||
|
||||
export function WeightHistoryCard() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const router = useRouter();
|
||||
const userProfile = useAppSelector((s) => s.user.profile);
|
||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||
|
||||
@@ -44,7 +53,6 @@ export function WeightHistoryCard() {
|
||||
|
||||
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
loadWeightHistory();
|
||||
@@ -59,7 +67,8 @@ export function WeightHistoryCard() {
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToCoach = () => {
|
||||
// 点击添加按钮 - 需要登录
|
||||
const handleAddWeight = () => {
|
||||
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
|
||||
};
|
||||
|
||||
@@ -67,85 +76,97 @@ export function WeightHistoryCard() {
|
||||
setShowBMIModal(false);
|
||||
};
|
||||
|
||||
// 点击卡片 - 直接跳转,不需要登录
|
||||
const navigateToWeightRecords = () => {
|
||||
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
|
||||
router.push(ROUTES.WEIGHT_RECORDS);
|
||||
};
|
||||
|
||||
|
||||
// Process weight history data
|
||||
const sortedHistory = [...weightHistory]
|
||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
.slice(-7); // Show only the last 7 records
|
||||
.slice(-7);
|
||||
|
||||
// return (
|
||||
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||
// <View style={styles.cardHeader}>
|
||||
// <Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||
// </View>
|
||||
// 是否有数据
|
||||
const hasData = sortedHistory.length > 0;
|
||||
|
||||
// <View style={styles.emptyContent}>
|
||||
// <Text style={styles.emptyDescription}>
|
||||
// No weight records yet, click the button below to start recording
|
||||
// </Text>
|
||||
// <TouchableOpacity
|
||||
// style={styles.recordButton}
|
||||
// onPress={(e) => {
|
||||
// e.stopPropagation();
|
||||
// navigateToCoach();
|
||||
// }}
|
||||
// activeOpacity={0.8}
|
||||
// >
|
||||
// <Ionicons name="add" size={18} color="#FFFFFF" />
|
||||
// <Text style={styles.recordButtonText}>{t('statistics.components.weight.addButton')}</Text>
|
||||
// </TouchableOpacity>
|
||||
// </View>
|
||||
// </TouchableOpacity>
|
||||
// );
|
||||
// }
|
||||
// 计算减重进度
|
||||
const currentWeight = userProfile?.weight ? parseFloat(userProfile.weight) : 0;
|
||||
const initialWeight = userProfile?.initialWeight
|
||||
? parseFloat(userProfile.initialWeight)
|
||||
: (sortedHistory.length > 0 ? parseFloat(sortedHistory[0].weight) : 0);
|
||||
const targetWeight = userProfile?.targetWeight ? parseFloat(userProfile.targetWeight) : 0;
|
||||
|
||||
// 计算进度百分比
|
||||
const hasTargetWeight = targetWeight > 0 && initialWeight > targetWeight;
|
||||
const totalToLose = initialWeight - targetWeight;
|
||||
const actualLost = initialWeight - currentWeight;
|
||||
const weightProgress = hasTargetWeight && totalToLose > 0 ? actualLost / totalToLose : 0;
|
||||
|
||||
// Generate chart data
|
||||
const weights = sortedHistory.map(item => parseFloat(item.weight));
|
||||
const minWeight = Math.min(...weights);
|
||||
const maxWeight = Math.max(...weights);
|
||||
const weights = hasData ? sortedHistory.map(item => parseFloat(item.weight)) : [];
|
||||
const minWeight = hasData ? Math.min(...weights) : 0;
|
||||
const maxWeight = hasData ? Math.max(...weights) : 0;
|
||||
const weightRange = maxWeight - minWeight || 1;
|
||||
|
||||
const points = sortedHistory.map((item, index) => {
|
||||
const points = hasData ? sortedHistory.map((item, index) => {
|
||||
const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING);
|
||||
const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange;
|
||||
// Reduce top margin, compress whitespace
|
||||
const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16);
|
||||
const y = PADDING + 10 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 20);
|
||||
return { x, y, weight: item.weight, date: item.createdAt };
|
||||
});
|
||||
}) : [];
|
||||
|
||||
// Generate path
|
||||
const pathData = points.map((point, index) => {
|
||||
if (index === 0) return `M ${point.x} ${point.y}`;
|
||||
return `L ${point.x} ${point.y}`;
|
||||
}).join(' ');
|
||||
// 生成平滑曲线路径(使用贝塞尔曲线)
|
||||
const generateSmoothPath = (pts: typeof points) => {
|
||||
if (pts.length === 0) return '';
|
||||
if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
|
||||
|
||||
// If there's only one data point, display as a horizontal line
|
||||
const singlePointPath = points.length === 1 ?
|
||||
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
|
||||
pathData;
|
||||
let path = `M ${pts[0].x} ${pts[0].y}`;
|
||||
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = pts[Math.max(0, i - 1)];
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[Math.min(pts.length - 1, i + 2)];
|
||||
|
||||
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
||||
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
||||
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
||||
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
||||
|
||||
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const smoothPath = generateSmoothPath(points);
|
||||
const singlePointPath = points.length === 1
|
||||
? `M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}`
|
||||
: smoothPath;
|
||||
|
||||
// 空状态下的占位曲线路径(水平虚线效果)
|
||||
const emptyLinePath = `M ${PADDING} ${CHART_HEIGHT / 2} L ${CHART_WIDTH - PADDING} ${CHART_HEIGHT / 2}`;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/icon-weight.png')}
|
||||
style={styles.iconSquare}
|
||||
/>
|
||||
<View style={styles.iconContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/icons/icon-weight.png')}
|
||||
style={styles.iconSquare}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.cardTitle}>{t('statistics.components.weight.title')}</Text>
|
||||
{isLgAvaliable ? (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToCoach();
|
||||
handleAddWeight();
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<GlassView style={styles.addButtonGlass}>
|
||||
<Ionicons name="add" size={18} color={Colors.light.primary} />
|
||||
<Ionicons name="add" size={18} color={THEME_PRIMARY} />
|
||||
</GlassView>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
@@ -153,68 +174,125 @@ export function WeightHistoryCard() {
|
||||
style={styles.addButton}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToCoach();
|
||||
handleAddWeight();
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="add" size={18} color={Colors.light.primary} />
|
||||
<Ionicons name="add" size={18} color={THEME_PRIMARY} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Default chart display */}
|
||||
{sortedHistory.length > 0 && (
|
||||
<View style={styles.chartContainer}>
|
||||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||||
{/* Background grid lines */}
|
||||
{/* 当前体重显示 */}
|
||||
<View style={styles.currentWeightSection}>
|
||||
<View style={styles.weightValueContainer}>
|
||||
<Text style={styles.weightValue}>{hasWeight ? currentWeight.toFixed(1) : '--'}</Text>
|
||||
<Text style={styles.weightUnit}>kg</Text>
|
||||
</View>
|
||||
{sortedHistory.length > 1 && (
|
||||
<View style={[
|
||||
styles.changeTag,
|
||||
{ backgroundColor: actualLost >= 0 ? 'rgba(34, 197, 94, 0.1)' : 'rgba(255, 107, 107, 0.1)' }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={actualLost >= 0 ? 'trending-down' : 'trending-up'}
|
||||
size={12}
|
||||
color={actualLost >= 0 ? THEME_SUCCESS : '#FF6B6B'}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.changeText,
|
||||
{ color: actualLost >= 0 ? THEME_SUCCESS : '#FF6B6B' }
|
||||
]}>
|
||||
{actualLost >= 0 ? '-' : '+'}{Math.abs(actualLost).toFixed(1)}kg
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* More abstract line - reduce line width and display details */}
|
||||
{/* 图表显示 */}
|
||||
<View style={styles.chartContainer}>
|
||||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||||
<Defs>
|
||||
<SvgLinearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<Stop offset="0%" stopColor={THEME_PRIMARY} stopOpacity="1" />
|
||||
<Stop offset="100%" stopColor={THEME_SECONDARY} stopOpacity="1" />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
|
||||
{hasData ? (
|
||||
<>
|
||||
{/* 平滑曲线 */}
|
||||
<Path
|
||||
d={singlePointPath}
|
||||
stroke="url(#lineGradient)"
|
||||
strokeWidth={2.5}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* 数据点 */}
|
||||
{points.map((point, index) => {
|
||||
const isLastPoint = index === points.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{/* 外圈光晕 */}
|
||||
{isLastPoint && (
|
||||
<Circle
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r={8}
|
||||
fill={THEME_PRIMARY}
|
||||
opacity={0.15}
|
||||
/>
|
||||
)}
|
||||
{/* 数据点 */}
|
||||
<Circle
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r={isLastPoint ? 4 : 2.5}
|
||||
fill={isLastPoint ? THEME_PRIMARY : THEME_SECONDARY}
|
||||
stroke={isLastPoint ? '#ffffff' : 'none'}
|
||||
strokeWidth={isLastPoint ? 2 : 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
/* 空状态 - 虚线占位 */
|
||||
<Path
|
||||
d={singlePointPath}
|
||||
stroke={Colors.light.accentGreen}
|
||||
d={emptyLinePath}
|
||||
stroke="#E8EAF0"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
opacity={0.8}
|
||||
strokeDasharray="8,6"
|
||||
/>
|
||||
)}
|
||||
</Svg>
|
||||
|
||||
{/* Simplified data points - smaller and more refined */}
|
||||
{points.map((point, index) => {
|
||||
const isLastPoint = index === points.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Circle
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r={isLastPoint ? 3 : 2}
|
||||
fill={Colors.light.accentGreen}
|
||||
opacity={0.9}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
</Svg>
|
||||
|
||||
{/* Concise chart information */}
|
||||
<View style={styles.chartInfo}>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>{sortedHistory.length}{t('statistics.components.weight.days')}</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>
|
||||
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
|
||||
</Text>
|
||||
</View>
|
||||
{/* 图表信息 */}
|
||||
<View style={styles.chartInfo}>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>{hasData ? sortedHistory.length : '--'}{t('statistics.components.weight.days')}</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>
|
||||
{hasData ? `${minWeight.toFixed(1)}-${maxWeight.toFixed(1)}kg` : '--'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 减重进度条 - 始终显示 */}
|
||||
<WeightProgressBar
|
||||
progress={weightProgress}
|
||||
currentWeight={currentWeight}
|
||||
targetWeight={targetWeight}
|
||||
initialWeight={initialWeight}
|
||||
/>
|
||||
|
||||
{/* BMI information modal */}
|
||||
<Modal
|
||||
@@ -323,32 +401,38 @@ export function WeightHistoryCard() {
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 22,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
marginTop: 16
|
||||
borderRadius: 24,
|
||||
padding: 18,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
marginTop: 16,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconSquare: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
iconContainer: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(79, 91, 213, 0.1)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 4,
|
||||
marginRight: 10,
|
||||
},
|
||||
iconSquare: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
tintColor: THEME_PRIMARY,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 14,
|
||||
color: '#192126',
|
||||
fontSize: 15,
|
||||
color: THEME_TEXT_PRIMARY,
|
||||
flex: 1,
|
||||
fontWeight: '600',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
headerButtons: {
|
||||
@@ -364,19 +448,56 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
addButton: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(79, 91, 213, 0.1)',
|
||||
},
|
||||
addButtonGlass: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(147, 112, 219, 0.3)',
|
||||
backgroundColor: 'rgba(79, 91, 213, 0.15)',
|
||||
},
|
||||
currentWeightSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 12,
|
||||
gap: 12,
|
||||
},
|
||||
weightValueContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
weightValue: {
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
color: THEME_TEXT_PRIMARY,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
weightUnit: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: THEME_TEXT_SECONDARY,
|
||||
fontFamily: 'AliRegular',
|
||||
marginLeft: 4,
|
||||
},
|
||||
changeTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
gap: 4,
|
||||
},
|
||||
changeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
@@ -384,12 +505,12 @@ const styles = StyleSheet.create({
|
||||
emptyTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
color: THEME_TEXT_PRIMARY,
|
||||
marginBottom: 6,
|
||||
},
|
||||
emptyDescription: {
|
||||
fontSize: 14,
|
||||
color: '#687076',
|
||||
color: THEME_TEXT_SECONDARY,
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
lineHeight: 20,
|
||||
@@ -397,14 +518,14 @@ const styles = StyleSheet.create({
|
||||
recordButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: Colors.light.accentGreen,
|
||||
backgroundColor: THEME_PRIMARY,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
recordButtonText: {
|
||||
color: '#192126',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
@@ -418,20 +539,25 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
width: '100%',
|
||||
marginTop: -14,
|
||||
},
|
||||
infoItem: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(79, 91, 213, 0.06)',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 10,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 11,
|
||||
color: '#687076',
|
||||
color: THEME_TEXT_SECONDARY,
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
color: THEME_TEXT_PRIMARY,
|
||||
},
|
||||
|
||||
// BMI modal styles
|
||||
@@ -556,7 +682,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
bmiModalButtonBackground: {
|
||||
backgroundColor: '#192126',
|
||||
backgroundColor: THEME_TEXT_PRIMARY,
|
||||
borderRadius: 16,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
|
||||
Reference in New Issue
Block a user