refactor(platform): extract blurActiveElement to infrastructure module
- Centralize duplicated Platform.OS === 'web' blur logic across 16 components - Add blurActiveElement utility to src/infrastructure/platform module - Optimize MessageBubble and ImageSegment with React.memo custom comparators - Add useMemo for computed values in MessageSegmentsRenderer - Update DTO types with is_system_notice and notice_content fields - Fix keyboard dismissal order in useChatScreen handleDismiss - Simplify userStore follow/unfollow state updates - Remove unnecessary TypeScript type assertions throughout codebase
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { blurActiveElement } from '../../infrastructure/platform';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { groupService } from '../../services/groupService';
|
||||
@@ -47,9 +48,8 @@ const CreateGroupScreen: React.FC = () => {
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inviteModalVisible && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
if (inviteModalVisible) {
|
||||
blurActiveElement();
|
||||
}
|
||||
}, [inviteModalVisible]);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
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/messageManager';
|
||||
@@ -119,9 +120,8 @@ const GroupInfoScreen: React.FC = () => {
|
||||
const [inviting, setInviting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ((editModalVisible || announcementModalVisible || transferModalVisible || joinTypeModalVisible || inviteModalVisible) && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
if (editModalVisible || announcementModalVisible || transferModalVisible || joinTypeModalVisible || inviteModalVisible) {
|
||||
blurActiveElement();
|
||||
}
|
||||
}, [editModalVisible, announcementModalVisible, transferModalVisible, joinTypeModalVisible, inviteModalVisible]);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { blurActiveElement } from '../../infrastructure/platform';
|
||||
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { groupService } from '../../services/groupService';
|
||||
@@ -133,9 +134,8 @@ const GroupMembersScreen: React.FC = () => {
|
||||
const [newNickname, setNewNickname] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if ((actionModalVisible || nicknameModalVisible) && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
if (actionModalVisible || nicknameModalVisible) {
|
||||
blurActiveElement();
|
||||
}
|
||||
}, [actionModalVisible, nicknameModalVisible]);
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
} from '../../theme';
|
||||
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto';
|
||||
import { authService } from '../../services';
|
||||
import { blurActiveElement } from '../../infrastructure/platform';
|
||||
import { useUserStore, useAuthStore } from '../../stores';
|
||||
// 【新架构】使用MessageManager hooks(会话列表数据源自 MessageManager 游标同步)
|
||||
import {
|
||||
@@ -138,9 +139,8 @@ export const MessageListScreen: React.FC = () => {
|
||||
const [actionMenuVisible, setActionMenuVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionMenuVisible && Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
if (actionMenuVisible) {
|
||||
blurActiveElement();
|
||||
}
|
||||
}, [actionMenuVisible]);
|
||||
const [scannerVisible, setScannerVisible] = useState(false);
|
||||
|
||||
@@ -15,15 +15,10 @@ import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive
|
||||
import { EMOJIS } from './constants';
|
||||
import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '../../../../services/stickerService';
|
||||
import { useAppColors, spacing } from '../../../../theme';
|
||||
import { blurActiveElement } from '../../../../infrastructure/platform';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
const blurActiveElementOnWeb = () => {
|
||||
if (Platform.OS !== 'web') return;
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
};
|
||||
|
||||
// 表情尺寸配置 - 固定宽度 40px,使用 flex 布局
|
||||
const EMOJI_SIZES = {
|
||||
mobile: { size: 24, itemWidth: 40, itemHeight: 40 },
|
||||
@@ -182,7 +177,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
|
||||
// 打开管理界面
|
||||
const handleOpenManage = () => {
|
||||
blurActiveElementOnWeb();
|
||||
blurActiveElement();
|
||||
setShowManageModal(true);
|
||||
setManageMode(false);
|
||||
setSelectedStickers(new Set());
|
||||
|
||||
@@ -20,6 +20,7 @@ import { LongPressMenuProps } from './types';
|
||||
import { RECALL_TIME_LIMIT } from './constants';
|
||||
import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto';
|
||||
import { isStickerExists, addStickerFromUrl } from '../../../../services/stickerService';
|
||||
import { blurActiveElement } from '../../../../infrastructure/platform';
|
||||
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
@@ -37,16 +38,11 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
||||
const styles = useChatScreenStyles();
|
||||
const scaleAnimation = useRef(new Animated.Value(0)).current;
|
||||
const opacityAnimation = useRef(new Animated.Value(0)).current;
|
||||
const blurActiveElementOnWeb = () => {
|
||||
if (Platform.OS !== 'web') return;
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
};
|
||||
|
||||
// 显示动画 - 缩放弹出
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
blurActiveElementOnWeb();
|
||||
blurActiveElement();
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnimation, {
|
||||
toValue: 1,
|
||||
@@ -61,7 +57,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
blurActiveElementOnWeb();
|
||||
blurActiveElement();
|
||||
Animated.parallel([
|
||||
Animated.timing(scaleAnimation, {
|
||||
toValue: 0,
|
||||
@@ -251,12 +247,12 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
onShow={blurActiveElementOnWeb}
|
||||
onShow={blurActiveElement}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
blurActiveElementOnWeb();
|
||||
blurActiveElement();
|
||||
onClose();
|
||||
}}
|
||||
style={styles.qqMenuOverlay}
|
||||
|
||||
@@ -31,7 +31,7 @@ const MAX_WIDTH_RATIO = {
|
||||
desktop: 0.55,
|
||||
};
|
||||
|
||||
export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
message,
|
||||
index,
|
||||
currentUserId,
|
||||
@@ -481,11 +481,37 @@ const segmentStyles = StyleSheet.create({
|
||||
minWidth: 120,
|
||||
},
|
||||
pureImageBubble: {
|
||||
// 纯图片消息:去除内边距,让图片紧贴气泡边缘
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export const MessageBubble = React.memo(MessageBubbleInner, (prev, next) => {
|
||||
if (prev.message.id !== next.message.id) return false;
|
||||
if (prev.message.status !== next.message.status) return false;
|
||||
if (prev.message.segments !== next.message.segments) return false;
|
||||
if (prev.message.sender_id !== next.message.sender_id) return false;
|
||||
if (prev.message.is_system_notice !== next.message.is_system_notice) return false;
|
||||
if (prev.message.category !== next.message.category) return false;
|
||||
if (prev.message.seq !== next.message.seq) return false;
|
||||
if (prev.message.sender !== next.message.sender) return false;
|
||||
if (prev.message.notice_content !== next.message.notice_content) return false;
|
||||
if (prev.index !== next.index) return false;
|
||||
if (prev.currentUserId !== next.currentUserId) return false;
|
||||
if (prev.otherUserLastReadSeq !== next.otherUserLastReadSeq) return false;
|
||||
if (prev.selectedMessageId !== next.selectedMessageId) return false;
|
||||
if (prev.isGroupChat !== next.isGroupChat) return false;
|
||||
if (prev.groupMembers !== next.groupMembers) return false;
|
||||
if (prev.currentUser !== next.currentUser) return false;
|
||||
if (prev.otherUser !== next.otherUser) return false;
|
||||
if (prev.messageMap !== next.messageMap) return false;
|
||||
if (prev.onLongPress !== next.onLongPress) return false;
|
||||
if (prev.onImagePress !== next.onImagePress) return false;
|
||||
if (prev.onReplyPress !== next.onReplyPress) return false;
|
||||
if (prev.shouldShowTime !== next.shouldShowTime) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
export default MessageBubble;
|
||||
|
||||
@@ -165,7 +165,7 @@ const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNod
|
||||
* 渲染图片 Segment
|
||||
*/
|
||||
// 图片Segment组件 - 使用Hook获取实际尺寸
|
||||
const ImageSegment: React.FC<{
|
||||
const ImageSegmentInner: React.FC<{
|
||||
data: ImageSegmentData;
|
||||
isMe: boolean;
|
||||
onImagePress?: (url: string) => void;
|
||||
@@ -284,7 +284,6 @@ const ImageSegment: React.FC<{
|
||||
style={{
|
||||
width: IMAGE_FALLBACK_SIZE.width,
|
||||
height: IMAGE_FALLBACK_SIZE.height,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@@ -312,7 +311,6 @@ const ImageSegment: React.FC<{
|
||||
style={{
|
||||
width: IMAGE_FALLBACK_SIZE.width,
|
||||
height: IMAGE_FALLBACK_SIZE.height,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
}}
|
||||
/>
|
||||
@@ -334,7 +332,6 @@ const ImageSegment: React.FC<{
|
||||
style={{
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
recyclingKey={stableImageKey}
|
||||
contentFit="cover"
|
||||
@@ -345,7 +342,6 @@ const ImageSegment: React.FC<{
|
||||
setLoadError(false);
|
||||
}}
|
||||
onError={() => {
|
||||
// 缩略图失败时自动回退原图,降低跳转定位后的白块/丢图概率
|
||||
if (data.thumbnail_url && data.url && imageUrl === data.thumbnail_url) {
|
||||
setCurrentImageUri(data.url);
|
||||
return;
|
||||
@@ -357,6 +353,17 @@ const ImageSegment: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const ImageSegment = React.memo(ImageSegmentInner, (prev, next) => {
|
||||
if (prev.data.url !== next.data.url) return false;
|
||||
if (prev.data.thumbnail_url !== next.data.thumbnail_url) return false;
|
||||
if (prev.data.width !== next.data.width) return false;
|
||||
if (prev.data.height !== next.data.height) return false;
|
||||
if (prev.isMe !== next.isMe) return false;
|
||||
if (prev.onImagePress !== next.onImagePress) return false;
|
||||
if (prev.onImageLongPress !== next.onImageLongPress) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const renderImageSegment = (
|
||||
data: ImageSegmentData,
|
||||
props: Omit<SegmentRendererProps, 'segment'>
|
||||
@@ -705,7 +712,7 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
||||
/**
|
||||
* 渲染完整的消息链(多个 Segment 组合)
|
||||
*/
|
||||
export const MessageSegmentsRenderer: React.FC<{
|
||||
const MessageSegmentsRendererInner: React.FC<{
|
||||
segments: MessageSegment[];
|
||||
isMe: boolean;
|
||||
currentUserId?: string;
|
||||
@@ -734,11 +741,11 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
const fontSize = useChatSettingsStore((s) => s.fontSize);
|
||||
const segStyles = useMemo(() => createSegmentStyles(themeColors, fontSize), [themeColors, fontSize]);
|
||||
|
||||
const replySegment = segments.find(s => s.type === 'reply');
|
||||
const otherSegments = segments.filter(s => s.type !== 'reply');
|
||||
const chunks = partitionMessageSegments(otherSegments);
|
||||
const replySegment = useMemo(() => segments.find(s => s.type === 'reply'), [segments]);
|
||||
const otherSegments = useMemo(() => segments.filter(s => s.type !== 'reply'), [segments]);
|
||||
const chunks = useMemo(() => partitionMessageSegments(otherSegments), [otherSegments]);
|
||||
|
||||
const renderProps = {
|
||||
const renderProps = useMemo(() => ({
|
||||
isMe,
|
||||
currentUserId,
|
||||
memberMap,
|
||||
@@ -747,7 +754,7 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
onImagePress,
|
||||
onImageLongPress,
|
||||
onLinkPress,
|
||||
};
|
||||
}), [isMe, currentUserId, memberMap, onAtPress, onReplyPress, onImagePress, onImageLongPress, onLinkPress]);
|
||||
|
||||
return (
|
||||
<SegmentStylesContext.Provider value={segStyles}>
|
||||
@@ -837,13 +844,16 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
},
|
||||
imagesChunk: {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageStackItem: {
|
||||
width: '100%',
|
||||
marginBottom: 6,
|
||||
marginBottom: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageStackItemLast: {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
blockChunk: {
|
||||
width: '100%',
|
||||
@@ -864,14 +874,13 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
|
||||
// @提及 - 微信/QQ 风格:更精致的高亮,支持动态字号
|
||||
// @提及 - 与普通文本字号一致,仅颜色区分
|
||||
atText: {
|
||||
fontSize,
|
||||
lineHeight: Math.round(fontSize * 1.4),
|
||||
fontWeight: '500',
|
||||
lineHeight: Math.round(fontSize * 1.43),
|
||||
},
|
||||
atTextMe: {
|
||||
color: '#1A5F9E', // 深蓝色,在绿色背景上更清晰
|
||||
color: '#1A5F9E',
|
||||
},
|
||||
atTextOther: {
|
||||
color: colors.chat.link,
|
||||
@@ -879,25 +888,15 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
atHighlight: {
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.15)',
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
|
||||
// 图片 - QQ风格:保持原始宽高比
|
||||
imageContainer: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginTop: 4,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
imageMe: {
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
imageOther: {
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
// 移除固定的 image 样式,改为在组件中动态计算
|
||||
|
||||
@@ -1143,4 +1142,19 @@ function useSegmentStyles(): SegmentStyles {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export const MessageSegmentsRenderer = React.memo(MessageSegmentsRendererInner, (prev, next) => {
|
||||
if (prev.segments !== next.segments) return false;
|
||||
if (prev.isMe !== next.isMe) return false;
|
||||
if (prev.currentUserId !== next.currentUserId) return false;
|
||||
if (prev.memberMap !== next.memberMap) return false;
|
||||
if (prev.replyMessage !== next.replyMessage) return false;
|
||||
if (prev.onAtPress !== next.onAtPress) return false;
|
||||
if (prev.onReplyPress !== next.onReplyPress) return false;
|
||||
if (prev.onImagePress !== next.onImagePress) return false;
|
||||
if (prev.onImageLongPress !== next.onImageLongPress) return false;
|
||||
if (prev.onLinkPress !== next.onLinkPress) return false;
|
||||
if (prev.getSenderInfo !== next.getSenderInfo) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
export default MessageSegmentsRenderer;
|
||||
@@ -174,9 +174,9 @@ export const useChatScreen = () => {
|
||||
status: (m.status || 'normal') as MessageStatus,
|
||||
category: m.category,
|
||||
created_at: m.created_at,
|
||||
sender: (m as any).sender,
|
||||
is_system_notice: (m as any).is_system_notice,
|
||||
notice_content: (m as any).notice_content,
|
||||
sender: m.sender,
|
||||
is_system_notice: m.is_system_notice,
|
||||
notice_content: m.notice_content,
|
||||
}));
|
||||
}, [messageManagerMessages]);
|
||||
|
||||
@@ -258,7 +258,7 @@ export const useChatScreen = () => {
|
||||
setOtherUser(prev => ({ ...(prev || {}), ...other }));
|
||||
}
|
||||
// 获取对方最后阅读位置
|
||||
setOtherUserLastReadSeq((conversation as any).other_last_read_seq || 0);
|
||||
setOtherUserLastReadSeq(conversation.other_last_read_seq || 0);
|
||||
}
|
||||
}, [conversation, isGroupChat, currentUserId]);
|
||||
|
||||
@@ -273,8 +273,8 @@ export const useChatScreen = () => {
|
||||
const detail = await userManager.getUserById(String(otherUserId), true);
|
||||
if (!detail || cancelled) return;
|
||||
setOtherUser(prev => ({ ...(prev || {}), ...detail }));
|
||||
if (typeof (detail as any).is_following_me === 'boolean') {
|
||||
setIsFollowedByOther((detail as any).is_following_me);
|
||||
if (typeof detail.is_following_me === 'boolean') {
|
||||
setIsFollowedByOther(detail.is_following_me);
|
||||
} else {
|
||||
setIsFollowedByOther(null);
|
||||
}
|
||||
@@ -360,7 +360,7 @@ export const useChatScreen = () => {
|
||||
}, [loading, loadingMore, messages.length, scrollToLatest]);
|
||||
|
||||
// 新消息跟随策略(Telegram/QQ 风格):
|
||||
// 仅当“最新端消息 seq 增长”且用户在底部附近时才跟随;
|
||||
// 仅当"最新端消息 seq 增长"且用户在底部附近时才跟随;
|
||||
// 历史加载只会增加旧消息,不会提升 latest seq,因此不会触发回底。
|
||||
useEffect(() => {
|
||||
const currentCount = messages.length;
|
||||
@@ -371,14 +371,14 @@ export const useChatScreen = () => {
|
||||
|
||||
if (loading || loadingMore) return;
|
||||
if (latestSeq <= prevLatestSeq) return;
|
||||
if (suppressAutoFollowRef.current) return;
|
||||
if (suppressAutoFollowRef.current || isBrowsingHistoryRef.current || isHistoryLoadingLocked()) return;
|
||||
if (!isNearBottom()) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
scrollToLatest(false, false, 'new-message-follow');
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}, [messages, loading, loadingMore, isNearBottom, scrollToLatest]);
|
||||
}, [messages, loading, loadingMore, isNearBottom, scrollToLatest, isHistoryLoadingLocked]);
|
||||
|
||||
// 获取当前用户信息
|
||||
useEffect(() => {
|
||||
@@ -559,10 +559,10 @@ export const useChatScreen = () => {
|
||||
if (!conversationId) return;
|
||||
if (!conversation) return;
|
||||
|
||||
const unreadCount = Number((conversation as any).unread_count || 0);
|
||||
const unreadCount = Number(conversation.unread_count || 0);
|
||||
if (unreadCount <= 0) return;
|
||||
|
||||
const latestFromConversation = Number((conversation as any).last_seq || 0);
|
||||
const latestFromConversation = Number(conversation.last_seq || 0);
|
||||
const latestFromMessages = messages.length > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0;
|
||||
const targetSeq = Math.max(latestFromConversation, latestFromMessages);
|
||||
if (targetSeq <= 0) return;
|
||||
@@ -1305,8 +1305,8 @@ export const useChatScreen = () => {
|
||||
|
||||
// 关闭所有面板
|
||||
const handleDismiss = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
if (activePanel !== 'none') {
|
||||
Keyboard.dismiss();
|
||||
setActivePanel('none');
|
||||
}
|
||||
}, [activePanel]);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
|
||||
import { authService } from '../../../services/authService';
|
||||
import { useAuthStore } from '../../../stores';
|
||||
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
|
||||
import { blurActiveElement } from '../../../infrastructure/platform';
|
||||
import { User } from '../../../types';
|
||||
|
||||
type MutualFollowSelectorModalProps = {
|
||||
@@ -57,10 +58,7 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if (Platform.OS === 'web') {
|
||||
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
|
||||
active?.blur?.();
|
||||
}
|
||||
blurActiveElement();
|
||||
if (!currentUserId) return;
|
||||
|
||||
const nextSelectedIds = new Set(stableInitialSelectedIds);
|
||||
|
||||
Reference in New Issue
Block a user