chore: update styles, add splash config, and improve search functionality
- Replace deprecated StyleSheet.absoluteFillObject with StyleSheet.absoluteFill (React Native 0.76+) - Add splash screen configuration in app.json - Fix duplicate UIBackgroundModes and add audio mode for iOS - Disable Android ripple effect on tab bar buttons - Improve SmartImage loading state to prevent unnecessary re-renders - Refactor ImageGrid to use mainUri with separate previewUrl for better preview handling - Add market search support with trade items tab in SearchScreen - Pass homeTab prop to SearchScreen for context-aware search behavior - Simplify fetchUnreadCount return type to void in MessageSyncService - Fix StatusBar import from expo to react-native in ChatScreen
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); deps={**d.get\\('dependencies',{}\\), **d.get\\('devDependencies',{}\\)}; [print\\(f'{k}: {v}'\\) for k,v in deps.items\\(\\) if 'navigation' in k or 'tab' in k.lower\\(\\)]\")",
|
||||
"Bash(npx tsc *)",
|
||||
"Bash(echo \"EXIT: $?\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
app.json
7
app.json
@@ -7,14 +7,17 @@
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"scheme": "withyou",
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"infoPlist": {
|
||||
"NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话",
|
||||
"NSCameraUsageDescription": "允许威友访问您的相机以进行视频通话",
|
||||
"UIBackgroundModes": [
|
||||
"fetch",
|
||||
"remote-notification",
|
||||
"fetch",
|
||||
"remote-notification",
|
||||
"audio"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { Platform, useWindowDimensions } from 'react-native';
|
||||
import { Platform, Pressable, useWindowDimensions } from 'react-native';
|
||||
import { Tabs, usePathname } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import type { PressableProps } from 'react-native';
|
||||
|
||||
import { useAppColors, shadows } from '../../../src/theme';
|
||||
import { BREAKPOINTS } from '../../../src/hooks';
|
||||
@@ -12,6 +13,16 @@ const TAB_BAR_HEIGHT = 56;
|
||||
const TAB_BAR_FLOATING_MARGIN = 12;
|
||||
const TAB_BAR_MARGIN = 20;
|
||||
|
||||
const TabBarButton = ({ children, style, ...rest }: PressableProps) => (
|
||||
<Pressable
|
||||
{...rest}
|
||||
style={style}
|
||||
android_ripple={null}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default function TabsLayout() {
|
||||
const colors = useAppColors();
|
||||
const { width } = useWindowDimensions();
|
||||
@@ -72,6 +83,7 @@ export default function TabsLayout() {
|
||||
marginHorizontal: 2,
|
||||
paddingVertical: 1,
|
||||
},
|
||||
tabBarButton: (props) => <TabBarButton {...props} />,
|
||||
tabBarShowLabel: true,
|
||||
tabBarLabelStyle: { fontSize: 11, fontWeight: '600', marginTop: -1 },
|
||||
}}
|
||||
|
||||
@@ -67,7 +67,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
uploadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -159,7 +159,7 @@ function createVoteCardStyles(colors: AppColors) {
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: colors.background.paper + 'CC',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -267,12 +267,12 @@ const CallScreen: React.FC = () => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
zIndex: 9999,
|
||||
},
|
||||
background: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
},
|
||||
topBar: {
|
||||
@@ -334,7 +334,7 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
fullScreenVideo: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
},
|
||||
localVideoContainer: {
|
||||
|
||||
@@ -252,12 +252,12 @@ const CallScreen: React.FC = () => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
zIndex: 9999,
|
||||
},
|
||||
background: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
},
|
||||
topBar: {
|
||||
@@ -319,7 +319,7 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
fullScreenVideo: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: '#1A1A2E',
|
||||
},
|
||||
localVideoContainer: {
|
||||
|
||||
@@ -346,7 +346,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
overlayTouchable: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
|
||||
@@ -548,7 +548,7 @@ const styles = StyleSheet.create({
|
||||
height: SCREEN_HEIGHT * 0.8,
|
||||
},
|
||||
errorContainer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
|
||||
@@ -148,7 +148,7 @@ function createImageGridStyles(colors: AppColors) {
|
||||
backgroundColor: colors.background.disabled,
|
||||
},
|
||||
moreOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@@ -390,10 +390,12 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
return (
|
||||
<View style={[gridStyles.horizontalContainer, { gap }]}>
|
||||
{displayImages.map((image, index) => {
|
||||
const previewUrl = usePreview && displayMode
|
||||
const mainUri = image.uri || image.url || '';
|
||||
const previewForSmart = usePreview && displayMode
|
||||
? getPreviewImageUrl(image as any, displayMode)
|
||||
: (image.uri || image.url || '');
|
||||
|
||||
: '';
|
||||
const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={image.id || index}
|
||||
@@ -408,7 +410,9 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
]}
|
||||
>
|
||||
<SmartImage
|
||||
source={{ uri: previewUrl }}
|
||||
source={{ uri: mainUri }}
|
||||
previewUrl={useSmartPreview ? previewForSmart : undefined}
|
||||
usePreview={useSmartPreview}
|
||||
style={gridStyles.fullSize}
|
||||
resizeMode="cover"
|
||||
borderRadius={borderRadiusValue}
|
||||
@@ -427,10 +431,12 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
{displayImages.map((image, index) => {
|
||||
const isLastVisible = index === displayImages.length - 1;
|
||||
const showOverlay = isLastVisible && remainingCount > 0 && showMoreOverlay;
|
||||
|
||||
const previewUrl = usePreview && displayMode
|
||||
|
||||
const mainUri = image.uri || image.url || '';
|
||||
const previewForSmart = usePreview && displayMode
|
||||
? getPreviewImageUrl(image as any, displayMode)
|
||||
: (image.uri || image.url || '');
|
||||
: '';
|
||||
const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -445,7 +451,9 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
]}
|
||||
>
|
||||
<SmartImage
|
||||
source={{ uri: previewUrl }}
|
||||
source={{ uri: mainUri }}
|
||||
previewUrl={useSmartPreview ? previewForSmart : undefined}
|
||||
usePreview={useSmartPreview}
|
||||
style={gridStyles.fullSize}
|
||||
resizeMode="cover"
|
||||
borderRadius={borderRadiusValue}
|
||||
@@ -489,10 +497,12 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
? image.width / image.height
|
||||
: 1;
|
||||
const height = itemWidth / aspectRatio;
|
||||
|
||||
const previewUrl = usePreview && displayMode
|
||||
|
||||
const mainUri = image.uri || image.url || '';
|
||||
const previewForSmart = usePreview && displayMode
|
||||
? getPreviewImageUrl(image as any, displayMode)
|
||||
: (image.uri || image.url || '');
|
||||
: '';
|
||||
const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -508,7 +518,9 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
||||
]}
|
||||
>
|
||||
<SmartImage
|
||||
source={{ uri: previewUrl }}
|
||||
source={{ uri: mainUri }}
|
||||
previewUrl={useSmartPreview ? previewForSmart : undefined}
|
||||
usePreview={useSmartPreview}
|
||||
style={gridStyles.fullSize}
|
||||
resizeMode="cover"
|
||||
borderRadius={borderRadiusValue}
|
||||
|
||||
@@ -30,7 +30,7 @@ function createSmartImageStyles(colors: AppColors) {
|
||||
flex: 1,
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.disabled,
|
||||
@@ -186,7 +186,8 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
||||
if (isUpgradingFromPreview.current) {
|
||||
return;
|
||||
}
|
||||
setLoadState('loading');
|
||||
// 已成功加载的图片不再回到loading状态(列表回收/re-render时onLoadStart可能重新触发)
|
||||
setLoadState(prev => (prev === 'success' ? prev : 'loading'));
|
||||
}, []);
|
||||
|
||||
// 处理加载完成
|
||||
|
||||
@@ -1045,7 +1045,7 @@ function createCreatePostStyles(colors: AppColors) {
|
||||
height: '100%',
|
||||
},
|
||||
uploadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -902,6 +902,7 @@ export const HomeScreen: React.FC = () => {
|
||||
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
|
||||
<SearchScreen
|
||||
onBack={() => setShowSearch(false)}
|
||||
homeTab={homeTab}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ function createMarketStyles(colors: AppColors, gap: number, padding: number) {
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: padding,
|
||||
paddingTop: gap / 2,
|
||||
paddingTop: padding,
|
||||
paddingBottom: 80,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,34 +18,42 @@ import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||
import { Post, User } from '../../types';
|
||||
import type { TradeItemDTO } from '../../types/trade';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { postService, authService } from '../../services';
|
||||
import { tradeService } from '../../services/trade/tradeService';
|
||||
import { postSyncService } from '@/services/post';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import { TradeCard } from '../../components/business/TradeCard/TradeCard';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
|
||||
const TABS = ['帖子', '用户'];
|
||||
const SQUARE_TABS = ['帖子', '用户'];
|
||||
const MARKET_TABS = ['商品', '用户'];
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
// 与 TabsLayout 中的常量保持一致:tabBarHeight 56 + 上下浮动间距 12*2
|
||||
const FLOATING_TAB_BAR_HEIGHT = 56 + 12 * 2;
|
||||
|
||||
type SearchType = 'posts' | 'users';
|
||||
type SearchType = 'posts' | 'users' | 'trades';
|
||||
|
||||
interface SearchScreenProps {
|
||||
onBack?: () => void;
|
||||
homeTab?: 'square' | 'market';
|
||||
}
|
||||
|
||||
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 'square' }) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createSearchScreenStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const history = useUserStore((state) => state.searchHistory);
|
||||
const { addSearchHistory, clearSearchHistory } = useUserStore.getState();
|
||||
|
||||
const isMarket = homeTab === 'market';
|
||||
const TABS = isMarket ? MARKET_TABS : SQUARE_TABS;
|
||||
|
||||
// 使用响应式 hook
|
||||
const {
|
||||
@@ -115,6 +123,10 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
const [userResults, setUserResults] = useState<User[]>([]);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
|
||||
// 市集搜索结果
|
||||
const [tradeResults, setTradeResults] = useState<TradeItemDTO[]>([]);
|
||||
const [tradeLoading, setTradeLoading] = useState(false);
|
||||
|
||||
const refreshRef = useRef(refresh);
|
||||
refreshRef.current = refresh;
|
||||
const resetRef = useRef(reset);
|
||||
@@ -143,14 +155,21 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
|
||||
try {
|
||||
const searchType = getSearchType();
|
||||
|
||||
|
||||
if (searchType === 'users') {
|
||||
setUserLoading(true);
|
||||
const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20);
|
||||
setUserResults(usersResponse.list || []);
|
||||
} else if (searchType === 'trades') {
|
||||
setTradeLoading(true);
|
||||
const tradeResponse = await tradeService.getTradeItems({ keyword: trimmedKeyword, page_size: DEFAULT_PAGE_SIZE });
|
||||
setTradeResults(tradeResponse.list || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
} finally {
|
||||
setUserLoading(false);
|
||||
setTradeLoading(false);
|
||||
}
|
||||
|
||||
setHasSearched(true);
|
||||
@@ -219,6 +238,13 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
|
||||
// 获取当前搜索类型
|
||||
const getSearchType = (): SearchType => {
|
||||
if (isMarket) {
|
||||
switch (activeIndex) {
|
||||
case 0: return 'trades';
|
||||
case 1: return 'users';
|
||||
default: return 'trades';
|
||||
}
|
||||
}
|
||||
switch (activeIndex) {
|
||||
case 0: return 'posts';
|
||||
case 1: return 'users';
|
||||
@@ -426,18 +452,54 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染市集搜索结果
|
||||
const renderTradeResults = () => {
|
||||
if (tradeResults.length === 0 && !tradeLoading) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="未找到相关商品"
|
||||
description="试试其他关键词吧"
|
||||
icon="shopping-search-outline"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={tradeResults}
|
||||
renderItem={({ item }) => (
|
||||
<View style={{ paddingHorizontal: responsivePadding, marginBottom: responsiveGap }}>
|
||||
<TradeCard
|
||||
item={item}
|
||||
variant="list"
|
||||
onPress={(id) => router.push(`/trade/${id}` as any)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ paddingVertical: responsiveGap, paddingBottom: listBottomInset }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={tradeLoading ? <Loading size="sm" /> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染搜索结果
|
||||
const renderSearchResults = () => {
|
||||
const searchType = getSearchType();
|
||||
|
||||
|
||||
if (searchType === 'posts') {
|
||||
return renderPostResults();
|
||||
}
|
||||
|
||||
|
||||
if (searchType === 'users') {
|
||||
return renderUserResults();
|
||||
}
|
||||
|
||||
|
||||
if (searchType === 'trades') {
|
||||
return renderTradeResults();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -517,7 +579,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onSubmit={handleSearch}
|
||||
placeholder="搜索帖子、用户"
|
||||
placeholder={isMarket ? "搜索商品、用户" : "搜索帖子、用户"}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
@@ -548,7 +610,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
}
|
||||
}}
|
||||
variant="modern"
|
||||
icons={['file-document-outline', 'account-outline']}
|
||||
icons={isMarket ? ['shopping-outline', 'account-outline'] : ['file-document-outline', 'account-outline']}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
BackHandler,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||
import { useAppColors, useResolvedColorScheme } from '../../theme';
|
||||
@@ -478,7 +478,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||
>
|
||||
{!props.isEmbedded && <StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />}
|
||||
{!props.isEmbedded && <StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />}
|
||||
|
||||
{/* 顶部栏 */}
|
||||
<ChatHeader
|
||||
|
||||
@@ -374,7 +374,7 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
function createGroupInfoPanelStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
zIndex: 100,
|
||||
},
|
||||
|
||||
@@ -379,7 +379,7 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
function createGroupInfoPanelStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.35)',
|
||||
zIndex: 100,
|
||||
},
|
||||
|
||||
@@ -551,7 +551,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
|
||||
// 输入框@高亮
|
||||
inputHighlightOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingTop: 8,
|
||||
@@ -1072,7 +1072,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
|
||||
// 遮罩层
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -1142,7 +1142,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
manageModalBackdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
manageModalHandle: {
|
||||
@@ -1165,7 +1165,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
stickerCheckOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(127, 182, 230, 0.14)',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
|
||||
@@ -68,7 +68,7 @@ function createEditProfileStyles(colors: AppColors) {
|
||||
alignItems: 'center',
|
||||
},
|
||||
coverOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -198,7 +198,7 @@ const CourseDetailScreen: React.FC = () => {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.mask}>
|
||||
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => router.back()} />
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={() => router.back()} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -207,7 +207,7 @@ const CourseDetailScreen: React.FC = () => {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.mask}>
|
||||
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => router.back()} />
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={() => router.back()} />
|
||||
<View style={styles.card}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
|
||||
@@ -259,7 +259,7 @@ function createStyles(colors: AppColors) {
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
uploadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -208,7 +208,7 @@ class MessageManager {
|
||||
return this.syncService.loadMoreMessages(conversationId, beforeSeq, limit);
|
||||
}
|
||||
|
||||
async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
return this.syncService.fetchUnreadCount();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
/** 正在加载会话列表下一页 */
|
||||
private loadingMoreConversations = false;
|
||||
/** fetchUnreadCount 去重:复用 in-flight promise */
|
||||
private fetchUnreadCountPromise: Promise<{ totalUnread: number; systemUnread: number }> | null = null;
|
||||
private fetchUnreadCountPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
getCurrentUserId: () => string | null,
|
||||
@@ -352,20 +352,19 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
/**
|
||||
* 获取未读数(去重:复用 in-flight promise)
|
||||
*/
|
||||
async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
if (this.fetchUnreadCountPromise) {
|
||||
return this.fetchUnreadCountPromise;
|
||||
}
|
||||
this.fetchUnreadCountPromise = this.doFetchUnreadCount();
|
||||
try {
|
||||
const result = await this.fetchUnreadCountPromise;
|
||||
return result;
|
||||
await this.fetchUnreadCountPromise;
|
||||
} finally {
|
||||
this.fetchUnreadCountPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async doFetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
private async doFetchUnreadCount(): Promise<void> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
@@ -393,10 +392,8 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
|
||||
// 服务端汇总未读为 0:不主动清零本地未读,等 fetchConversations 提供权威数据
|
||||
// 避免后端缓存竞态导致的未读数抖动
|
||||
return { totalUnread, systemUnread };
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取未读数失败:', error);
|
||||
return { totalUnread: 0, systemUnread: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user