- Introduced a new service for managing notification preferences, including push notifications, sound, and vibration settings. - Updated the notification handling logic to respect user preferences, ensuring notifications are displayed according to user settings. - Refactored the App and various screens to integrate the new notification preferences, improving user experience and consistency. - Enhanced the HomeScreen and NotificationSettingsScreen to load and update notification settings seamlessly. - Implemented a mechanism to hide the bottom tab bar based on scroll events, improving navigation usability.
709 lines
21 KiB
TypeScript
709 lines
21 KiB
TypeScript
/**
|
||
* PostCard 主组件
|
||
* 帖子卡片组件(新实现)
|
||
*
|
||
* @example
|
||
* // 新 API 使用方式
|
||
* <PostCard
|
||
* post={post}
|
||
* onAction={(action) => handleAction(post, action)}
|
||
* variant="list"
|
||
* features="full"
|
||
* />
|
||
*/
|
||
|
||
import React, { useMemo, useState, memo, useEffect } from 'react';
|
||
import { View, TouchableOpacity, StyleSheet, Alert, TextStyle } from 'react-native';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { PostCardProps, PostCardAction } from './types';
|
||
import { ImageGridItem, SmartImage } from '../../common';
|
||
import Text from '../../common/Text';
|
||
import Avatar from '../../common/Avatar';
|
||
import { colors, spacing, borderRadius, fontSizes } from '../../../theme';
|
||
import { getPreviewImageUrl } from '../../../utils/imageHelper';
|
||
import PostImages from './components/PostImages';
|
||
import { useResponsive } from '../../../hooks/useResponsive';
|
||
import { formatPostCreatedAtString } from '../../../core/entities/Post';
|
||
|
||
/** 列表相对时间:独立定时刷新,避免外层 PostCard.memo 挡住「分钟前」更新 */
|
||
const PostCardRelativeTime: React.FC<{ createdAt?: string | null; style?: TextStyle | TextStyle[] }> = ({
|
||
createdAt,
|
||
style: timeStyle,
|
||
}) => {
|
||
const [label, setLabel] = useState(() => formatPostCreatedAtString(createdAt));
|
||
|
||
useEffect(() => {
|
||
setLabel(formatPostCreatedAtString(createdAt));
|
||
const tick = () => setLabel(formatPostCreatedAtString(createdAt));
|
||
const id = setInterval(tick, 60_000);
|
||
return () => clearInterval(id);
|
||
}, [createdAt]);
|
||
|
||
return (
|
||
<Text style={timeStyle} numberOfLines={1}>
|
||
{label}
|
||
</Text>
|
||
);
|
||
};
|
||
|
||
function imagesSignature(
|
||
images: PostCardProps['post']['images'] | undefined
|
||
): string {
|
||
if (!images?.length) return '';
|
||
return images.map((i) => i.id).join('\u001f');
|
||
}
|
||
|
||
function authorSignature(author: PostCardProps['post']['author']): string {
|
||
if (!author) return '';
|
||
return [author.id, author.nickname ?? '', author.avatar ?? ''].join('\u001f');
|
||
}
|
||
|
||
function topCommentSignature(tc: PostCardProps['post']['top_comment']): string {
|
||
if (!tc) return '';
|
||
return [tc.id, tc.content ?? '', tc.author?.id ?? ''].join('\u001f');
|
||
}
|
||
|
||
function channelSignature(ch: PostCardProps['post']['channel']): string {
|
||
if (!ch) return '';
|
||
return [ch.id, ch.name ?? ''].join('\u001f');
|
||
}
|
||
|
||
function featuresComparable(f: PostCardProps['features']): string {
|
||
if (f == null) return '';
|
||
if (typeof f === 'string') return f;
|
||
return JSON.stringify(f);
|
||
}
|
||
|
||
/**
|
||
* 自定义比较:忽略 onAction 引用(父组件应用 postId + ref 解析最新 post,避免 Tab 切换时全列表因回调重建而重绘)。
|
||
* index 未参与 UI,不比较以免列表重排时多余更新。
|
||
*/
|
||
function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolean {
|
||
if (prev.variant !== next.variant) return false;
|
||
if (prev.isPostAuthor !== next.isPostAuthor) return false;
|
||
if (prev.isAuthor !== next.isAuthor) return false;
|
||
if (prev.style !== next.style) return false;
|
||
if (featuresComparable(prev.features) !== featuresComparable(next.features)) return false;
|
||
|
||
const a = prev.post;
|
||
const b = next.post;
|
||
if (a === b) return true;
|
||
|
||
if (a.id !== b.id) return false;
|
||
if (a.title !== b.title) return false;
|
||
if (a.content !== b.content) return false;
|
||
if ((a.likes_count ?? 0) !== (b.likes_count ?? 0)) return false;
|
||
if ((a.comments_count ?? 0) !== (b.comments_count ?? 0)) return false;
|
||
if ((a.favorites_count ?? 0) !== (b.favorites_count ?? 0)) return false;
|
||
if ((a.shares_count ?? 0) !== (b.shares_count ?? 0)) return false;
|
||
if ((a.views_count ?? 0) !== (b.views_count ?? 0)) return false;
|
||
if (!!a.is_pinned !== !!b.is_pinned) return false;
|
||
if (!!a.is_locked !== !!b.is_locked) return false;
|
||
if (!!a.is_vote !== !!b.is_vote) return false;
|
||
if (a.created_at !== b.created_at) return false;
|
||
if (a.updated_at !== b.updated_at) return false;
|
||
if (a.content_edited_at !== b.content_edited_at) return false;
|
||
if (!!a.is_liked !== !!b.is_liked) return false;
|
||
if (!!a.is_favorited !== !!b.is_favorited) return false;
|
||
if (authorSignature(a.author) !== authorSignature(b.author)) return false;
|
||
if (imagesSignature(a.images) !== imagesSignature(b.images)) return false;
|
||
if (topCommentSignature(a.top_comment) !== topCommentSignature(b.top_comment)) return false;
|
||
if (channelSignature(a.channel) !== channelSignature(b.channel)) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* PostCard 主组件
|
||
* 仅支持新 API
|
||
*/
|
||
const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||
const {
|
||
post,
|
||
onAction,
|
||
variant = 'list',
|
||
features,
|
||
isPostAuthor = false,
|
||
isAuthor = false,
|
||
index,
|
||
style,
|
||
} = normalizedProps;
|
||
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
|
||
const isCompact =
|
||
features === 'compact' ||
|
||
(typeof features === 'object' && features?.showContent === false);
|
||
|
||
const emit = (action: PostCardAction) => {
|
||
onAction(action);
|
||
};
|
||
|
||
const handleImagePress = (images: ImageGridItem[], imageIndex: number) => {
|
||
emit({ type: 'imagePress', payload: { images, imageIndex } });
|
||
};
|
||
|
||
const showGrid = variant === 'grid';
|
||
const author = post.author;
|
||
const rawContent = post.content ?? '';
|
||
const { isDesktop, isTablet, isWideScreen, isLandscape } = useResponsive();
|
||
|
||
const handleCardPress = () => emit({ type: 'press' });
|
||
const handleUserPress = () => emit({ type: 'userPress' });
|
||
const handleLike = () => emit({ type: post.is_liked ? 'unlike' : 'like' });
|
||
const handleComment = () => emit({ type: 'comment' });
|
||
const handleBookmark = () => emit({ type: post.is_favorited ? 'unbookmark' : 'bookmark' });
|
||
const handleShare = () => emit({ type: 'share' });
|
||
const handleDelete = () => {
|
||
if (isDeleting) return;
|
||
Alert.alert(
|
||
'删除帖子',
|
||
'确定要删除这篇帖子吗?删除后将无法恢复。',
|
||
[
|
||
{ text: '取消', style: 'cancel' },
|
||
{
|
||
text: '删除',
|
||
style: 'destructive',
|
||
onPress: async () => {
|
||
setIsDeleting(true);
|
||
try {
|
||
emit({ type: 'delete' });
|
||
} finally {
|
||
setIsDeleting(false);
|
||
}
|
||
},
|
||
},
|
||
],
|
||
);
|
||
};
|
||
|
||
const images: ImageGridItem[] = Array.isArray(post.images)
|
||
? post.images.map((img) => ({
|
||
id: img.id,
|
||
url: img.url,
|
||
thumbnail_url: img.thumbnail_url,
|
||
preview_url: img.preview_url,
|
||
preview_url_large: img.preview_url_large,
|
||
width: img.width,
|
||
height: img.height,
|
||
}))
|
||
: [];
|
||
|
||
const formatNumber = (num: number): string => {
|
||
if (num >= 10000) return `${(num / 10000).toFixed(1)}w`;
|
||
if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
|
||
return String(num);
|
||
};
|
||
|
||
const getResponsiveMaxLength = () => {
|
||
if (isWideScreen) return 300;
|
||
if (isDesktop) return 250;
|
||
if (isTablet) return 200;
|
||
return 100;
|
||
};
|
||
|
||
const contentNumberOfLines = useMemo(() => {
|
||
if (isWideScreen) return 8;
|
||
if (isDesktop) return 6;
|
||
if (isTablet) return 5;
|
||
return 3;
|
||
}, [isWideScreen, isDesktop, isTablet]);
|
||
|
||
const getTruncatedContent = (value: string): string => {
|
||
const maxLength = getResponsiveMaxLength();
|
||
if (value.length <= maxLength || isExpanded) return value;
|
||
return `${value.substring(0, maxLength)}...`;
|
||
};
|
||
|
||
const shouldTruncate = !showGrid && !isCompact && rawContent.length > getResponsiveMaxLength();
|
||
const content = useMemo(
|
||
() => (showGrid || isCompact ? rawContent : getTruncatedContent(rawContent)),
|
||
[rawContent, showGrid, isCompact, isExpanded, isWideScreen, isDesktop, isTablet]
|
||
);
|
||
|
||
const renderImages = () => {
|
||
if (images.length === 0) return null;
|
||
|
||
if (showGrid) {
|
||
const cover = images[0];
|
||
const coverUrl = getPreviewImageUrl(cover as any, 'grid');
|
||
return (
|
||
<TouchableOpacity activeOpacity={0.9} onPress={() => handleImagePress(images, 0)}>
|
||
<SmartImage
|
||
source={{ uri: coverUrl || cover.url || '' }}
|
||
style={styles.gridCover}
|
||
resizeMode="cover"
|
||
usePreview={true}
|
||
/>
|
||
</TouchableOpacity>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PostImages
|
||
images={post.images || []}
|
||
displayMode="list"
|
||
onImagePress={handleImagePress}
|
||
/>
|
||
);
|
||
};
|
||
|
||
const renderTopComment = () => {
|
||
if (showGrid || !post.top_comment) return null;
|
||
return (
|
||
<TouchableOpacity style={styles.topCommentBox} activeOpacity={0.85} onPress={handleComment}>
|
||
<Text style={styles.topCommentAuthor} numberOfLines={1}>
|
||
{post.top_comment.author?.nickname || '匿名用户'}:
|
||
</Text>
|
||
<Text style={styles.topCommentText} numberOfLines={1}>
|
||
{post.top_comment.content}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
const renderGridCard = () => {
|
||
const cover = images[0];
|
||
const hasImage = !!cover;
|
||
const coverUrl = hasImage ? getPreviewImageUrl(cover as any, 'grid') : '';
|
||
const contentPreview = rawContent
|
||
.replace(/[#*_~`\[\]()]/g, '')
|
||
.replace(/\n+/g, ' ')
|
||
.trim();
|
||
|
||
return (
|
||
<TouchableOpacity
|
||
activeOpacity={0.9}
|
||
onPress={handleCardPress}
|
||
style={[styles.gridRootCard, style]}
|
||
>
|
||
{hasImage ? (
|
||
<TouchableOpacity activeOpacity={0.9} onPress={() => handleImagePress(images, 0)}>
|
||
<SmartImage
|
||
source={{ uri: coverUrl || cover.url || '' }}
|
||
style={styles.gridCover}
|
||
resizeMode="cover"
|
||
usePreview={true}
|
||
/>
|
||
</TouchableOpacity>
|
||
) : (
|
||
<View style={styles.gridNoImagePreview}>
|
||
<Text style={styles.gridNoImageText} numberOfLines={6}>
|
||
{contentPreview}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{post.is_vote && (
|
||
<View style={styles.gridVoteTag}>
|
||
<MaterialCommunityIcons name="vote" size={11} color={colors.primary.contrast} />
|
||
<Text style={styles.gridVoteTagText}>投票</Text>
|
||
</View>
|
||
)}
|
||
|
||
{!!post.title && (
|
||
<Text style={styles.gridTitleMain} numberOfLines={hasImage ? 2 : 3}>
|
||
{post.title}
|
||
</Text>
|
||
)}
|
||
|
||
{!!post.channel?.name && (
|
||
<View style={styles.gridChannelRow}>
|
||
<MaterialCommunityIcons name="tag-outline" size={12} color={colors.text.hint} />
|
||
<Text style={styles.gridChannelText} numberOfLines={1}>
|
||
{post.channel.name}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
<View style={styles.gridFooter}>
|
||
<TouchableOpacity onPress={handleUserPress} style={styles.gridUserArea}>
|
||
<Avatar source={author?.avatar} size={20} name={author?.nickname || '匿名用户'} />
|
||
<Text style={styles.gridUsername} numberOfLines={1}>{author?.nickname || '匿名用户'}</Text>
|
||
</TouchableOpacity>
|
||
<View style={styles.gridLikeArea}>
|
||
<MaterialCommunityIcons name="heart-outline" size={14} color={colors.text.secondary} />
|
||
<Text style={styles.gridLikeCount}>{formatNumber(post.likes_count || 0)}</Text>
|
||
</View>
|
||
</View>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
return (
|
||
showGrid ? renderGridCard() : (
|
||
<TouchableOpacity
|
||
activeOpacity={0.9}
|
||
onPress={handleCardPress}
|
||
style={[styles.card, style]}
|
||
>
|
||
<View style={styles.header}>
|
||
<TouchableOpacity onPress={handleUserPress} activeOpacity={0.8} style={styles.authorWrap}>
|
||
<Avatar source={author?.avatar} size={showGrid ? 22 : 36} name={author?.nickname || '匿名用户'} />
|
||
<View style={styles.authorMeta}>
|
||
<Text style={styles.authorName} numberOfLines={1}>{author?.nickname || '匿名用户'}</Text>
|
||
{!showGrid && (
|
||
<View style={styles.metaRow}>
|
||
<PostCardRelativeTime createdAt={post.created_at} style={styles.timeText} />
|
||
{post.is_pinned && (
|
||
<View style={styles.pinnedTag}>
|
||
<MaterialCommunityIcons name="pin" size={10} color={colors.warning.main} />
|
||
<Text style={styles.pinnedText}>置顶</Text>
|
||
</View>
|
||
)}
|
||
{isAuthor && <Text style={styles.badgeText}>楼主</Text>}
|
||
</View>
|
||
)}
|
||
</View>
|
||
</TouchableOpacity>
|
||
{isPostAuthor && (
|
||
<TouchableOpacity onPress={handleDelete} hitSlop={8} style={styles.deleteButton} disabled={isDeleting}>
|
||
<MaterialCommunityIcons
|
||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||
size={isDesktop ? 20 : 18}
|
||
color={colors.text.hint}
|
||
/>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
|
||
{!!post.title && (
|
||
<Text style={styles.title} numberOfLines={isCompact ? 2 : undefined}>
|
||
{post.title}
|
||
</Text>
|
||
)}
|
||
|
||
{!isCompact && !!content && (
|
||
<>
|
||
<Text
|
||
style={styles.content}
|
||
numberOfLines={isExpanded ? undefined : contentNumberOfLines}
|
||
>
|
||
{content}
|
||
</Text>
|
||
{shouldTruncate && (
|
||
<TouchableOpacity onPress={() => setIsExpanded((prev) => !prev)} style={styles.expandBtn}>
|
||
<Text style={styles.expandText}>{isExpanded ? '收起' : '展开全文'}</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{renderImages()}
|
||
{renderTopComment()}
|
||
|
||
<View style={styles.actions}>
|
||
<View style={styles.actionsLeading}>
|
||
<View style={styles.viewsWrap}>
|
||
<MaterialCommunityIcons name="eye-outline" size={16} color={colors.text.hint} />
|
||
<Text style={styles.viewsText}>{post.views_count || 0}</Text>
|
||
</View>
|
||
{!!post.channel?.name && (
|
||
<View style={[styles.channelTag, styles.channelTagAfterViews]}>
|
||
<MaterialCommunityIcons name="tag-outline" size={11} color={colors.primary.main} />
|
||
<Text style={styles.channelTagText} numberOfLines={1}>
|
||
{post.channel.name}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={styles.actionButtons}>
|
||
<TouchableOpacity style={styles.actionBtn} onPress={handleLike}>
|
||
<MaterialCommunityIcons
|
||
name={post.is_liked ? 'heart' : 'heart-outline'}
|
||
size={18}
|
||
color={post.is_liked ? colors.error.main : colors.text.secondary}
|
||
/>
|
||
<Text style={post.is_liked ? styles.activeActionTextMerged : styles.actionText}>
|
||
{post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={styles.actionBtn} onPress={handleComment}>
|
||
<MaterialCommunityIcons name="comment-outline" size={18} color={colors.text.secondary} />
|
||
<Text style={styles.actionText}>{post.comments_count > 0 ? formatNumber(post.comments_count) : '评论'}</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
|
||
<MaterialCommunityIcons name="share-outline" size={18} color={colors.text.secondary} />
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={styles.actionBtn} onPress={handleBookmark}>
|
||
<MaterialCommunityIcons
|
||
name={post.is_favorited ? 'bookmark' : 'bookmark-outline'}
|
||
size={18}
|
||
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
|
||
/>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</TouchableOpacity>
|
||
)
|
||
);
|
||
};
|
||
|
||
PostCardInner.displayName = 'PostCard';
|
||
|
||
const PostCard = memo(PostCardInner, arePostCardPropsEqual);
|
||
|
||
const styles = StyleSheet.create({
|
||
card: {
|
||
backgroundColor: colors.background.paper,
|
||
borderRadius: borderRadius.lg,
|
||
padding: spacing.md,
|
||
borderWidth: StyleSheet.hairlineWidth,
|
||
borderColor: colors.divider,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
marginBottom: spacing.sm,
|
||
},
|
||
authorWrap: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
},
|
||
authorMeta: {
|
||
marginLeft: spacing.sm,
|
||
flex: 1,
|
||
minWidth: 0,
|
||
},
|
||
authorName: {
|
||
color: colors.text.primary,
|
||
fontWeight: '600',
|
||
fontSize: fontSizes.md,
|
||
},
|
||
timeText: {
|
||
color: colors.text.hint,
|
||
fontSize: fontSizes.xs,
|
||
marginTop: 2,
|
||
},
|
||
metaRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
marginTop: 2,
|
||
flexWrap: 'wrap',
|
||
},
|
||
channelTag: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
maxWidth: 140,
|
||
paddingHorizontal: 6,
|
||
paddingVertical: 2,
|
||
borderRadius: borderRadius.sm,
|
||
backgroundColor: `${colors.primary.main}12`,
|
||
gap: 4,
|
||
},
|
||
channelTagText: {
|
||
fontSize: fontSizes.xs,
|
||
color: colors.primary.main,
|
||
fontWeight: '600',
|
||
flexShrink: 1,
|
||
},
|
||
pinnedTag: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: `${colors.warning.light}22`,
|
||
borderRadius: borderRadius.sm,
|
||
paddingHorizontal: 4,
|
||
paddingVertical: 1,
|
||
gap: 2,
|
||
},
|
||
pinnedText: {
|
||
color: colors.warning.main,
|
||
fontSize: fontSizes.xs,
|
||
},
|
||
badgeText: {
|
||
color: colors.primary.main,
|
||
fontSize: fontSizes.xs,
|
||
fontWeight: '600',
|
||
},
|
||
deleteButton: {
|
||
padding: spacing.xs,
|
||
marginLeft: spacing.sm,
|
||
},
|
||
title: {
|
||
color: colors.text.primary,
|
||
fontSize: fontSizes.lg,
|
||
fontWeight: '600',
|
||
marginBottom: spacing.xs,
|
||
},
|
||
content: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.md,
|
||
marginBottom: spacing.xs,
|
||
lineHeight: fontSizes.md * 1.45,
|
||
},
|
||
expandBtn: {
|
||
marginBottom: spacing.sm,
|
||
},
|
||
expandText: {
|
||
color: colors.primary.main,
|
||
fontSize: fontSizes.sm,
|
||
},
|
||
topCommentBox: {
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: borderRadius.sm,
|
||
paddingHorizontal: spacing.sm,
|
||
paddingVertical: spacing.xs,
|
||
marginBottom: spacing.sm,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
topCommentAuthor: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
fontWeight: '600',
|
||
marginRight: 4,
|
||
},
|
||
topCommentText: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
flex: 1,
|
||
},
|
||
gridCover: {
|
||
width: '100%',
|
||
aspectRatio: 0.78,
|
||
backgroundColor: colors.background.default,
|
||
},
|
||
actions: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
},
|
||
actionsLeading: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
marginRight: spacing.sm,
|
||
gap: spacing.sm,
|
||
},
|
||
channelTagAfterViews: {
|
||
maxWidth: 130,
|
||
flexShrink: 1,
|
||
},
|
||
viewsWrap: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flexShrink: 0,
|
||
},
|
||
viewsText: {
|
||
color: colors.text.hint,
|
||
fontSize: fontSizes.sm,
|
||
marginLeft: 4,
|
||
},
|
||
actionButtons: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
actionBtn: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginLeft: spacing.md,
|
||
},
|
||
actionText: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
marginLeft: 4,
|
||
},
|
||
activeActionText: {
|
||
color: colors.error.main,
|
||
},
|
||
activeActionTextMerged: {
|
||
color: colors.error.main,
|
||
fontSize: fontSizes.sm,
|
||
marginLeft: 4,
|
||
},
|
||
gridRootCard: {
|
||
backgroundColor: colors.background.paper,
|
||
borderRadius: borderRadius.lg,
|
||
overflow: 'hidden',
|
||
borderWidth: StyleSheet.hairlineWidth,
|
||
borderColor: colors.divider,
|
||
},
|
||
gridNoImagePreview: {
|
||
backgroundColor: colors.background.default,
|
||
margin: 6,
|
||
borderRadius: borderRadius.md,
|
||
minHeight: 120,
|
||
padding: 8,
|
||
},
|
||
gridNoImageText: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
lineHeight: 20,
|
||
},
|
||
gridVoteTag: {
|
||
position: 'absolute',
|
||
top: 8,
|
||
right: 8,
|
||
backgroundColor: `${colors.primary.main}CC`,
|
||
borderRadius: borderRadius.full,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
},
|
||
gridVoteTagText: {
|
||
color: colors.primary.contrast,
|
||
fontSize: 10,
|
||
fontWeight: '600',
|
||
},
|
||
gridTitleMain: {
|
||
color: colors.text.primary,
|
||
fontSize: fontSizes.md,
|
||
lineHeight: 20,
|
||
paddingHorizontal: 8,
|
||
paddingTop: 8,
|
||
minHeight: 44,
|
||
},
|
||
gridChannelRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
paddingHorizontal: 8,
|
||
paddingTop: 4,
|
||
paddingBottom: 2,
|
||
},
|
||
gridChannelText: {
|
||
fontSize: fontSizes.xs,
|
||
color: colors.text.secondary,
|
||
fontWeight: '600',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
},
|
||
gridFooter: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 10,
|
||
},
|
||
gridUserArea: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
},
|
||
gridUsername: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
marginLeft: 6,
|
||
flex: 1,
|
||
},
|
||
gridLikeArea: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
gridLikeCount: {
|
||
color: colors.text.secondary,
|
||
fontSize: fontSizes.sm,
|
||
marginLeft: 4,
|
||
},
|
||
});
|
||
|
||
export default PostCard; |