Files
frontend/src/screens/message/GroupInfoScreen.tsx
lafay 5815f1a5f7
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m49s
Frontend CI / ota-android (push) Successful in 10m29s
Frontend CI / build-android-apk (push) Successful in 40m32s
refactor(home): redesign home screen with sort bar and add recommendation feed
- Replace PagerView-based tabs with modern sort bar navigation
- Add "推荐" (recommended) feed option with "hot" sort type
- Update tab icons and labels to match new sort paradigm
- Optimize PostCard, MessageBubble conditional styles

perf(chat): migrate FlatList to FlashList for emoji/sticker panels

- Improve virtualized rendering performance for emoji and sticker grids
- Fix jump-to-latest scroll behavior for inverted FlashList

perf(profile): extract user header and tab bar into separate memoized renders

- Prevent unnecessary re-renders when switching tabs

feat(api): add channel_id filter support for post queries

- Include channel_id in cursor pagination requests
- Update CursorPaginationRequest type definition

style(chat-info): redesign group and private chat info screens with flat layout

- Remove card borders and shadows for cleaner appearance
- Adjust avatar sizes and spacing for consistency
2026-04-26 12:04:27 +08:00

1694 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GroupInfoScreen 群组信息/设置界面
* 显示群组详细信息,提供群组管理功能
* 支持响应式布局
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
Clipboard,
TextInput,
Modal,
Switch,
KeyboardAvoidingView,
Platform,
Image,
Dimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFocusEffect } from '@react-navigation/native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import {
spacing,
fontSizes,
borderRadius,
shadows,
useAppColors,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { groupService } from '@/services/message';
import { uploadService } from '@/services/upload';
import { messageService } from '@/services/message';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import {
GroupResponse,
GroupMemberResponse,
GroupAnnouncementResponse,
JoinType,
} from '../../types/dto';
import { User } from '../../types';
import { blurActiveElement } from '../../infrastructure/platform';
import { firstRouteParam } from '../../navigation/paramUtils';
import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores/message';
import { useMessageStore } from '../../stores/message/store';
import { groupManager } from '../../stores/group';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
import { AppBackButton } from '../../components/common';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
// 加群方式选项
const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: string }[] = [
{ value: 0, label: '允许任何人加入', icon: 'earth', desc: '任何人都可以直接加入群聊' },
{ value: 1, label: '需要管理员审批', icon: 'shield-check', desc: '新成员需要管理员审核' },
{ value: 2, label: '不允许任何人加入', icon: 'lock', desc: '只能通过邀请加入群聊' },
];
const GroupInfoScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createGroupInfoStyles(colors), [colors]);
const router = useRouter();
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
groupId?: string | string[];
conversationId?: string | string[];
}>();
const groupId = firstRouteParam(groupIdParam) ?? '';
const conversationId = firstRouteParam(conversationIdParam);
const { currentUser } = useAuthStore();
// 响应式布局
const { isDesktop, isTablet, width } = useResponsive();
const isWideScreen = useBreakpointGTE('lg');
// 计算成员网格列数
const memberColumns = useMemo(() => {
if (width >= 1200) return 6;
if (width >= 900) return 5;
if (width >= 768) return 4;
return 5; // 移动端默认
}, [width]);
// 群组信息状态
const [group, setGroup] = useState<GroupResponse | null>(null);
const [members, setMembers] = useState<GroupMemberResponse[]>([]);
const [announcements, setAnnouncements] = useState<GroupAnnouncementResponse[]>([]);
const [currentMember, setCurrentMember] = useState<GroupMemberResponse | null>(null);
const [loading, setLoading] = useState(true);
const [isPinned, setIsPinned] = useState(false);
const [pinLoading, setPinLoading] = useState(false);
const isNotificationMuted = useMessageStore(state =>
conversationId ? (state.notificationMutedMap.get(conversationId) || false) : false
);
// 编辑模态框状态
const [editModalVisible, setEditModalVisible] = useState(false);
const [editName, setEditName] = useState('');
const [editDescription, setEditDescription] = useState('');
const [editAvatar, setEditAvatar] = useState<string>('');
const [saving, setSaving] = useState(false);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
// 群公告模态框状态
const [announcementModalVisible, setAnnouncementModalVisible] = useState(false);
const [newAnnouncement, setNewAnnouncement] = useState('');
// 转让群主模态框状态
const [transferModalVisible, setTransferModalVisible] = useState(false);
const [transferUserId, setTransferUserId] = useState<string>('');
// 加群方式模态框状态
const [joinTypeModalVisible, setJoinTypeModalVisible] = useState(false);
// 邀请成员模态框状态
const [inviteModalVisible, setInviteModalVisible] = useState(false);
const [inviting, setInviting] = useState(false);
useEffect(() => {
if (editModalVisible || announcementModalVisible || transferModalVisible || joinTypeModalVisible || inviteModalVisible) {
blurActiveElement();
}
}, [editModalVisible, announcementModalVisible, transferModalVisible, joinTypeModalVisible, inviteModalVisible]);
// 计算当前用户的角色
const isOwner = currentMember?.role === 'owner';
const isAdmin = currentMember?.role === 'admin' || isOwner;
// 加载群组信息
const loadGroupInfo = useCallback(async () => {
if (!groupId) return;
try {
setLoading(true);
// 并行加载群组信息和成员列表
const [groupData, membersData] = await Promise.all([
groupManager.getGroup(groupId, true),
groupManager.getMembers(groupId, 1, 100, true),
]);
setGroup(groupData);
setMembers(membersData.list);
// 查找当前用户的成员信息
const myMember = membersData.list.find(m => m.user_id === currentUser?.id);
setCurrentMember(myMember || null);
// 加载群公告
try {
const announcementsData = await groupService.getAnnouncements(groupId, 1, 10);
setAnnouncements(announcementsData.list);
} catch (e) {
console.error('加载群公告失败:', e);
}
} catch (error) {
console.error('加载群组信息失败:', error);
Alert.alert('错误', '加载群组信息失败', [
{ text: '确定', onPress: () => router.back() },
]);
} finally {
setLoading(false);
}
}, [groupId, currentUser]);
// 加载会话置顶状态
const loadPinStatus = useCallback(async () => {
if (!conversationId) {
setIsPinned(false);
return;
}
const conv = messageManager.getConversation(conversationId);
if (conv) {
setIsPinned(Boolean(conv.is_pinned));
return;
}
try {
const detail = await messageService.getConversationById(conversationId);
setIsPinned(Boolean(detail.is_pinned));
} catch (error) {
console.error('加载置顶状态失败:', error);
}
}, [conversationId]);
// 页面聚焦时刷新数据
useFocusEffect(
useCallback(() => {
loadGroupInfo();
loadPinStatus();
}, [loadGroupInfo, loadPinStatus])
);
// 设置会话置顶
const handleTogglePinned = async () => {
if (!conversationId || pinLoading) return;
const nextValue = !isPinned;
setIsPinned(nextValue);
setPinLoading(true);
try {
await messageService.setConversationPinned(conversationId, nextValue);
messageManager.updateConversation(conversationId, { is_pinned: nextValue });
} catch (error: any) {
setIsPinned(!nextValue);
Alert.alert('错误', error?.message || '设置置顶失败');
} finally {
setPinLoading(false);
}
};
// 设置会话免打扰
const handleToggleNotificationMuted = async () => {
if (!conversationId) return;
try {
await messageManager.toggleNotificationMuted(conversationId, !isNotificationMuted);
} catch (error: any) {
Alert.alert('错误', error?.message || '设置免打扰失败');
}
};
// 打开编辑模态框
const openEditModal = () => {
if (group) {
setEditName(group.name);
setEditDescription(group.description || '');
setEditAvatar(group.avatar || '');
setEditModalVisible(true);
}
};
// 选择群头像
const handleSelectGroupAvatar = async () => {
try {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('提示', '需要访问相册权限才能选择头像');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const selectedAsset = result.assets[0];
setUploadingAvatar(true);
try {
const uploadResult = await uploadService.uploadGroupAvatar({
uri: selectedAsset.uri,
type: selectedAsset.mimeType || 'image/jpeg',
});
if (uploadResult && uploadResult.url) {
setEditAvatar(uploadResult.url);
} else {
Alert.alert('上传失败', '头像上传失败,请重试');
}
} catch (error) {
console.error('上传头像失败:', error);
Alert.alert('上传失败', '头像上传失败,请重试');
} finally {
setUploadingAvatar(false);
}
}
} catch (error) {
console.error('选择头像失败:', error);
Alert.alert('错误', '选择头像时发生错误');
}
};
// 保存群组信息
const handleSaveGroupInfo = async () => {
if (!editName.trim()) {
Alert.alert('提示', '群名称不能为空');
return;
}
setSaving(true);
try {
// 更新群名称
await groupService.setGroupName(groupId, editName.trim());
// 如果头像有变化,更新群头像
if (editAvatar !== (group?.avatar || '')) {
await groupService.setGroupAvatar(groupId, editAvatar);
}
// 重新获取群信息
const updatedGroup = await groupManager.getGroup(groupId, true);
setGroup(updatedGroup);
setEditModalVisible(false);
Alert.alert('成功', '群信息已更新');
} catch (error: any) {
console.error('更新群信息失败:', error);
Alert.alert('错误', error.message || '更新群信息失败');
} finally {
setSaving(false);
}
};
// 设置全员禁言
const handleToggleMuteAll = async () => {
if (!group) return;
const newMuteAll = !group.mute_all;
Alert.alert(
newMuteAll ? '开启全员禁言' : '关闭全员禁言',
newMuteAll
? '确定要开启全员禁言吗?开启后只有管理员可以发言。'
: '确定要关闭全员禁言吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: async () => {
try {
await groupService.setMuteAll(groupId, { mute_all: newMuteAll });
setGroup({ ...group, mute_all: newMuteAll });
Alert.alert('成功', newMuteAll ? '已开启全员禁言' : '已关闭全员禁言');
} catch (error: any) {
console.error('设置全员禁言失败:', error);
Alert.alert('错误', error.message || '设置失败');
}
},
},
]
);
};
// 设置加群方式
const handleSetJoinType = async (joinType: JoinType) => {
try {
await groupService.setJoinType(groupId, { join_type: joinType });
if (group) {
setGroup({ ...group, join_type: joinType });
}
setJoinTypeModalVisible(false);
Alert.alert('成功', '加群方式已更新');
} catch (error: any) {
console.error('设置加群方式失败:', error);
Alert.alert('错误', error.message || '设置失败');
}
};
// 发布群公告
const handlePublishAnnouncement = async () => {
if (!newAnnouncement.trim()) {
Alert.alert('提示', '公告内容不能为空');
return;
}
setSaving(true);
try {
await groupService.createAnnouncement(groupId, {
content: newAnnouncement.trim(),
});
setNewAnnouncement('');
setAnnouncementModalVisible(false);
// 刷新公告列表
const announcementsData = await groupService.getAnnouncements(groupId, 1, 10);
setAnnouncements(announcementsData.list);
Alert.alert('成功', '群公告已发布');
} catch (error: any) {
console.error('发布群公告失败:', error);
Alert.alert('错误', error.message || '发布失败');
} finally {
setSaving(false);
}
};
// 转让群主
const handleTransferOwner = async () => {
if (!transferUserId) {
Alert.alert('提示', '请选择新群主');
return;
}
Alert.alert(
'转让群主',
'确定要将群主转让给该成员吗?转让后您将成为普通成员。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: async () => {
try {
await groupService.transferOwner(groupId, {
new_owner_id: transferUserId,
});
setTransferModalVisible(false);
Alert.alert('成功', '群主已转让', [
{ text: '确定', onPress: () => loadGroupInfo() },
]);
} catch (error: any) {
console.error('转让群主失败:', error);
Alert.alert('错误', error.message || '转让失败');
}
},
},
]
);
};
// 解散群组
const handleDissolveGroup = () => {
Alert.alert(
'解散群组',
'确定要解散群组吗?此操作不可撤销,所有成员将被移出。',
[
{ text: '取消', style: 'cancel' },
{
text: '解散',
style: 'destructive',
onPress: async () => {
try {
await groupService.dissolveGroup(groupId);
Alert.alert('成功', '群组已解散', [
{ text: '确定', onPress: () => router.back() },
]);
} catch (error: any) {
console.error('解散群组失败:', error);
Alert.alert('错误', error.message || '解散失败');
}
},
},
]
);
};
// 退出群聊
const handleLeaveGroup = () => {
Alert.alert(
'退出群聊',
'确定要退出群聊吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '退出',
style: 'destructive',
onPress: async () => {
try {
await groupService.leaveGroup(groupId);
Alert.alert('成功', '已退出群聊', [
{ text: '确定', onPress: () => router.back() },
]);
} catch (error: any) {
console.error('退出群聊失败:', error);
Alert.alert('错误', error.message || '退出失败');
}
},
},
]
);
};
// 打开邀请成员模态框
const openInviteModal = () => {
setInviteModalVisible(true);
};
// 邀请成员
const handleInviteMembers = async (selectedUsers: User[]) => {
if (selectedUsers.length === 0) {
Alert.alert('提示', '请选择要邀请的成员');
return;
}
setInviting(true);
try {
const memberIds = selectedUsers.map(user => user.id);
await groupService.inviteMembers(groupId, { member_ids: memberIds });
setInviteModalVisible(false);
Alert.alert('成功', '邀请已发送');
// 刷新成员列表
loadGroupInfo();
} catch (error: any) {
console.error('邀请成员失败:', error);
Alert.alert('错误', error.message || '邀请失败');
} finally {
setInviting(false);
}
};
// 跳转到成员管理
const goToMembers = () => {
router.push(hrefs.hrefGroupMembers(groupId));
};
// 获取加群方式文本
const getJoinTypeText = (joinType: JoinType): string => {
const option = JOIN_TYPE_OPTIONS.find(o => o.value === joinType);
return option?.label || '未知';
};
// 获取加群方式图标
const getJoinTypeIcon = (joinType: JoinType): string => {
const option = JOIN_TYPE_OPTIONS.find(o => o.value === joinType);
return option?.icon || 'help-circle';
};
const handleCopyGroupNo = () => {
if (!group) return;
Clipboard.setString(String(group.id));
Alert.alert('已复制', '群号已复制到剪贴板');
};
const formatGroupNo = (id: string) => {
const raw = id;
if (raw.length <= 12) return raw;
return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
};
// 渲染设置项
const renderSettingItem = (
icon: string,
title: string,
value?: string,
onPress?: () => void,
danger: boolean = false
) => (
<TouchableOpacity
style={styles.settingItem}
onPress={onPress}
disabled={!onPress}
activeOpacity={0.7}
>
<View style={[styles.settingIconContainer, danger && styles.settingIconDanger]}>
<MaterialCommunityIcons
name={icon as any}
size={20}
color={danger ? colors.error.main : colors.primary.main}
/>
</View>
<View style={styles.settingContent}>
<Text variant="body" style={danger ? styles.dangerText : undefined}>
{title}
</Text>
{value && (
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
{value}
</Text>
)}
</View>
{onPress && (
<MaterialCommunityIcons
name="chevron-right"
size={22}
color={colors.text.hint}
/>
)}
</TouchableOpacity>
);
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<Loading />
</SafeAreaView>
);
}
if (!group) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<View style={styles.errorContainer}>
<Text variant="body" color={colors.text.secondary}>
</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.pageHeader}>
<AppBackButton onPress={() => router.back()} />
<Text variant="h3" style={styles.pageTitle}></Text>
<View style={styles.pageHeaderPlaceholder} />
</View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 群组基本信息 */}
<View style={styles.headerCard}>
<View style={styles.groupHeader}>
<Avatar
source={group.avatar}
size={80}
name={group.name}
/>
<View style={styles.groupInfo}>
<Text variant="h3" style={styles.groupName} numberOfLines={1}>
{group.name}
</Text>
<View style={styles.groupMeta}>
<MaterialCommunityIcons name="account-group" size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary}>
{group.member_count}
</Text>
<Text variant="caption" color={colors.text.hint}> · </Text>
<MaterialCommunityIcons name={getJoinTypeIcon(group.join_type) as any} size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} numberOfLines={1} style={{ flex: 1 }}>
{getJoinTypeText(group.join_type)}
</Text>
</View>
<View style={styles.groupNoRow}>
<Text variant="caption" color={colors.text.secondary}>
{formatGroupNo(group.id)}
</Text>
<TouchableOpacity style={styles.copyGroupNoBtn} onPress={handleCopyGroupNo} activeOpacity={0.7}>
<MaterialCommunityIcons name="content-copy" size={14} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} style={styles.copyGroupNoBtnText}>
</Text>
</TouchableOpacity>
</View>
</View>
</View>
{group.description ? (
<View style={styles.descriptionContainer}>
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.secondary} />
<Text variant="body" color={colors.text.secondary} style={styles.groupDesc}>
{group.description}
</Text>
</View>
) : (
<View style={styles.descriptionContainer}>
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
<Text variant="body" color={colors.text.hint} style={styles.groupDesc}>
</Text>
</View>
)}
</View>
{/* 群公告 */}
{announcements.length > 0 && (
<View style={styles.card}>
<View style={styles.cardHeader}>
<View style={styles.cardIconContainer}>
<MaterialCommunityIcons name="bullhorn" size={18} color={colors.warning.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
</View>
<View style={styles.announcementContent}>
<Text variant="body" style={styles.announcementText}>
{announcements[0].content}
</Text>
<Text variant="caption" color={colors.text.hint} style={styles.announcementTime}>
{(() => {
const d = new Date(announcements[0].created_at);
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
})()}
</Text>
</View>
</View>
)}
{/* 成员预览 */}
<View style={styles.card}>
<TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}>
<View style={styles.cardIconContainer}>
<MaterialCommunityIcons name="account-multiple" size={18} color={colors.info.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
<Text variant="caption" color={colors.text.secondary} style={styles.memberCount}>
{group.member_count}
</Text>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
<View style={styles.memberPreview}>
{members.slice(0, 8).map((member, index) => (
<View
key={member.id}
style={[
styles.memberAvatar,
index === 0 && styles.memberAvatarFirst,
{ zIndex: index + 1 },
]}
>
<Avatar
source={member.user?.avatar}
size={44}
name={member.user?.nickname || member.nickname}
/>
{member.role === 'owner' && (
<View style={styles.ownerBadge}>
<Text variant="caption" color={colors.background.paper} style={styles.ownerBadgeText}></Text>
</View>
)}
</View>
))}
{group.member_count > 8 && (
<View style={styles.moreMembers}>
<Text
variant="caption"
color={colors.text.secondary}
style={styles.moreMembersText}
numberOfLines={1}
>
+{group.member_count - 8}
</Text>
</View>
)}
</View>
{/* 邀请成员按钮 */}
<TouchableOpacity style={styles.inviteButton} onPress={openInviteModal} activeOpacity={0.8}>
<View style={styles.inviteIconContainer}>
<MaterialCommunityIcons name="account-plus" size={20} color={colors.primary.main} />
</View>
<Text variant="body" color={colors.primary.main} style={styles.inviteButtonText}>
</Text>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.primary.main} />
</TouchableOpacity>
</View>
{/* 管理员设置 */}
{isAdmin && (
<View style={styles.card}>
<View style={styles.cardHeader}>
<View style={[styles.cardIconContainer, { backgroundColor: colors.primary.light + '20' }]}>
<MaterialCommunityIcons name="shield-crown" size={18} color={colors.primary.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
</View>
<View style={styles.settingsList}>
{renderSettingItem('pencil-outline', '修改群信息', undefined, openEditModal)}
<Divider style={styles.settingDivider} />
{isOwner && renderSettingItem('earth', '加群方式', getJoinTypeText(group.join_type), () => setJoinTypeModalVisible(true))}
{isOwner && <Divider style={styles.settingDivider} />}
{isOwner && (
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="microphone-off" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{group.mute_all ? '已开启' : '已关闭'}
</Text>
</View>
<Switch
value={group.mute_all}
onValueChange={handleToggleMuteAll}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={group.mute_all ? colors.primary.main : colors.background.paper}
/>
</View>
)}
<Divider style={styles.settingDivider} />
{renderSettingItem('bullhorn-outline', '发布群公告', undefined, () => setAnnouncementModalVisible(true))}
{isOwner && <Divider style={styles.settingDivider} />}
{isOwner && renderSettingItem('account-switch', '转让群主', undefined, () => setTransferModalVisible(true))}
</View>
</View>
)}
{/* 聊天设置 */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<View style={[styles.cardIconContainer, { backgroundColor: colors.warning.light + '20' }]}>
<MaterialCommunityIcons name="chat-outline" size={18} color={colors.warning.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
</View>
<View style={styles.settingsList}>
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="pin-outline" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{conversationId ? (isPinned ? '已置顶' : '未置顶') : '请从聊天页面进入后设置'}
</Text>
</View>
<Switch
value={isPinned}
onValueChange={handleTogglePinned}
disabled={!conversationId || pinLoading}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
/>
</View>
<Divider style={styles.settingDivider} />
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{conversationId ? (isNotificationMuted ? '已开启' : '未开启') : '请从聊天页面进入后设置'}
</Text>
</View>
<Switch
value={isNotificationMuted}
onValueChange={handleToggleNotificationMuted}
disabled={!conversationId}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={isNotificationMuted ? colors.primary.main : colors.background.paper}
/>
</View>
</View>
</View>
{/* 危险操作区域 */}
<View style={[styles.card, styles.dangerCard]}>
{isOwner ? (
<TouchableOpacity
style={styles.dangerButton}
onPress={handleDissolveGroup}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="delete-forever" size={22} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.dangerButtonText}>
</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.dangerButton}
onPress={handleLeaveGroup}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="logout" size={22} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.dangerButtonText}>
退
</Text>
</TouchableOpacity>
)}
</View>
</ScrollView>
{/* 编辑群信息模态框 */}
<Modal
visible={editModalVisible}
animationType="slide"
transparent
onRequestClose={() => setEditModalVisible(false)}
>
<KeyboardAvoidingView
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setEditModalVisible(false)}>
<Text variant="body" color={colors.text.secondary}></Text>
</TouchableOpacity>
<Text variant="h3" style={styles.modalTitle}></Text>
<TouchableOpacity onPress={handleSaveGroupInfo} disabled={saving}>
<Text variant="body" color={saving ? colors.text.disabled : colors.primary.main} style={styles.saveButton}>
{saving ? '保存中...' : '保存'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.editForm}>
<View style={styles.editAvatarContainer}>
<TouchableOpacity onPress={handleSelectGroupAvatar} disabled={uploadingAvatar}>
{uploadingAvatar ? (
<View style={[styles.editAvatarPlaceholder, { width: 90, height: 90 }]}>
<Loading />
</View>
) : editAvatar ? (
<Image source={{ uri: editAvatar }} style={styles.editAvatarImage} />
) : (
<Avatar source={undefined} size={90} name={editName || group.name} />
)}
</TouchableOpacity>
<TouchableOpacity style={styles.editAvatarButton} onPress={handleSelectGroupAvatar} disabled={uploadingAvatar}>
<MaterialCommunityIcons name="camera" size={16} color={colors.background.paper} />
</TouchableOpacity>
</View>
<View style={styles.editInputGroup}>
<Text variant="label" style={styles.editLabel}></Text>
<TextInput
style={styles.editInput}
value={editName}
onChangeText={setEditName}
placeholder="请输入群名称"
placeholderTextColor={colors.text.hint}
maxLength={50}
/>
<Text variant="caption" color={colors.text.hint} style={styles.editCharCount}>
{editName.length}/50
</Text>
</View>
<View style={styles.editInputGroup}>
<Text variant="label" style={styles.editLabel}></Text>
<TextInput
style={[styles.editInput, styles.editTextArea]}
value={editDescription}
onChangeText={setEditDescription}
placeholder="介绍一下这个群聊..."
placeholderTextColor={colors.text.hint}
maxLength={500}
multiline
numberOfLines={4}
textAlignVertical="top"
/>
<Text variant="caption" color={colors.text.hint} style={styles.editCharCount}>
{editDescription.length}/500
</Text>
</View>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
{/* 发布群公告模态框 */}
<Modal
visible={announcementModalVisible}
animationType="slide"
transparent
onRequestClose={() => setAnnouncementModalVisible(false)}
>
<KeyboardAvoidingView
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setAnnouncementModalVisible(false)}>
<Text variant="body" color={colors.text.secondary}></Text>
</TouchableOpacity>
<Text variant="h3" style={styles.modalTitle}></Text>
<TouchableOpacity onPress={handlePublishAnnouncement} disabled={saving}>
<Text variant="body" color={saving ? colors.text.disabled : colors.primary.main} style={styles.saveButton}>
{saving ? '发布中...' : '发布'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.announcementForm}>
<TextInput
style={styles.announcementInput}
value={newAnnouncement}
onChangeText={setNewAnnouncement}
placeholder="请输入群公告内容..."
placeholderTextColor={colors.text.hint}
maxLength={500}
multiline
numberOfLines={8}
textAlignVertical="top"
/>
<Text variant="caption" color={colors.text.hint} style={styles.announcementCharCount}>
{newAnnouncement.length}/500
</Text>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
{/* 加群方式模态框 */}
<Modal
visible={joinTypeModalVisible}
animationType="slide"
transparent
onRequestClose={() => setJoinTypeModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setJoinTypeModalVisible(false)}>
<Text variant="body" color={colors.text.secondary}></Text>
</TouchableOpacity>
<Text variant="h3" style={styles.modalTitle}></Text>
<View style={{ width: 40 }} />
</View>
<View style={styles.joinTypeList}>
{JOIN_TYPE_OPTIONS.map((option) => (
<TouchableOpacity
key={option.value}
style={[
styles.joinTypeItem,
group.join_type === option.value && styles.joinTypeItemSelected,
]}
onPress={() => handleSetJoinType(option.value)}
activeOpacity={0.7}
>
<View style={[styles.joinTypeIcon, group.join_type === option.value && styles.joinTypeIconSelected]}>
<MaterialCommunityIcons
name={option.icon as any}
size={24}
color={group.join_type === option.value ? colors.primary.main : colors.text.secondary}
/>
</View>
<View style={styles.joinTypeInfo}>
<Text
variant="body"
style={group.join_type === option.value ? styles.joinTypeTextSelected : undefined}
>
{option.label}
</Text>
<Text variant="caption" color={colors.text.secondary}>
{option.desc}
</Text>
</View>
{group.join_type === option.value && (
<MaterialCommunityIcons name="check-circle" size={24} color={colors.primary.main} />
)}
</TouchableOpacity>
))}
</View>
</View>
</View>
</Modal>
{/* 转让群主模态框 */}
<Modal
visible={transferModalVisible}
animationType="slide"
transparent
onRequestClose={() => setTransferModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setTransferModalVisible(false)}>
<Text variant="body" color={colors.text.secondary}></Text>
</TouchableOpacity>
<Text variant="h3" style={styles.modalTitle}></Text>
<View style={{ width: 40 }} />
</View>
<Text variant="caption" color={colors.text.secondary} style={styles.transferDesc}>
</Text>
<ScrollView style={styles.transferList} showsVerticalScrollIndicator={false}>
{members
.filter(m => m.role === 'admin' || m.role === 'member')
.map((member) => (
<TouchableOpacity
key={member.id}
style={[
styles.transferItem,
transferUserId === member.user_id && styles.transferItemSelected,
]}
onPress={() => setTransferUserId(member.user_id)}
activeOpacity={0.7}
>
<Avatar
source={member.user?.avatar}
size={48}
name={member.user?.nickname || member.nickname}
/>
<View style={styles.transferItemInfo}>
<Text variant="body">{member.user?.nickname || member.nickname}</Text>
<View style={styles.transferRoleBadge}>
<Text variant="caption" color={colors.primary.main}>
{member.role === 'admin' ? '管理员' : '成员'}
</Text>
</View>
</View>
<View style={[styles.transferRadio, transferUserId === member.user_id && styles.transferRadioSelected]}>
{transferUserId === member.user_id && (
<MaterialCommunityIcons name="check" size={14} color={colors.background.paper} />
)}
</View>
</TouchableOpacity>
))}
</ScrollView>
<Button
title="确认转让"
onPress={handleTransferOwner}
disabled={!transferUserId}
fullWidth
style={styles.transferButton}
/>
</View>
</View>
</Modal>
<MutualFollowSelectorModal
visible={inviteModalVisible}
title="邀请成员"
confirmText="确认"
confirmingText="发送中..."
confirmLoading={inviting}
excludeUserIds={members.map(member => member.user_id)}
onClose={() => setInviteModalVisible(false)}
onConfirm={handleInviteMembers}
/>
</SafeAreaView>
);
};
function createGroupInfoStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
pageHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: 0,
},
pageTitle: {
fontWeight: '600',
fontSize: fontSizes.xl,
},
pageHeaderPlaceholder: {
width: 40,
height: 40,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl * 2,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
// 头部区域 - 扁平化无卡片
headerCard: {
backgroundColor: colors.background.paper,
paddingVertical: spacing.lg,
marginBottom: spacing.md,
},
groupHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.lg,
},
groupInfo: {
marginLeft: spacing.lg,
flex: 1,
},
groupName: {
fontWeight: '700',
fontSize: fontSizes['2xl'],
marginBottom: spacing.xs,
},
groupMeta: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
marginBottom: spacing.xs,
},
groupNoRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
copyGroupNoBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '15',
},
copyGroupNoBtnText: {
marginLeft: 4,
fontWeight: '500',
},
descriptionContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.md,
},
groupDesc: {
marginLeft: spacing.sm,
flex: 1,
lineHeight: 22,
fontWeight: '400',
},
// 通用卡片样式 - 扁平化:无阴影无圆角,使用分割线
card: {
backgroundColor: colors.background.paper,
marginBottom: spacing.md,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
paddingTop: spacing.sm,
},
cardIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.info.light + '15',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
fontSize: fontSizes.md,
flex: 1,
},
// 公告样式 - 扁平化
announcementContent: {
backgroundColor: colors.warning.light + '08',
borderRadius: borderRadius.lg,
padding: spacing.md,
borderLeftWidth: 3,
borderLeftColor: colors.warning.main,
},
announcementText: {
lineHeight: 22,
marginBottom: spacing.sm,
fontWeight: '400',
},
announcementTime: {
textAlign: 'right',
fontWeight: '500',
},
// 成员预览样式
memberCount: {
marginRight: spacing.xs,
fontWeight: '600',
color: colors.text.secondary,
},
memberPreview: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'nowrap',
marginBottom: spacing.md,
paddingLeft: spacing.xs,
},
memberAvatar: {
position: 'relative',
marginLeft: -8,
},
memberAvatarFirst: {
marginLeft: 0,
},
ownerBadge: {
position: 'absolute',
bottom: -2,
right: -2,
backgroundColor: colors.warning.main,
borderRadius: borderRadius.sm,
paddingHorizontal: 4,
paddingVertical: 1,
borderWidth: 2,
borderColor: colors.background.paper,
},
ownerBadgeText: {
fontSize: 8,
fontWeight: '800',
},
moreMembers: {
minWidth: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.divider + '60',
marginLeft: -8,
paddingHorizontal: spacing.xs,
},
moreMembersText: {
fontWeight: '700',
includeFontPadding: false,
fontSize: fontSizes.sm,
},
// 邀请按钮样式 - 扁平化
inviteButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.light + '10',
borderWidth: 1.5,
borderColor: colors.primary.light + '60',
borderStyle: 'dashed',
},
inviteIconContainer: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.sm,
},
inviteButtonText: {
fontWeight: '700',
marginRight: spacing.xs,
},
// 设置项样式 - 扁平化:无圆角背景,简洁分割线
settingsList: {
marginTop: 0,
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: 0,
marginLeft: 0,
marginRight: 0,
},
settingIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '15',
},
settingContent: {
flex: 1,
},
dangerText: {
color: colors.error.main,
fontWeight: '600',
},
settingDivider: {
marginLeft: 52,
width: 'auto',
marginVertical: 0,
opacity: 0.5,
},
// 危险操作区域 - 扁平化
dangerCard: {
padding: 0,
overflow: 'hidden',
marginTop: spacing.sm,
},
dangerButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.lg,
gap: spacing.sm,
},
dangerButtonText: {
fontWeight: '600',
},
// 模态框样式 - 扁平化
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.45)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius['2xl'],
borderTopRightRadius: borderRadius['2xl'],
height: '80%',
paddingHorizontal: spacing.lg,
paddingTop: spacing.md,
paddingBottom: spacing.xl,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.lg,
paddingBottom: spacing.md,
borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '60',
},
modalHeaderButton: {
paddingVertical: spacing.sm,
minWidth: 60,
},
modalTitle: {
fontWeight: '700',
fontSize: fontSizes.xl,
},
saveButton: {
fontWeight: '600',
},
confirmButton: {
fontWeight: '600',
textAlign: 'right',
},
// 编辑表单样式 - 扁平化
editForm: {
alignItems: 'center',
},
editAvatarContainer: {
position: 'relative',
marginBottom: spacing.xl,
},
editAvatarImage: {
width: 90,
height: 90,
borderRadius: 45,
borderWidth: 2,
borderColor: colors.primary.light + '40',
},
editAvatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: 45,
borderWidth: 2,
borderColor: colors.divider,
borderStyle: 'dashed',
},
editAvatarButton: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.primary.main,
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 3,
borderColor: colors.background.paper,
},
editInputGroup: {
width: '100%',
marginBottom: spacing.lg,
},
editLabel: {
marginBottom: spacing.sm,
fontWeight: '600',
fontSize: fontSizes.sm,
},
editInput: {
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.md,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider + '80',
fontWeight: '400',
},
editTextArea: {
minHeight: 100,
paddingTop: spacing.md,
lineHeight: 22,
},
editCharCount: {
textAlign: 'right',
marginTop: spacing.xs,
fontWeight: '500',
},
// 公告表单样式
announcementForm: {
flex: 1,
},
announcementInput: {
flex: 1,
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.md,
fontSize: fontSizes.md,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider + '80',
lineHeight: 22,
textAlignVertical: 'top',
},
announcementCharCount: {
textAlign: 'right',
marginTop: spacing.sm,
fontWeight: '500',
},
// 加群方式样式 - 更现代的选中状态
joinTypeList: {
gap: spacing.md,
},
joinTypeItem: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider + '80',
backgroundColor: colors.background.default,
},
joinTypeItemSelected: {
borderColor: colors.primary.main,
backgroundColor: colors.primary.light + '12',
borderWidth: 1.5,
},
joinTypeIcon: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.background.paper,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
joinTypeIconSelected: {
backgroundColor: colors.primary.light + '25',
},
joinTypeInfo: {
flex: 1,
},
joinTypeTextSelected: {
fontWeight: '700',
color: colors.primary.main,
},
// 转让群主样式
transferDesc: {
marginBottom: spacing.md,
fontWeight: '500',
},
transferList: {
flex: 1,
marginBottom: spacing.md,
},
transferItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.lg,
marginBottom: spacing.sm,
backgroundColor: colors.background.default,
},
transferItemSelected: {
backgroundColor: colors.primary.light + '12',
borderWidth: 1,
borderColor: colors.primary.light + '60',
},
transferItemInfo: {
flex: 1,
marginLeft: spacing.md,
},
transferRoleBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.sm,
paddingVertical: 2,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginTop: spacing.xs,
},
transferRadio: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.paper,
},
transferRadioSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
transferButton: {
marginTop: spacing.sm,
},
// 邀请成员模态框样式
tabContainer: {
flexDirection: 'row',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.xs,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.divider + '60',
},
tab: {
flex: 1,
paddingVertical: spacing.sm,
alignItems: 'center',
borderRadius: borderRadius.md,
},
tabActive: {
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider + '60',
},
tabTextActive: {
fontWeight: '700',
},
selectedBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginBottom: spacing.md,
},
friendListContent: {
paddingBottom: spacing.xl,
},
friendItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.sm,
backgroundColor: colors.background.default,
},
friendItemSelected: {
backgroundColor: colors.primary.light + '12',
borderWidth: 1,
borderColor: colors.primary.light + '60',
},
friendInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
marginBottom: 2,
},
checkbox: {
width: 26,
height: 26,
borderRadius: 13,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.paper,
},
checkboxSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
});
}
export default GroupInfoScreen;