feat(message): add conversation notification mute feature
Add notification mute functionality for conversations with the following changes: - Add notification_muted field to ConversationResponse and related DTOs - Add notificationMutedMap to message store for tracking mute state - Add setConversationNotificationMuted API method - Integrate mute toggle in GroupInfoScreen and PrivateChatInfoScreen - Skip system notifications for muted conversations - Suppress vibration for muted conversations in WS handler - Clean up selected mentions when @mentions are removed from text Chore changes: - Update PostCard bookmark icon from 'bookmark' to 'star' - Refine card styles across AppsScreen, MaterialsScreen, and SubjectMaterialsScreen - Improve week selector scrolling to center selected week in ScheduleScreen
This commit is contained in:
@@ -721,7 +721,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.actionBtn} onPress={handleBookmark}>
|
<TouchableOpacity style={styles.actionBtn} onPress={handleBookmark}>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={post.is_favorited ? 'bookmark' : 'bookmark-outline'}
|
name={post.is_favorited ? 'star' : 'star-outline'}
|
||||||
size={18}
|
size={18}
|
||||||
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
|
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ export const AppsScreen: React.FC = () => {
|
|||||||
() => ({
|
() => ({
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: 16,
|
borderRadius: borderRadius.lg,
|
||||||
overflow: 'hidden' as const,
|
overflow: 'hidden' as const,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.06,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 8,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
maxWidth: APP_CARD_MAX_WIDTH,
|
maxWidth: APP_CARD_MAX_WIDTH,
|
||||||
alignSelf: 'center' as const,
|
alignSelf: 'center' as const,
|
||||||
width: '100%' as const,
|
width: '100%' as const,
|
||||||
@@ -102,7 +102,7 @@ export const AppsScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<View style={styles.appCardInner}>
|
<View style={styles.appCardInner}>
|
||||||
<View style={styles.appIconCircle}>
|
<View style={styles.appIconCircle}>
|
||||||
<MaterialCommunityIcons name={item.icon} size={26} color={colors.primary.contrast} />
|
<MaterialCommunityIcons name={item.icon} size={24} color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.appCardText}>
|
<View style={styles.appCardText}>
|
||||||
<Text variant="body" color={colors.text.primary} style={styles.appTitle}>
|
<Text variant="body" color={colors.text.primary} style={styles.appTitle}>
|
||||||
@@ -112,7 +112,7 @@ export const AppsScreen: React.FC = () => {
|
|||||||
{item.subtitle}
|
{item.subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<MaterialCommunityIcons name="chevron-right" size={22} color={colors.primary.main} />
|
<MaterialCommunityIcons name="chevron-right" size={22} color={colors.text.hint} />
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -223,12 +223,12 @@ function createAppsStyles(colors: AppColors) {
|
|||||||
paddingVertical: spacing.lg,
|
paddingVertical: spacing.lg,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
/** 与首页发帖 FAB 同主色实心圆 */
|
/** 扁平化主色图标背景 */
|
||||||
appIconCircle: {
|
appIconCircle: {
|
||||||
width: 52,
|
width: 48,
|
||||||
height: 52,
|
height: 48,
|
||||||
borderRadius: borderRadius.full,
|
borderRadius: borderRadius.lg,
|
||||||
backgroundColor: colors.primary.main,
|
backgroundColor: `${colors.primary.main}14`,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ export const MaterialDetailScreen: React.FC = () => {
|
|||||||
const cardStyle = useMemo(
|
const cardStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: 16,
|
borderRadius: borderRadius.lg,
|
||||||
overflow: 'hidden' as const,
|
overflow: 'hidden' as const,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.06,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 8,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
}),
|
}),
|
||||||
[colors]
|
[colors]
|
||||||
);
|
);
|
||||||
@@ -390,7 +390,7 @@ function createMaterialDetailStyles(colors: AppColors) {
|
|||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
},
|
},
|
||||||
fileTypeBanner: {
|
fileTypeBanner: {
|
||||||
height: 100,
|
height: 88,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -461,10 +461,9 @@ function createMaterialDetailStyles(colors: AppColors) {
|
|||||||
tag: {
|
tag: {
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.xs,
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: `${colors.primary.main}10`,
|
||||||
borderRadius: borderRadius.sm,
|
borderRadius: borderRadius.md,
|
||||||
borderWidth: 1,
|
borderWidth: 0,
|
||||||
borderColor: colors.primary.main,
|
|
||||||
},
|
},
|
||||||
actionButtons: {
|
actionButtons: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -477,8 +476,10 @@ function createMaterialDetailStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.md,
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: colors.divider,
|
||||||
},
|
},
|
||||||
downloadButton: {
|
downloadButton: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -486,7 +487,7 @@ function createMaterialDetailStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.md,
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
},
|
},
|
||||||
actionButtonText: {
|
actionButtonText: {
|
||||||
|
|||||||
@@ -84,13 +84,13 @@ export const MaterialsScreen: React.FC = () => {
|
|||||||
const cardStyle = useMemo(
|
const cardStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: 16,
|
borderRadius: borderRadius.lg,
|
||||||
overflow: 'hidden' as const,
|
overflow: 'hidden' as const,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.06,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 8,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
maxWidth: SUBJECT_CARD_MAX_WIDTH,
|
maxWidth: SUBJECT_CARD_MAX_WIDTH,
|
||||||
alignSelf: 'center' as const,
|
alignSelf: 'center' as const,
|
||||||
width: '100%' as const,
|
width: '100%' as const,
|
||||||
@@ -117,9 +117,6 @@ export const MaterialsScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.subjectMeta}>
|
<View style={styles.subjectMeta}>
|
||||||
<Text variant="caption" color={colors.text.hint}>
|
|
||||||
{subject.material_count}
|
|
||||||
</Text>
|
|
||||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -271,9 +268,9 @@ function createMaterialsStyles(colors: AppColors) {
|
|||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
},
|
},
|
||||||
subjectIconCircle: {
|
subjectIconCircle: {
|
||||||
width: 56,
|
width: 48,
|
||||||
height: 56,
|
height: 48,
|
||||||
borderRadius: borderRadius.full,
|
borderRadius: borderRadius.lg,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,11 +117,11 @@ export const SubjectMaterialsScreen: React.FC = () => {
|
|||||||
const cardStyle = useMemo(
|
const cardStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: 12,
|
borderRadius: borderRadius.lg,
|
||||||
overflow: 'hidden' as const,
|
overflow: 'hidden' as const,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.05,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
maxWidth: MATERIAL_CARD_MAX_WIDTH,
|
maxWidth: MATERIAL_CARD_MAX_WIDTH,
|
||||||
@@ -178,14 +178,14 @@ export const SubjectMaterialsScreen: React.FC = () => {
|
|||||||
style={[
|
style={[
|
||||||
styles.filterTab,
|
styles.filterTab,
|
||||||
selectedFileType === type && styles.filterTabActive,
|
selectedFileType === type && styles.filterTabActive,
|
||||||
selectedFileType === type && { backgroundColor: colors.primary.main },
|
selectedFileType === type && { backgroundColor: `${colors.primary.main}14` },
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedFileType(type)}
|
onPress={() => setSelectedFileType(type)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color={selectedFileType === type ? colors.primary.contrast : colors.text.secondary}
|
color={selectedFileType === type ? colors.primary.main : colors.text.secondary}
|
||||||
style={styles.filterTabText}
|
style={styles.filterTabText}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -337,11 +337,13 @@ function createSubjectMaterialsStyles(colors: AppColors) {
|
|||||||
filterTab: {
|
filterTab: {
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.sm,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
borderRadius: borderRadius.full,
|
borderRadius: borderRadius.md,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: colors.divider,
|
||||||
},
|
},
|
||||||
filterTabActive: {
|
filterTabActive: {
|
||||||
// backgroundColor set inline
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
filterTabText: {
|
filterTabText: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@@ -354,8 +356,8 @@ function createSubjectMaterialsStyles(colors: AppColors) {
|
|||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
},
|
},
|
||||||
fileTypeIcon: {
|
fileTypeIcon: {
|
||||||
width: 44,
|
width: 40,
|
||||||
height: 44,
|
height: 40,
|
||||||
borderRadius: borderRadius.md,
|
borderRadius: borderRadius.md,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { blurActiveElement } from '../../infrastructure/platform';
|
|||||||
import { firstRouteParam } from '../../navigation/paramUtils';
|
import { firstRouteParam } from '../../navigation/paramUtils';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { messageManager } from '../../stores/message';
|
import { messageManager } from '../../stores/message';
|
||||||
|
import { useMessageStore } from '../../stores/message/store';
|
||||||
import { groupManager } from '../../stores/group';
|
import { groupManager } from '../../stores/group';
|
||||||
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
|
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
|
||||||
import { AppBackButton } from '../../components/common';
|
import { AppBackButton } from '../../components/common';
|
||||||
@@ -95,6 +96,9 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
const [pinLoading, setPinLoading] = useState(false);
|
const [pinLoading, setPinLoading] = useState(false);
|
||||||
|
const isNotificationMuted = useMessageStore(state =>
|
||||||
|
conversationId ? (state.notificationMutedMap.get(conversationId) || false) : false
|
||||||
|
);
|
||||||
|
|
||||||
// 编辑模态框状态
|
// 编辑模态框状态
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
@@ -212,6 +216,16 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设置会话免打扰
|
||||||
|
const handleToggleNotificationMuted = async () => {
|
||||||
|
if (!conversationId) return;
|
||||||
|
try {
|
||||||
|
await messageManager.toggleNotificationMuted(conversationId, !isNotificationMuted);
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert('错误', error?.message || '设置免打扰失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 打开编辑模态框
|
// 打开编辑模态框
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
if (group) {
|
if (group) {
|
||||||
@@ -799,6 +813,24 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
|
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<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>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { messageService } from '@/services/message';
|
|||||||
import { ApiError } from '@/services/core';
|
import { ApiError } from '@/services/core';
|
||||||
import { messageRepository, conversationRepository } from '@/database';
|
import { messageRepository, conversationRepository } from '@/database';
|
||||||
import { messageManager } from '../../stores/message';
|
import { messageManager } from '../../stores/message';
|
||||||
|
import { useMessageStore } from '../../stores/message/store';
|
||||||
import { userManager } from '../../stores/user';
|
import { userManager } from '../../stores/user';
|
||||||
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
|
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
|
||||||
import { User } from '../../types';
|
import { User } from '../../types';
|
||||||
@@ -51,7 +52,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
const [isBlocked, setIsBlocked] = useState(false);
|
const [isBlocked, setIsBlocked] = useState(false);
|
||||||
|
|
||||||
// 聊天设置状态
|
// 聊天设置状态
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const isMuted = useMessageStore(state => state.notificationMutedMap.get(conversationId) || false);
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
|
|
||||||
// 加载用户信息
|
// 加载用户信息
|
||||||
@@ -96,15 +97,12 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
loadChatSettings();
|
loadChatSettings();
|
||||||
}, [loadUserInfo, loadChatSettings]);
|
}, [loadUserInfo, loadChatSettings]);
|
||||||
|
|
||||||
// 切换免打扰(仅本地状态,实际需要API支持)
|
// 切换免打扰
|
||||||
const toggleMute = async () => {
|
const toggleMute = async () => {
|
||||||
try {
|
try {
|
||||||
const newValue = !isMuted;
|
const newValue = !isMuted;
|
||||||
setIsMuted(newValue);
|
await messageManager.toggleNotificationMuted(conversationId, newValue);
|
||||||
// TODO: 调用API更新免打扰状态
|
|
||||||
// await conversationService.updateConversationMute(conversationId, newValue);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsMuted(!isMuted);
|
|
||||||
Alert.alert('错误', '设置免打扰失败');
|
Alert.alert('错误', '设置免打扰失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -705,8 +705,25 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
|||||||
} else {
|
} else {
|
||||||
setActivePanel('none');
|
setActivePanel('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedMentions.length > 0) {
|
||||||
|
const stillPresent = selectedMentions.filter(userId => {
|
||||||
|
const member = groupMembers.find(m => m.user_id === userId);
|
||||||
|
const nickname = member?.nickname || member?.user?.nickname;
|
||||||
|
if (!nickname) return false;
|
||||||
|
const escapedName = nickname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
return new RegExp(`@${escapedName}(\\s|$)`, 'g').test(text);
|
||||||
|
});
|
||||||
|
if (stillPresent.length !== selectedMentions.length) {
|
||||||
|
setSelectedMentions(stillPresent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentionAll && !text.includes('@所有人')) {
|
||||||
|
setMentionAll(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isGroupChat]);
|
}, [isGroupChat, selectedMentions, mentionAll, groupMembers]);
|
||||||
|
|
||||||
// 【改造】获取发送者信息(群聊)- 优先从消息的 sender 字段获取
|
// 【改造】获取发送者信息(群聊)- 优先从消息的 sender 字段获取
|
||||||
const getSenderInfo = useCallback((senderId: string): SenderInfo => {
|
const getSenderInfo = useCallback((senderId: string): SenderInfo => {
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ import { scheduleService } from '@/services/platform';
|
|||||||
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
|
||||||
|
// 周选择器固定宽度常量(用于精确计算滚动位置,确保选中周始终居中)
|
||||||
|
const WEEK_ITEM_WIDTH = 64; // 固定宽度,确保"第20周"能放下
|
||||||
|
const WEEK_ITEM_MARGIN_HORIZONTAL = 3; // marginHorizontal 单侧值
|
||||||
|
const WEEK_LIST_PADDING = 12; // contentContainer paddingHorizontal = spacing.sm
|
||||||
|
const SCROLL_VIEW_VISIBLE_WIDTH = SCREEN_WIDTH - 40 - 40; // 扣除返回按钮(40)和设置按钮(40)
|
||||||
|
|
||||||
// 时间段列宽度(按需求缩短)
|
// 时间段列宽度(按需求缩短)
|
||||||
const TIME_COLUMN_WIDTH = 38;
|
const TIME_COLUMN_WIDTH = 38;
|
||||||
const VISIBLE_DAY_VALUES = [0, 1, 2, 3, 4, 5, 6]; // 周一到周日(0=周一)
|
const VISIBLE_DAY_VALUES = [0, 1, 2, 3, 4, 5, 6]; // 周一到周日(0=周一)
|
||||||
@@ -183,8 +189,8 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
// 电脑端:第六节课刚好填到底部(可用高度 = 屏幕高度 - 周选择器 - 星期标题)
|
// 电脑端:第六节课刚好填到底部(可用高度 = 屏幕高度 - 周选择器 - 星期标题)
|
||||||
const sectionHeight = useMemo((): number => {
|
const sectionHeight = useMemo((): number => {
|
||||||
const totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT;
|
const totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT;
|
||||||
// 悬浮TabBar高度 + 安全区域底部
|
// 悬浮TabBar高度 + 安全区域底部 + 额外底部padding避免遮挡
|
||||||
const bottomOffset = isMobile ? FLOATING_TAB_BAR_HEIGHT + insets.bottom : 0;
|
const bottomOffset = isMobile ? FLOATING_TAB_BAR_HEIGHT + insets.bottom + 8 : 0;
|
||||||
// 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移
|
// 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移
|
||||||
const availableHeight = screenHeight - totalHeaderHeight - bottomOffset;
|
const availableHeight = screenHeight - totalHeaderHeight - bottomOffset;
|
||||||
// 6节课,每节课高度 = 可用高度 / 6
|
// 6节课,每节课高度 = 可用高度 / 6
|
||||||
@@ -256,11 +262,24 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
return Array.from({ length: TOTAL_WEEKS }, (_, i) => i + 1);
|
return Array.from({ length: TOTAL_WEEKS }, (_, i) => i + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 将当前选中周滚动到 ScrollView 可视区域正中央
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const WEEK_ITEM_ESTIMATED_WIDTH = 92;
|
// 第 N 个 item 的左边缘在内容坐标系中的位置
|
||||||
const centerOffset = SCREEN_WIDTH / 2 - WEEK_ITEM_ESTIMATED_WIDTH / 2 - 40;
|
// = padding + (N-1) * (width + margin*2) + margin
|
||||||
const targetX = Math.max(0, (currentWeek - 1) * WEEK_ITEM_ESTIMATED_WIDTH - centerOffset);
|
const itemLeft = WEEK_LIST_PADDING
|
||||||
weekScrollRef.current?.scrollTo({ x: targetX, animated: true });
|
+ (currentWeek - 1) * (WEEK_ITEM_WIDTH + WEEK_ITEM_MARGIN_HORIZONTAL * 2)
|
||||||
|
+ WEEK_ITEM_MARGIN_HORIZONTAL;
|
||||||
|
|
||||||
|
// 目标滚动偏移 = item中心 - 可视区域中心
|
||||||
|
const targetX = itemLeft + WEEK_ITEM_WIDTH / 2 - SCROLL_VIEW_VISIBLE_WIDTH / 2;
|
||||||
|
|
||||||
|
// 限制在有效范围内(不小于0,不超过最大可滚动距离)
|
||||||
|
const totalContentWidth = WEEK_LIST_PADDING * 2
|
||||||
|
+ TOTAL_WEEKS * (WEEK_ITEM_WIDTH + WEEK_ITEM_MARGIN_HORIZONTAL * 2);
|
||||||
|
const maxScrollX = Math.max(0, totalContentWidth - SCROLL_VIEW_VISIBLE_WIDTH);
|
||||||
|
const clampedX = Math.max(0, Math.min(targetX, maxScrollX));
|
||||||
|
|
||||||
|
weekScrollRef.current?.scrollTo({ x: clampedX, animated: true });
|
||||||
}, [currentWeek]);
|
}, [currentWeek]);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -427,14 +446,14 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="返回"
|
accessibilityLabel="返回"
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="chevron-left" size={28} color="#FFFFFF" />
|
<MaterialCommunityIcons name="chevron-left" size={28} color={colors.text.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null}
|
) : null}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.settingsButton}
|
style={styles.settingsButton}
|
||||||
onPress={() => setIsSettingsModalVisible(true)}
|
onPress={() => setIsSettingsModalVisible(true)}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="cog" size={24} color="#FFFFFF" />
|
<MaterialCommunityIcons name="cog" size={22} color={colors.text.secondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
ref={weekScrollRef}
|
ref={weekScrollRef}
|
||||||
@@ -442,25 +461,31 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.weekList}
|
contentContainerStyle={styles.weekList}
|
||||||
>
|
>
|
||||||
{weeks.map(week => (
|
{weeks.map(week => {
|
||||||
<TouchableOpacity
|
const isCurrentRealWeek = week === INITIAL_WEEK;
|
||||||
key={week}
|
const isSelected = currentWeek === week;
|
||||||
style={[
|
return (
|
||||||
styles.weekItem,
|
<TouchableOpacity
|
||||||
currentWeek === week && styles.weekItemActive,
|
key={week}
|
||||||
]}
|
|
||||||
onPress={() => setCurrentWeek(week)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
style={[
|
||||||
styles.weekItemText,
|
styles.weekItem,
|
||||||
currentWeek === week && styles.weekItemTextActive,
|
isSelected && styles.weekItemActive,
|
||||||
|
isCurrentRealWeek && !isSelected && styles.weekItemCurrentWeek,
|
||||||
]}
|
]}
|
||||||
|
onPress={() => setCurrentWeek(week)}
|
||||||
>
|
>
|
||||||
第{week}周
|
<Text
|
||||||
</Text>
|
style={[
|
||||||
</TouchableOpacity>
|
styles.weekItemText,
|
||||||
))}
|
isSelected && styles.weekItemTextActive,
|
||||||
|
isCurrentRealWeek && !isSelected && styles.weekItemTextCurrentWeek,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
第{week}周
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -805,7 +830,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
<StatusBar barStyle="light-content" backgroundColor={colors.primary.main} />
|
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
|
||||||
|
|
||||||
{/* 周选择器 */}
|
{/* 周选择器 */}
|
||||||
{renderWeekSelector()}
|
{renderWeekSelector()}
|
||||||
@@ -1147,20 +1172,23 @@ function createScheduleStyles(colors: AppColors) {
|
|||||||
// ── 周选择器 ──────────────────────────────────────────
|
// ── 周选择器 ──────────────────────────────────────────
|
||||||
weekSelector: {
|
weekSelector: {
|
||||||
height: WEEK_SELECTOR_HEIGHT,
|
height: WEEK_SELECTOR_HEIGHT,
|
||||||
backgroundColor: colors.primary.main,
|
backgroundColor: colors.background.paper,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: colors.divider,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
},
|
},
|
||||||
weekBarIconButton: {
|
weekBarIconButton: {
|
||||||
width: 40,
|
width: 36,
|
||||||
height: 40,
|
height: 36,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: -8,
|
marginLeft: -4,
|
||||||
},
|
},
|
||||||
settingsButton: {
|
settingsButton: {
|
||||||
width: 40,
|
width: 36,
|
||||||
height: 40,
|
height: 36,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: spacing.xs,
|
marginLeft: spacing.xs,
|
||||||
@@ -1170,31 +1198,41 @@ function createScheduleStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
weekItem: {
|
weekItem: {
|
||||||
paddingHorizontal: spacing.md,
|
width: WEEK_ITEM_WIDTH,
|
||||||
paddingVertical: 5,
|
paddingVertical: 5,
|
||||||
marginHorizontal: 3,
|
marginHorizontal: WEEK_ITEM_MARGIN_HORIZONTAL,
|
||||||
borderRadius: borderRadius.full,
|
borderRadius: borderRadius.md,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
},
|
},
|
||||||
weekItemActive: {
|
weekItemActive: {
|
||||||
backgroundColor: 'rgba(255,255,255,0.28)',
|
backgroundColor: colors.primary.main,
|
||||||
|
},
|
||||||
|
weekItemCurrentWeek: {
|
||||||
|
backgroundColor: `${colors.primary.main}18`,
|
||||||
},
|
},
|
||||||
weekItemText: {
|
weekItemText: {
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: 'rgba(255,255,255,0.75)',
|
color: colors.text.secondary,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
weekItemTextActive: {
|
weekItemTextActive: {
|
||||||
color: colors.text.inverse,
|
color: colors.text.inverse,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
weekItemTextCurrentWeek: {
|
||||||
|
color: colors.primary.main,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
|
||||||
// ── 星期标题行 ─────────────────────────────────────────
|
// ── 星期标题行 ─────────────────────────────────────────
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: HEADER_HEIGHT,
|
height: HEADER_HEIGHT,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomColor: colors.divider,
|
||||||
},
|
},
|
||||||
timeHeader: {
|
timeHeader: {
|
||||||
@@ -1268,7 +1306,7 @@ function createScheduleStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#EBEBEB',
|
borderBottomColor: colors.divider,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
timeSectionBadge: {
|
timeSectionBadge: {
|
||||||
@@ -1303,22 +1341,22 @@ function createScheduleStyles(colors: AppColors) {
|
|||||||
// ── 单日列 ────────────────────────────────────────────
|
// ── 单日列 ────────────────────────────────────────────
|
||||||
dayColumn: {
|
dayColumn: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.default, // 使用主题默认背景色
|
backgroundColor: colors.background.default,
|
||||||
borderRightWidth: 1,
|
borderRightWidth: StyleSheet.hairlineWidth,
|
||||||
borderRightColor: '#EBEBEB',
|
borderRightColor: colors.divider,
|
||||||
},
|
},
|
||||||
gridCell: {
|
gridCell: {
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#EBEBEB',
|
borderBottomColor: colors.divider,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 课程卡片 ──────────────────────────────────────────
|
// ── 课程卡片 ──────────────────────────────────────────
|
||||||
courseCard: {
|
courseCard: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
borderRadius: borderRadius.xl,
|
borderRadius: borderRadius.lg,
|
||||||
paddingVertical: spacing.xs + 2,
|
paddingVertical: spacing.xs + 2,
|
||||||
paddingHorizontal: spacing.xs + 1,
|
paddingHorizontal: spacing.xs + 1,
|
||||||
...shadows.md,
|
...shadows.sm,
|
||||||
},
|
},
|
||||||
courseName: {
|
courseName: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
|||||||
@@ -274,6 +274,12 @@ class MessageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setConversationNotificationMuted(conversationId: string, notificationMuted: boolean): Promise<void> {
|
||||||
|
await api.put(`/conversations/${encodeURIComponent(conversationId)}/notification_muted`, {
|
||||||
|
notification_muted: notificationMuted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 消息相关 ====================
|
// ==================== 消息相关 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type { WSChatMessage, WSNotificationMessage, WSAnnouncementMessage } from
|
|||||||
import { extractTextFromSegments } from '@/types/dto';
|
import { extractTextFromSegments } from '@/types/dto';
|
||||||
import { getNotificationPreferencesSync } from './notificationPreferences';
|
import { getNotificationPreferencesSync } from './notificationPreferences';
|
||||||
import { vibrateOnMessage } from '../platform/messageVibrationService';
|
import { vibrateOnMessage } from '../platform/messageVibrationService';
|
||||||
|
import { useMessageStore } from '@/stores/message/store';
|
||||||
|
import { normalizeConversationId } from '@/stores/message/store';
|
||||||
|
|
||||||
// 通知渠道配置
|
// 通知渠道配置
|
||||||
const CHANNEL_ID = 'default';
|
const CHANNEL_ID = 'default';
|
||||||
@@ -207,9 +209,12 @@ class SystemNotificationService {
|
|||||||
// 判断是否是聊天消息(通过 segments 字段)
|
// 判断是否是聊天消息(通过 segments 字段)
|
||||||
if ('segments' in message) {
|
if ('segments' in message) {
|
||||||
const chatMsg = message as WSChatMessage;
|
const chatMsg = message as WSChatMessage;
|
||||||
const body = extractTextFromSegments(chatMsg.segments);
|
const conversationId = normalizeConversationId(chatMsg.conversation_id);
|
||||||
|
const isNotificationMuted = useMessageStore.getState().isNotificationMuted(conversationId);
|
||||||
|
if (isNotificationMuted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
void chatMsg;
|
void chatMsg;
|
||||||
void body;
|
|
||||||
await this.showChatNotification(chatMsg);
|
await this.showChatNotification(chatMsg);
|
||||||
} else {
|
} else {
|
||||||
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
|
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
|
||||||
|
|||||||
@@ -310,6 +310,21 @@ class MessageManager {
|
|||||||
return useMessageStore.getState().setMutedStatus(groupId, isMuted);
|
return useMessageStore.getState().setMutedStatus(groupId, isMuted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isNotificationMuted(conversationId: string): boolean {
|
||||||
|
return useMessageStore.getState().isNotificationMuted(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotificationMuted(conversationId: string, notificationMuted: boolean): void {
|
||||||
|
return useMessageStore.getState().setNotificationMuted(conversationId, notificationMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleNotificationMuted(conversationId: string, notificationMuted: boolean): Promise<void> {
|
||||||
|
const { messageService } = await import('@/services/message');
|
||||||
|
await messageService.setConversationNotificationMuted(conversationId, notificationMuted);
|
||||||
|
this.setNotificationMuted(conversationId, notificationMuted);
|
||||||
|
this.updateConversation(conversationId, { notification_muted: notificationMuted });
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 活动会话管理 ====================
|
// ==================== 活动会话管理 ====================
|
||||||
|
|
||||||
setActiveConversation(conversationId: string | null): void {
|
setActiveConversation(conversationId: string | null): void {
|
||||||
|
|||||||
@@ -317,7 +317,8 @@ export class WSMessageHandler implements IWSMessageHandler {
|
|||||||
!_isAck;
|
!_isAck;
|
||||||
|
|
||||||
if (shouldIncrementUnread) {
|
if (shouldIncrementUnread) {
|
||||||
if (!suppressVibration) {
|
const isNotificationMuted = store.isNotificationMuted(normalizedConversationId);
|
||||||
|
if (!suppressVibration && !isNotificationMuted) {
|
||||||
const vibrationType = message.type === 'group_message' ? 'group_message' : 'message';
|
const vibrationType = message.type === 'group_message' ? 'group_message' : 'message';
|
||||||
eventBus.emit({ type: 'VIBRATE', payload: { type: vibrationType } });
|
eventBus.emit({ type: 'VIBRATE', payload: { type: vibrationType } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export interface MessageState {
|
|||||||
|
|
||||||
// 当前用户的禁言状态 - 按群组ID存储
|
// 当前用户的禁言状态 - 按群组ID存储
|
||||||
mutedStatusMap: Map<string, boolean>;
|
mutedStatusMap: Map<string, boolean>;
|
||||||
|
|
||||||
|
// 会话免打扰状态 - 按会话ID存储
|
||||||
|
notificationMutedMap: Map<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Actions 接口 ====================
|
// ==================== Actions 接口 ====================
|
||||||
@@ -72,6 +75,7 @@ export interface MessageActions {
|
|||||||
getTypingUsers: (groupId: string) => string[];
|
getTypingUsers: (groupId: string) => string[];
|
||||||
isMuted: (groupId: string) => boolean;
|
isMuted: (groupId: string) => boolean;
|
||||||
getActiveConversation: () => string | null;
|
getActiveConversation: () => string | null;
|
||||||
|
isNotificationMuted: (conversationId: string) => boolean;
|
||||||
|
|
||||||
// 设置状态
|
// 设置状态
|
||||||
setConversations: (conversations: Map<string, ConversationResponse>) => void;
|
setConversations: (conversations: Map<string, ConversationResponse>) => void;
|
||||||
@@ -88,6 +92,7 @@ export interface MessageActions {
|
|||||||
setLoadingMessages: (conversationId: string, loading: boolean) => void;
|
setLoadingMessages: (conversationId: string, loading: boolean) => void;
|
||||||
setTypingUsers: (groupId: string, users: string[]) => void;
|
setTypingUsers: (groupId: string, users: string[]) => void;
|
||||||
setMutedStatus: (groupId: string, isMuted: boolean) => void;
|
setMutedStatus: (groupId: string, isMuted: boolean) => void;
|
||||||
|
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => void;
|
||||||
setInitialized: (initialized: boolean) => void;
|
setInitialized: (initialized: boolean) => void;
|
||||||
|
|
||||||
// 重置
|
// 重置
|
||||||
@@ -110,6 +115,7 @@ const initialState: MessageState = {
|
|||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
typingUsersMap: new Map(),
|
typingUsersMap: new Map(),
|
||||||
mutedStatusMap: new Map(),
|
mutedStatusMap: new Map(),
|
||||||
|
notificationMutedMap: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
@@ -177,6 +183,7 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
|
|||||||
isInitialized: state.isInitialized,
|
isInitialized: state.isInitialized,
|
||||||
typingUsersMap: state.typingUsersMap,
|
typingUsersMap: state.typingUsersMap,
|
||||||
mutedStatusMap: state.mutedStatusMap,
|
mutedStatusMap: state.mutedStatusMap,
|
||||||
|
notificationMutedMap: state.notificationMutedMap,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -227,11 +234,25 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
|
|||||||
return get().currentConversationId;
|
return get().currentConversationId;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isNotificationMuted: (conversationId: string) => {
|
||||||
|
return get().notificationMutedMap.get(conversationId) || false;
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== 设置状态 ====================
|
// ==================== 设置状态 ====================
|
||||||
|
|
||||||
setConversations: (conversations: Map<string, ConversationResponse>) => {
|
setConversations: (conversations: Map<string, ConversationResponse>) => {
|
||||||
const conversationList = sortConversationList(conversations);
|
const conversationList = sortConversationList(conversations);
|
||||||
set({ conversations, conversationList });
|
const notificationMutedMap = new Map<string, boolean>();
|
||||||
|
conversations.forEach((conv, id) => {
|
||||||
|
if (conv.notification_muted) {
|
||||||
|
notificationMutedMap.set(id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
set(state => ({
|
||||||
|
conversations,
|
||||||
|
conversationList,
|
||||||
|
notificationMutedMap: new Map([...state.notificationMutedMap, ...notificationMutedMap]),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
updateConversation: (conversation: ConversationResponse) => {
|
updateConversation: (conversation: ConversationResponse) => {
|
||||||
@@ -240,7 +261,13 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
|
|||||||
const newConversations = new Map(state.conversations);
|
const newConversations = new Map(state.conversations);
|
||||||
newConversations.set(id, { ...conversation, id });
|
newConversations.set(id, { ...conversation, id });
|
||||||
const conversationList = sortConversationList(newConversations);
|
const conversationList = sortConversationList(newConversations);
|
||||||
return { conversations: newConversations, conversationList };
|
const newNotificationMutedMap = new Map(state.notificationMutedMap);
|
||||||
|
if (conversation.notification_muted) {
|
||||||
|
newNotificationMutedMap.set(id, true);
|
||||||
|
} else {
|
||||||
|
newNotificationMutedMap.delete(id);
|
||||||
|
}
|
||||||
|
return { conversations: newConversations, conversationList, notificationMutedMap: newNotificationMutedMap };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -374,6 +401,14 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => {
|
||||||
|
set(state => {
|
||||||
|
const newNotificationMutedMap = new Map(state.notificationMutedMap);
|
||||||
|
newNotificationMutedMap.set(conversationId, notificationMuted);
|
||||||
|
return { notificationMutedMap: newNotificationMutedMap };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setInitialized: (initialized: boolean) => {
|
setInitialized: (initialized: boolean) => {
|
||||||
set({ isInitialized: initialized });
|
set({ isInitialized: initialized });
|
||||||
},
|
},
|
||||||
@@ -388,6 +423,7 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
|
|||||||
loadingMessagesSet: new Set(),
|
loadingMessagesSet: new Set(),
|
||||||
typingUsersMap: new Map(),
|
typingUsersMap: new Map(),
|
||||||
mutedStatusMap: new Map(),
|
mutedStatusMap: new Map(),
|
||||||
|
notificationMutedMap: new Map(),
|
||||||
});
|
});
|
||||||
AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {});
|
AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ export interface ConversationResponse {
|
|||||||
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
|
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
|
||||||
type: ConversationType;
|
type: ConversationType;
|
||||||
is_pinned?: boolean;
|
is_pinned?: boolean;
|
||||||
|
notification_muted?: boolean;
|
||||||
last_seq: number;
|
last_seq: number;
|
||||||
last_message?: MessageResponse;
|
last_message?: MessageResponse;
|
||||||
last_message_at: string;
|
last_message_at: string;
|
||||||
@@ -283,7 +284,8 @@ export interface ConversationResponse {
|
|||||||
export interface ConversationParticipantResponse {
|
export interface ConversationParticipantResponse {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
last_read_seq: number;
|
last_read_seq: number;
|
||||||
muted: boolean;
|
notification_muted: boolean;
|
||||||
|
is_pinned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 会话详情响应
|
// 会话详情响应
|
||||||
@@ -291,6 +293,7 @@ export interface ConversationDetailResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
type: ConversationType;
|
type: ConversationType;
|
||||||
is_pinned?: boolean;
|
is_pinned?: boolean;
|
||||||
|
notification_muted?: boolean;
|
||||||
last_seq: number;
|
last_seq: number;
|
||||||
last_message?: MessageResponse;
|
last_message?: MessageResponse;
|
||||||
last_message_at: string;
|
last_message_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user