2026-04-25 15:09:00 +08:00
|
|
|
|
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
TextInput,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
ActivityIndicator,
|
|
|
|
|
|
Clipboard,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
RefreshControl,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
Animated,
|
|
|
|
|
|
KeyboardAvoidingView,
|
|
|
|
|
|
Platform,
|
|
|
|
|
|
ScrollView,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
} from 'react-native';
|
2026-04-25 15:09:00 +08:00
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import { useRouter } from 'expo-router';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
|
2026-03-30 17:53:30 +08:00
|
|
|
|
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import Avatar from '../../components/common/Avatar';
|
|
|
|
|
|
import Text from '../../components/common/Text';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { groupService } from '@/services/message';
|
2026-04-13 04:56:58 +08:00
|
|
|
|
import { groupManager } from '../../stores/group';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { GroupResponse, JoinType } from '../../types/dto';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
|
|
|
|
|
import { EmptyState } from '../../components/common';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
function createJoinGroupStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
keyboardView: {
|
|
|
|
|
|
flex: 1,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
scrollContent: {
|
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
|
paddingHorizontal: 28,
|
|
|
|
|
|
paddingTop: 40,
|
|
|
|
|
|
paddingBottom: 40,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
content: {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
flex: 1,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
maxWidth: 400,
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
alignSelf: 'center',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
|
|
|
|
|
|
// 标题区域
|
|
|
|
|
|
titleSection: {
|
|
|
|
|
|
marginBottom: 32,
|
|
|
|
|
|
marginTop: 12,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
title: {
|
|
|
|
|
|
fontSize: 28,
|
|
|
|
|
|
fontWeight: '700',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
color: colors.text.primary,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
lineHeight: 36,
|
|
|
|
|
|
},
|
|
|
|
|
|
underline: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 4,
|
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
|
marginTop: 12,
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
},
|
|
|
|
|
|
subtitle: {
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索区域
|
|
|
|
|
|
searchSection: {
|
|
|
|
|
|
marginBottom: 24,
|
|
|
|
|
|
},
|
|
|
|
|
|
inputLabel: {
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
marginBottom: 10,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
searchRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
inputWrapper: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
paddingHorizontal: 18,
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: 'transparent',
|
|
|
|
|
|
},
|
|
|
|
|
|
input: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
height: 56,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
searchBtn: {
|
2026-04-25 15:09:00 +08:00
|
|
|
|
width: 56,
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
borderRadius: 14,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginLeft: spacing.sm,
|
|
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
searchBtnDisabled: {
|
|
|
|
|
|
opacity: 0.5,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索结果区域
|
2026-03-25 05:16:54 +08:00
|
|
|
|
searchResultSection: {
|
2026-04-25 15:09:00 +08:00
|
|
|
|
marginBottom: 24,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
sectionTitle: {
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
marginBottom: 12,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
emptyText: {
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
color: colors.text.hint,
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
paddingVertical: spacing.lg,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
|
|
|
|
|
|
// 群卡片
|
2026-03-25 05:16:54 +08:00
|
|
|
|
groupCard: {
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
borderRadius: 16,
|
|
|
|
|
|
padding: spacing.lg,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
marginBottom: spacing.sm,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
groupMeta: {
|
|
|
|
|
|
marginLeft: spacing.md,
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupName: {
|
2026-03-30 17:53:30 +08:00
|
|
|
|
fontWeight: '700',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
marginBottom: 2,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
groupDesc: {
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
color: colors.text.secondary,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
lineHeight: 20,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
marginTop: spacing.sm,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
groupInfoRow: {
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
groupInfoText: {
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
2026-03-25 05:16:54 +08:00
|
|
|
|
groupNoRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
groupNoText: {
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
2026-03-25 05:16:54 +08:00
|
|
|
|
copyBtn: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.sm,
|
|
|
|
|
|
paddingVertical: 4,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
backgroundColor: colors.primary.light + '15',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
copyBtnText: {
|
|
|
|
|
|
marginLeft: 4,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 12,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
fontWeight: '600',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
color: colors.primary.main,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
submitBtn: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
borderRadius: 14,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
backgroundColor: colors.primary.main,
|
2026-04-25 15:09:00 +08:00
|
|
|
|
height: 48,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
submitBtnDisabled: {
|
|
|
|
|
|
opacity: 0.5,
|
|
|
|
|
|
},
|
|
|
|
|
|
submitText: {
|
|
|
|
|
|
marginLeft: spacing.xs,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
fontWeight: '700',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
color: colors.text.inverse,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
|
|
|
|
|
|
// 推荐区域
|
|
|
|
|
|
recommendSection: {
|
|
|
|
|
|
flex: 1,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
loadingFooter: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
loadMoreBtn: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
2026-04-25 15:09:00 +08:00
|
|
|
|
loadMoreText: {
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
color: colors.primary.main,
|
|
|
|
|
|
},
|
2026-03-25 05:16:54 +08:00
|
|
|
|
noMoreText: {
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
paddingVertical: spacing.md,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
fontWeight: '500',
|
2026-04-25 15:09:00 +08:00
|
|
|
|
fontSize: 13,
|
2026-03-30 17:53:30 +08:00
|
|
|
|
color: colors.text.hint,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const JoinGroupScreen: React.FC = () => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const styles = useMemo(() => createJoinGroupStyles(colors), [colors]);
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [keyword, setKeyword] = useState('');
|
|
|
|
|
|
const [searching, setSearching] = useState(false);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const [joiningGroupId, setJoiningGroupId] = useState<string | null>(null);
|
|
|
|
|
|
const [searchedGroup, setSearchedGroup] = useState<GroupResponse | null>(null);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [searched, setSearched] = useState(false);
|
|
|
|
|
|
|
2026-04-25 15:09:00 +08:00
|
|
|
|
// 动画值
|
|
|
|
|
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
|
|
const slideAnim = useRef(new Animated.Value(20)).current;
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
Animated.parallel([
|
|
|
|
|
|
Animated.timing(fadeAnim, {
|
|
|
|
|
|
toValue: 1,
|
|
|
|
|
|
duration: 400,
|
|
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
Animated.timing(slideAnim, {
|
|
|
|
|
|
toValue: 0,
|
|
|
|
|
|
duration: 400,
|
|
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
]).start();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 使用游标分页 Hook 管理群组列表
|
|
|
|
|
|
const {
|
2026-03-23 03:58:26 +08:00
|
|
|
|
list: groups,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
isLoading,
|
|
|
|
|
|
isRefreshing,
|
|
|
|
|
|
hasMore,
|
|
|
|
|
|
loadMore,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
error,
|
|
|
|
|
|
} = useCursorPagination(
|
|
|
|
|
|
async ({ cursor, pageSize }) => {
|
|
|
|
|
|
return await groupService.getGroupsCursor({
|
|
|
|
|
|
cursor,
|
|
|
|
|
|
page_size: pageSize,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
{ pageSize: 20 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const getJoinTypeText = (joinType: JoinType) => {
|
|
|
|
|
|
if (joinType === 0) return '允许加入';
|
|
|
|
|
|
if (joinType === 1) return '需要审批';
|
|
|
|
|
|
return '禁止加入';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = async () => {
|
|
|
|
|
|
const trimmed = keyword.trim();
|
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
|
Alert.alert('提示', '请输入群ID进行搜索');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSearching(true);
|
|
|
|
|
|
setSearched(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await groupManager.getGroup(trimmed, true);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
setSearchedGroup(result);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error: any) {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
setSearchedGroup(null);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const message = error?.response?.data?.message || error?.message || '';
|
|
|
|
|
|
if (String(message).includes('不存在') || error?.response?.status === 404) {
|
|
|
|
|
|
Alert.alert('未找到', '未搜索到该群聊,请确认群ID是否正确');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Alert.alert('搜索失败', '请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSearching(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const handleJoin = async (group: GroupResponse) => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (!group?.id) return;
|
2026-03-20 23:00:27 +08:00
|
|
|
|
setJoiningGroupId(String(group.id));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await groupService.joinGroup(group.id);
|
|
|
|
|
|
Alert.alert('成功', '操作已提交', [
|
|
|
|
|
|
{
|
|
|
|
|
|
text: '确定',
|
2026-03-24 14:21:31 +08:00
|
|
|
|
onPress: () => router.back(),
|
2026-03-09 21:29:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
const message =
|
|
|
|
|
|
error?.response?.data?.message ||
|
|
|
|
|
|
error?.message ||
|
|
|
|
|
|
'操作失败,请稍后重试';
|
|
|
|
|
|
Alert.alert('操作失败', String(message));
|
|
|
|
|
|
} finally {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
setJoiningGroupId(null);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const handleCopyGroupId = (groupId: string) => {
|
|
|
|
|
|
Clipboard.setString(groupId);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
Alert.alert('已复制', '群号已复制到剪贴板');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const formatGroupNo = (id: string) => {
|
|
|
|
|
|
const raw = id;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (raw.length <= 12) return raw;
|
|
|
|
|
|
return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const renderGroupItem = ({ item: group }: { item: GroupResponse }) => {
|
|
|
|
|
|
const isJoining = joiningGroupId === String(group.id);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.groupCard}>
|
|
|
|
|
|
<View style={styles.groupHeader}>
|
|
|
|
|
|
<Avatar source={group.avatar} size={52} name={group.name} />
|
|
|
|
|
|
<View style={styles.groupMeta}>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.groupName} numberOfLines={1}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{group.name}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{!!group.description && (
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.groupDesc}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{group.description}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<View style={styles.groupInfoRow}>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.groupInfoText}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
成员 {group.member_count}/{group.max_members}
|
|
|
|
|
|
</Text>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.groupInfoText}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{getJoinTypeText(group.join_type)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.groupNoRow}>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.groupNoText}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
群号:{formatGroupNo(group.id)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<TouchableOpacity style={styles.copyBtn} onPress={() => handleCopyGroupId(group.id)}>
|
|
|
|
|
|
<MaterialCommunityIcons name="content-copy" size={14} color={colors.primary.main} />
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.copyBtnText}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
复制
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[styles.submitBtn, isJoining && styles.submitBtnDisabled]}
|
|
|
|
|
|
onPress={() => handleJoin(group)}
|
|
|
|
|
|
disabled={isJoining}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isJoining ? (
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<ActivityIndicator color={colors.text.inverse} />
|
2026-03-20 23:00:27 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<MaterialCommunityIcons name="send-outline" size={18} color={colors.text.inverse} />
|
|
|
|
|
|
<Text style={styles.submitText}>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
申请入群
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderEmptyList = () => {
|
|
|
|
|
|
if (isLoading) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无群组"
|
|
|
|
|
|
description="还没有可加入的群组"
|
|
|
|
|
|
icon="account-group-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderSearchResult = () => {
|
|
|
|
|
|
if (!searched) return null;
|
|
|
|
|
|
|
|
|
|
|
|
if (searchedGroup) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.searchResultSection}>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<Text style={styles.sectionTitle}>搜索结果</Text>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
{renderGroupItem({ item: searchedGroup })}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!searching) {
|
|
|
|
|
|
return (
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<View style={styles.searchResultSection}>
|
|
|
|
|
|
<Text style={styles.sectionTitle}>搜索结果</Text>
|
|
|
|
|
|
<Text style={styles.emptyText}>
|
|
|
|
|
|
暂无搜索结果,请检查群ID后重试
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return (
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
|
|
|
|
<KeyboardAvoidingView
|
|
|
|
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
|
|
|
|
style={styles.keyboardView}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
contentContainerStyle={styles.scrollContent}
|
|
|
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={isRefreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Animated.View
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.content,
|
|
|
|
|
|
{
|
|
|
|
|
|
opacity: fadeAnim,
|
|
|
|
|
|
transform: [{ translateY: slideAnim }],
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
{/* 标题区域 */}
|
|
|
|
|
|
<View style={styles.titleSection}>
|
|
|
|
|
|
<Text style={styles.title}>加入群聊</Text>
|
|
|
|
|
|
<View style={styles.underline} />
|
|
|
|
|
|
<Text style={styles.subtitle}>搜索群ID或浏览推荐群组</Text>
|
|
|
|
|
|
</View>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-25 15:09:00 +08:00
|
|
|
|
{/* 搜索区域 */}
|
|
|
|
|
|
<View style={styles.searchSection}>
|
|
|
|
|
|
<Text style={styles.inputLabel}>搜索群聊(群ID)</Text>
|
|
|
|
|
|
<View style={styles.searchRow}>
|
|
|
|
|
|
<View style={styles.inputWrapper}>
|
|
|
|
|
|
<TextInput
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChangeText={setKeyword}
|
|
|
|
|
|
placeholder="例如:7391234567890"
|
|
|
|
|
|
placeholderTextColor={colors.text.hint}
|
|
|
|
|
|
style={styles.input}
|
|
|
|
|
|
editable={!searching && !joiningGroupId}
|
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
|
cursorColor={colors.primary.main}
|
|
|
|
|
|
selectionColor={`${colors.primary.main}40`}
|
|
|
|
|
|
/>
|
2026-03-20 23:00:27 +08:00
|
|
|
|
</View>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.searchBtn,
|
|
|
|
|
|
(!keyword.trim() || searching || !!joiningGroupId) && styles.searchBtnDisabled,
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={handleSearch}
|
|
|
|
|
|
disabled={!keyword.trim() || searching || !!joiningGroupId}
|
|
|
|
|
|
>
|
|
|
|
|
|
{searching ? (
|
|
|
|
|
|
<ActivityIndicator color={colors.text.inverse} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<MaterialCommunityIcons name="magnify" size={22} color={colors.text.inverse} />
|
|
|
|
|
|
)}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
</TouchableOpacity>
|
2026-04-25 15:09:00 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 搜索结果 */}
|
|
|
|
|
|
{renderSearchResult()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 推荐群组 */}
|
|
|
|
|
|
<View style={styles.recommendSection}>
|
|
|
|
|
|
<Text style={styles.sectionTitle}>推荐群组</Text>
|
|
|
|
|
|
{groups.length === 0 && !isLoading ? (
|
|
|
|
|
|
renderEmptyList()
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{groups.map((group) => (
|
|
|
|
|
|
<View key={String(group.id)}>
|
|
|
|
|
|
{renderGroupItem({ item: group })}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{isLoading && (
|
|
|
|
|
|
<View style={styles.loadingFooter}>
|
|
|
|
|
|
<ActivityIndicator color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{hasMore && !isLoading && (
|
|
|
|
|
|
<TouchableOpacity style={styles.loadMoreBtn} onPress={loadMore}>
|
|
|
|
|
|
<Text style={styles.loadMoreText}>
|
|
|
|
|
|
加载更多
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!hasMore && groups.length > 0 && (
|
|
|
|
|
|
<Text style={styles.noMoreText}>
|
|
|
|
|
|
没有更多群组了
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</Animated.View>
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</KeyboardAvoidingView>
|
|
|
|
|
|
</SafeAreaView>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default JoinGroupScreen;
|