feat(post): add post reference functionality with inline cards and suggestion search
Add support for referencing/quoting other posts within post content. Includes: - New `post_ref` segment type with `PostRefSegmentData` interface for post references - New `PostRefCard` component to display referenced posts inline - Post suggestion search triggered by `#` in `PostMentionInput` - Updated `PostContentRenderer` to render post reference segments - Add `suggestPosts` and `recordRefClick` API methods in postService Additional improvements: - Add image gallery preview for user profile covers and avatars - Make text selectable across CommentItem, PostContentRenderer, and PostDetailScreen - Migrate FlatList to FlashList in PostDetailScreen and UserProfileScreen - Add nestedScrollEnabled to CreatePostScreen - Add clickable member avatars in GroupMembersScreen
This commit is contained in:
@@ -493,7 +493,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
|
||||
</View>
|
||||
{/* 显示回复内容(如果有文字) */}
|
||||
{reply.content ? (
|
||||
<Text variant="body" color={colors.text.primary} style={styles.subReplyBody}>
|
||||
<Text variant="body" color={colors.text.primary} selectable style={styles.subReplyBody}>
|
||||
{reply.content}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -625,7 +625,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
|
||||
textStyle={[styles.text, { color: colors.text.primary }]}
|
||||
/>
|
||||
) : (
|
||||
<Text variant="body" color={colors.text.primary} style={styles.text}>
|
||||
<Text variant="body" color={colors.text.primary} selectable style={styles.text}>
|
||||
{comment.content}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -9,13 +9,15 @@ import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
|
||||
import Text from '../common/Text';
|
||||
import { useAppColors } from '../../theme';
|
||||
import { MessageSegment, AtSegmentData, VoteSegmentData } from '../../types';
|
||||
import { MessageSegment, AtSegmentData, VoteSegmentData, PostRefSegmentData } from '../../types';
|
||||
import PostRefCard from './PostRefCard';
|
||||
|
||||
interface PostContentRendererProps {
|
||||
content?: string;
|
||||
segments?: MessageSegment[];
|
||||
numberOfLines?: number;
|
||||
onMentionPress?: (userId: string) => void;
|
||||
onPostRefPress?: (postId: string) => void;
|
||||
style?: any;
|
||||
textStyle?: any;
|
||||
}
|
||||
@@ -25,6 +27,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
segments,
|
||||
numberOfLines,
|
||||
onMentionPress,
|
||||
onPostRefPress,
|
||||
style,
|
||||
textStyle,
|
||||
}) => {
|
||||
@@ -32,7 +35,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={textStyle}>
|
||||
<Text numberOfLines={numberOfLines} selectable style={textStyle}>
|
||||
{content || ''}
|
||||
</Text>
|
||||
);
|
||||
@@ -49,7 +52,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
const text = segment.data?.text || segment.data?.content || '';
|
||||
if (text) {
|
||||
elements.push(
|
||||
<Text key={key} style={textStyle}>
|
||||
<Text key={key} selectable style={textStyle}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
@@ -65,6 +68,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
elements.push(
|
||||
<Text
|
||||
key={key}
|
||||
selectable
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
@@ -88,7 +92,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
📊 投票({voteData.options.length} 个选项)
|
||||
</Text>
|
||||
{voteData.options.map((opt, i) => (
|
||||
<Text key={`vote_opt_${i}`} style={[styles.voteOption, { color: colors.text.primary }]}>
|
||||
<Text key={`vote_opt_${i}`} selectable style={[styles.voteOption, { color: colors.text.primary }]}>
|
||||
{i + 1}. {opt.content}
|
||||
</Text>
|
||||
))}
|
||||
@@ -98,6 +102,19 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_ref': {
|
||||
const refData = segment.data as PostRefSegmentData;
|
||||
elements.push(
|
||||
<PostRefCard
|
||||
key={key}
|
||||
data={refData}
|
||||
compact={!!numberOfLines}
|
||||
onPress={onPostRefPress}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'image':
|
||||
case 'link':
|
||||
case 'face':
|
||||
@@ -108,7 +125,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
|
||||
if (elements.length === 0) {
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={textStyle}>
|
||||
<Text numberOfLines={numberOfLines} selectable style={textStyle}>
|
||||
{content || ''}
|
||||
</Text>
|
||||
);
|
||||
@@ -117,7 +134,7 @@ const PostContentRenderer: React.FC<PostContentRendererProps> = ({
|
||||
if (numberOfLines) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<Text numberOfLines={numberOfLines} style={textStyle}>
|
||||
<Text numberOfLines={numberOfLines} selectable style={textStyle}>
|
||||
{elements}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
FlatList,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
@@ -20,6 +20,7 @@ import Avatar from '../common/Avatar';
|
||||
import { useAppColors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { authService } from '../../services/auth';
|
||||
import { postService } from '../../services/post/postService';
|
||||
import { MessageSegment, AtSegmentData } from '../../types';
|
||||
|
||||
interface MentionUser {
|
||||
@@ -28,6 +29,14 @@ interface MentionUser {
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
interface SuggestPost {
|
||||
id: string;
|
||||
title: string;
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
type SuggestionMode = 'none' | 'mention' | 'postref';
|
||||
|
||||
interface PostMentionInputProps {
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
@@ -37,6 +46,7 @@ interface PostMentionInputProps {
|
||||
style?: any;
|
||||
minHeight?: number;
|
||||
autoExpand?: boolean;
|
||||
onPostRefPasted?: (postId: string, title: string) => void;
|
||||
}
|
||||
|
||||
const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
@@ -48,14 +58,17 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
style,
|
||||
minHeight = 100,
|
||||
autoExpand = false,
|
||||
onPostRefPasted,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const currentUser = useAuthStore(s => s.currentUser);
|
||||
const [followingUsers, setFollowingUsers] = useState<MentionUser[]>([]);
|
||||
const [showMentions, setShowMentions] = useState(false);
|
||||
const [suggestPosts, setSuggestPosts] = useState<SuggestPost[]>([]);
|
||||
const [suggestionMode, setSuggestionMode] = useState<SuggestionMode>('none');
|
||||
const [mentionQuery, setMentionQuery] = useState('');
|
||||
const [mentionStartIndex, setMentionStartIndex] = useState(-1);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [postSearchTimer, setPostSearchTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -125,8 +138,42 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
onChangeText(text);
|
||||
|
||||
const cursorPos = text.length;
|
||||
let atStart = -1;
|
||||
|
||||
// Check for # trigger (post ref search)
|
||||
let hashStart = -1;
|
||||
for (let i = cursorPos - 1; i >= 0; i--) {
|
||||
if (text[i] === '#') {
|
||||
hashStart = i;
|
||||
break;
|
||||
}
|
||||
if (text[i] === ' ' || text[i] === '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hashStart >= 0) {
|
||||
const query = text.slice(hashStart + 1, cursorPos);
|
||||
if (query.length >= 1 && !query.includes(' ') && !query.includes('\n')) {
|
||||
if (postSearchTimer) {
|
||||
clearTimeout(postSearchTimer);
|
||||
}
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const results = await postService.suggestPosts(query, 8);
|
||||
setSuggestPosts(results);
|
||||
setSuggestionMode('postref');
|
||||
setMentionStartIndex(hashStart);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 300);
|
||||
setPostSearchTimer(timer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for @ trigger (mention)
|
||||
let atStart = -1;
|
||||
for (let i = cursorPos - 1; i >= 0; i--) {
|
||||
if (text[i] === '@') {
|
||||
atStart = i;
|
||||
@@ -142,15 +189,15 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
if (!query.includes(' ') && !query.includes('\n')) {
|
||||
setMentionQuery(query);
|
||||
setMentionStartIndex(atStart);
|
||||
setShowMentions(true);
|
||||
setSuggestionMode('mention');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setShowMentions(false);
|
||||
setSuggestionMode('none');
|
||||
rebuildSegments(text);
|
||||
},
|
||||
[onChangeText, rebuildSegments]
|
||||
[onChangeText, rebuildSegments, postSearchTimer],
|
||||
);
|
||||
|
||||
const handleSelectMention = useCallback(
|
||||
@@ -168,7 +215,7 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
});
|
||||
|
||||
onChangeText(newText);
|
||||
setShowMentions(false);
|
||||
setSuggestionMode('none');
|
||||
setMentionQuery('');
|
||||
setMentionStartIndex(-1);
|
||||
|
||||
@@ -176,7 +223,31 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[value, mentionStartIndex, onChangeText, rebuildSegments]
|
||||
[value, mentionStartIndex, onChangeText, rebuildSegments],
|
||||
);
|
||||
|
||||
const handleSelectPost = useCallback(
|
||||
(post: SuggestPost) => {
|
||||
if (mentionStartIndex < 0) return;
|
||||
|
||||
const before = value.slice(0, mentionStartIndex);
|
||||
const refText = `#${post.title} `;
|
||||
const newText = before + refText;
|
||||
|
||||
onChangeText(newText);
|
||||
setSuggestionMode('none');
|
||||
setSuggestPosts([]);
|
||||
setMentionStartIndex(-1);
|
||||
|
||||
if (onPostRefPasted) {
|
||||
onPostRefPasted(post.id, post.title);
|
||||
}
|
||||
|
||||
rebuildSegments(newText);
|
||||
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[value, mentionStartIndex, onChangeText, rebuildSegments, onPostRefPasted],
|
||||
);
|
||||
|
||||
const renderMentionItem = ({ item }: { item: MentionUser }) => (
|
||||
@@ -221,26 +292,66 @@ const PostMentionInput: React.FC<PostMentionInputProps> = ({
|
||||
]}
|
||||
/>
|
||||
|
||||
{showMentions && filteredUsers.length > 0 && (
|
||||
{suggestionMode === 'mention' && filteredUsers.length > 0 && (
|
||||
<View style={[styles.mentionPanel, { backgroundColor: colors.background.paper, borderColor: colors.divider }]}>
|
||||
<FlatList
|
||||
data={filteredUsers}
|
||||
renderItem={renderMentionItem}
|
||||
keyExtractor={item => item.id}
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
nestedScrollEnabled
|
||||
style={styles.mentionList}
|
||||
/>
|
||||
>
|
||||
{filteredUsers.map(item => (
|
||||
<React.Fragment key={item.id}>
|
||||
{renderMentionItem({ item })}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showMentions && filteredUsers.length === 0 && (
|
||||
{suggestionMode === 'mention' && filteredUsers.length === 0 && (
|
||||
<View style={[styles.mentionPanel, { backgroundColor: colors.background.paper, borderColor: colors.divider }]}>
|
||||
<Text style={[styles.emptyText, { color: colors.text.hint }]}>
|
||||
没有找到匹配的关注用户
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{suggestionMode === 'postref' && suggestPosts.length > 0 && (
|
||||
<View style={[styles.mentionPanel, { backgroundColor: colors.background.paper, borderColor: colors.divider }]}>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
nestedScrollEnabled
|
||||
style={styles.mentionList}
|
||||
>
|
||||
{suggestPosts.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[styles.mentionItem, { borderBottomColor: colors.divider }]}
|
||||
onPress={() => handleSelectPost(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons name="file-document-outline" size={18} color={colors.primary.main} />
|
||||
<View style={styles.mentionInfo}>
|
||||
<Text style={[styles.mentionName, { color: colors.text.primary }]} numberOfLines={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text style={[styles.mentionHint, { color: colors.text.hint }]}>
|
||||
点击引用帖子
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{suggestionMode === 'postref' && suggestPosts.length === 0 && (
|
||||
<View style={[styles.mentionPanel, { backgroundColor: colors.background.paper, borderColor: colors.divider }]}>
|
||||
<Text style={[styles.emptyText, { color: colors.text.hint }]}>
|
||||
没有找到匹配的帖子
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
124
src/components/business/PostRefCard.tsx
Normal file
124
src/components/business/PostRefCard.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
import Text from '../common/Text';
|
||||
import { useAppColors, spacing, borderRadius } from '../../theme';
|
||||
import type { PostRefSegmentData } from '../../types';
|
||||
|
||||
interface PostRefCardProps {
|
||||
data: PostRefSegmentData;
|
||||
compact?: boolean;
|
||||
onPress?: (postId: string) => void;
|
||||
}
|
||||
|
||||
const PostRefCard: React.FC<PostRefCardProps> = ({ data, compact = false, onPress }) => {
|
||||
const colors = useAppColors();
|
||||
const router = useRouter();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (onPress) {
|
||||
onPress(data.post_id);
|
||||
} else {
|
||||
router.push(`/post/${data.post_id}` as any);
|
||||
}
|
||||
}, [data.post_id, onPress, router]);
|
||||
|
||||
const isDeleted = data.status === 'deleted';
|
||||
const isHidden = data.status === 'hidden';
|
||||
const isUnavailable = isDeleted || isHidden || !data.accessible;
|
||||
const authorName = data.author?.nickname || '';
|
||||
|
||||
if (isUnavailable) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: colors.background.default, borderColor: colors.divider },
|
||||
compact && styles.containerCompact,
|
||||
]}
|
||||
>
|
||||
<MaterialCommunityIcons name="link-off" size={16} color={colors.text.hint} />
|
||||
<Text style={[styles.unavailableText, { color: colors.text.hint }]} numberOfLines={1}>
|
||||
{isDeleted ? '该帖子已删除' : isHidden ? '该帖子不可见' : '请登录后查看'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: colors.background.default, borderColor: colors.primary.light },
|
||||
compact && styles.containerCompact,
|
||||
]}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.iconWrap, { backgroundColor: colors.primary.light }]}>
|
||||
<MaterialCommunityIcons name="file-document-outline" size={14} color={colors.primary.main} />
|
||||
</View>
|
||||
<View style={styles.contentWrap}>
|
||||
<Text
|
||||
style={[styles.title, { color: colors.text.primary }]}
|
||||
numberOfLines={compact ? 1 : 2}
|
||||
>
|
||||
{data.title || '帖子'}
|
||||
</Text>
|
||||
{authorName ? (
|
||||
<Text style={[styles.author, { color: colors.text.secondary }]} numberOfLines={1}>
|
||||
@{authorName}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={16} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: borderRadius.md,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
containerCompact: {
|
||||
paddingVertical: spacing.xs,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
iconWrap: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
contentWrap: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
},
|
||||
author: {
|
||||
fontSize: 11,
|
||||
lineHeight: 14,
|
||||
marginTop: 1,
|
||||
},
|
||||
unavailableText: {
|
||||
fontSize: 13,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
});
|
||||
|
||||
export default PostRefCard;
|
||||
@@ -20,6 +20,8 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
|
||||
import { User } from '../../types';
|
||||
import Text from '../common/Text';
|
||||
import Avatar from '../common/Avatar';
|
||||
import { ImageGallery } from '../common';
|
||||
import type { GalleryImageItem } from '../common';
|
||||
import { useResponsive } from '../../hooks';
|
||||
|
||||
interface UserProfileHeaderProps {
|
||||
@@ -55,6 +57,9 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const moreButtonRef = useRef<View>(null);
|
||||
const [galleryVisible, setGalleryVisible] = useState(false);
|
||||
const [galleryImages, setGalleryImages] = useState<GalleryImageItem[]>([]);
|
||||
const [galleryIndex, setGalleryIndex] = useState(0);
|
||||
|
||||
const formatCount = (count: number | undefined): string => {
|
||||
if (count === undefined || count === null) return '0';
|
||||
@@ -106,6 +111,24 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
onBlock?.();
|
||||
}, [onBlock]);
|
||||
|
||||
const openImageGallery = useCallback((images: GalleryImageItem[], index: number = 0) => {
|
||||
setGalleryImages(images);
|
||||
setGalleryIndex(index);
|
||||
setGalleryVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleCoverPress = useCallback(() => {
|
||||
if (user.cover_url) {
|
||||
openImageGallery([{ id: 'cover', url: user.cover_url }], 0);
|
||||
}
|
||||
}, [user.cover_url, openImageGallery]);
|
||||
|
||||
const handleAvatarPressPreview = useCallback(() => {
|
||||
if (user.avatar) {
|
||||
openImageGallery([{ id: 'avatar', url: user.avatar }], 0);
|
||||
}
|
||||
}, [user.avatar, openImageGallery]);
|
||||
|
||||
const renderStatLink = (count: number, label: string, onPress?: () => void) => {
|
||||
if (!onPress) {
|
||||
return (
|
||||
@@ -165,7 +188,12 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
<View style={styles.container}>
|
||||
{/* 全宽封面背景 */}
|
||||
<View style={[styles.coverContainer, { height: coverHeight }]}>
|
||||
<View style={styles.coverTouchable}>
|
||||
<TouchableOpacity
|
||||
style={styles.coverTouchable}
|
||||
onPress={handleCoverPress}
|
||||
activeOpacity={0.9}
|
||||
disabled={!user.cover_url}
|
||||
>
|
||||
{user.cover_url ? (
|
||||
<Image
|
||||
source={{ uri: user.cover_url }}
|
||||
@@ -177,7 +205,7 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
<MaterialCommunityIcons name="image-outline" size={48} color="rgba(255,255,255,0.4)" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 顶部导航栏按钮 */}
|
||||
<View style={styles.topBar}>
|
||||
@@ -194,18 +222,23 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
{/* 头像与操作按钮行 */}
|
||||
<View style={styles.avatarActionRow}>
|
||||
<View style={[styles.avatarWrapper, { marginTop: -avatarSize / 2 }]}>
|
||||
<View style={[styles.avatarContainer, { width: avatarSize, height: avatarSize }]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.avatarContainer, { width: avatarSize, height: avatarSize }]}
|
||||
onPress={isCurrentUser && onAvatarPress ? onAvatarPress : handleAvatarPressPreview}
|
||||
activeOpacity={0.9}
|
||||
disabled={isCurrentUser ? !onAvatarPress : !user.avatar}
|
||||
>
|
||||
<Avatar
|
||||
source={user.avatar}
|
||||
size={avatarSize - 6}
|
||||
name={user.nickname}
|
||||
/>
|
||||
{isCurrentUser && onAvatarPress && (
|
||||
<TouchableOpacity style={styles.editAvatarButton} onPress={onAvatarPress}>
|
||||
<View style={styles.editAvatarButton} pointerEvents="none">
|
||||
<MaterialCommunityIcons name="camera" size={14} color={colors.text.inverse} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionButtons}>
|
||||
@@ -291,6 +324,15 @@ const UserProfileHeader: React.FC<UserProfileHeaderProps> = ({
|
||||
</View>
|
||||
|
||||
{renderDropdownMenu()}
|
||||
|
||||
{/* 图片预览 */}
|
||||
<ImageGallery
|
||||
visible={galleryVisible}
|
||||
images={galleryImages}
|
||||
initialIndex={galleryIndex}
|
||||
onClose={() => setGalleryVisible(false)}
|
||||
enableSave
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user