feat(message): add conversation notification mute feature
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 57s
Frontend CI / ota-android (push) Successful in 10m53s
Frontend CI / build-android-apk (push) Failing after 1h12m12s

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:
lafay
2026-04-25 21:23:22 +08:00
parent de0afa93a1
commit 5fa5403d6a
15 changed files with 252 additions and 101 deletions

View File

@@ -47,6 +47,12 @@ import { scheduleService } from '@/services/platform';
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 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 totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT;
// 悬浮TabBar高度 + 安全区域底部
const bottomOffset = isMobile ? FLOATING_TAB_BAR_HEIGHT + insets.bottom : 0;
// 悬浮TabBar高度 + 安全区域底部 + 额外底部padding避免遮挡
const bottomOffset = isMobile ? FLOATING_TAB_BAR_HEIGHT + insets.bottom + 8 : 0;
// 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移
const availableHeight = screenHeight - totalHeaderHeight - bottomOffset;
// 6节课每节课高度 = 可用高度 / 6
@@ -256,11 +262,24 @@ export const ScheduleScreen: React.FC = () => {
return Array.from({ length: TOTAL_WEEKS }, (_, i) => i + 1);
}, []);
// 将当前选中周滚动到 ScrollView 可视区域正中央
useEffect(() => {
const WEEK_ITEM_ESTIMATED_WIDTH = 92;
const centerOffset = SCREEN_WIDTH / 2 - WEEK_ITEM_ESTIMATED_WIDTH / 2 - 40;
const targetX = Math.max(0, (currentWeek - 1) * WEEK_ITEM_ESTIMATED_WIDTH - centerOffset);
weekScrollRef.current?.scrollTo({ x: targetX, animated: true });
// 第 N 个 item 的左边缘在内容坐标系中的位置
// = padding + (N-1) * (width + margin*2) + margin
const itemLeft = WEEK_LIST_PADDING
+ (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]);
useFocusEffect(
@@ -427,14 +446,14 @@ export const ScheduleScreen: React.FC = () => {
accessibilityRole="button"
accessibilityLabel="返回"
>
<MaterialCommunityIcons name="chevron-left" size={28} color="#FFFFFF" />
<MaterialCommunityIcons name="chevron-left" size={28} color={colors.text.primary} />
</TouchableOpacity>
) : null}
<TouchableOpacity
style={styles.settingsButton}
onPress={() => setIsSettingsModalVisible(true)}
>
<MaterialCommunityIcons name="cog" size={24} color="#FFFFFF" />
<MaterialCommunityIcons name="cog" size={22} color={colors.text.secondary} />
</TouchableOpacity>
<ScrollView
ref={weekScrollRef}
@@ -442,25 +461,31 @@ export const ScheduleScreen: React.FC = () => {
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.weekList}
>
{weeks.map(week => (
<TouchableOpacity
key={week}
style={[
styles.weekItem,
currentWeek === week && styles.weekItemActive,
]}
onPress={() => setCurrentWeek(week)}
>
<Text
{weeks.map(week => {
const isCurrentRealWeek = week === INITIAL_WEEK;
const isSelected = currentWeek === week;
return (
<TouchableOpacity
key={week}
style={[
styles.weekItemText,
currentWeek === week && styles.weekItemTextActive,
styles.weekItem,
isSelected && styles.weekItemActive,
isCurrentRealWeek && !isSelected && styles.weekItemCurrentWeek,
]}
onPress={() => setCurrentWeek(week)}
>
{week}
</Text>
</TouchableOpacity>
))}
<Text
style={[
styles.weekItemText,
isSelected && styles.weekItemTextActive,
isCurrentRealWeek && !isSelected && styles.weekItemTextCurrentWeek,
]}
>
{week}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
);
@@ -805,7 +830,7 @@ export const ScheduleScreen: React.FC = () => {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar barStyle="light-content" backgroundColor={colors.primary.main} />
<StatusBar barStyle="dark-content" backgroundColor={colors.background.paper} />
{/* 周选择器 */}
{renderWeekSelector()}
@@ -1147,20 +1172,23 @@ function createScheduleStyles(colors: AppColors) {
// ── 周选择器 ──────────────────────────────────────────
weekSelector: {
height: WEEK_SELECTOR_HEIGHT,
backgroundColor: colors.primary.main,
backgroundColor: colors.background.paper,
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
paddingHorizontal: spacing.sm,
},
weekBarIconButton: {
width: 40,
height: 40,
width: 36,
height: 36,
justifyContent: 'center',
alignItems: 'center',
marginLeft: -8,
marginLeft: -4,
},
settingsButton: {
width: 40,
height: 40,
width: 36,
height: 36,
justifyContent: 'center',
alignItems: 'center',
marginLeft: spacing.xs,
@@ -1170,31 +1198,41 @@ function createScheduleStyles(colors: AppColors) {
alignItems: 'center',
},
weekItem: {
paddingHorizontal: spacing.md,
width: WEEK_ITEM_WIDTH,
paddingVertical: 5,
marginHorizontal: 3,
borderRadius: borderRadius.full,
marginHorizontal: WEEK_ITEM_MARGIN_HORIZONTAL,
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.background.default,
},
weekItemActive: {
backgroundColor: 'rgba(255,255,255,0.28)',
backgroundColor: colors.primary.main,
},
weekItemCurrentWeek: {
backgroundColor: `${colors.primary.main}18`,
},
weekItemText: {
fontSize: fontSizes.sm,
fontWeight: '500',
color: 'rgba(255,255,255,0.75)',
color: colors.text.secondary,
letterSpacing: 0.2,
},
weekItemTextActive: {
color: colors.text.inverse,
fontWeight: '700',
},
weekItemTextCurrentWeek: {
color: colors.primary.main,
fontWeight: '600',
},
// ── 星期标题行 ─────────────────────────────────────────
header: {
flexDirection: 'row',
height: HEADER_HEIGHT,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
timeHeader: {
@@ -1268,7 +1306,7 @@ function createScheduleStyles(colors: AppColors) {
alignItems: 'center',
justifyContent: 'center',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#EBEBEB',
borderBottomColor: colors.divider,
gap: 4,
},
timeSectionBadge: {
@@ -1303,22 +1341,22 @@ function createScheduleStyles(colors: AppColors) {
// ── 单日列 ────────────────────────────────────────────
dayColumn: {
flex: 1,
backgroundColor: colors.background.default, // 使用主题默认背景色
borderRightWidth: 1,
borderRightColor: '#EBEBEB',
backgroundColor: colors.background.default,
borderRightWidth: StyleSheet.hairlineWidth,
borderRightColor: colors.divider,
},
gridCell: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#EBEBEB',
borderBottomColor: colors.divider,
},
// ── 课程卡片 ──────────────────────────────────────────
courseCard: {
position: 'absolute',
borderRadius: borderRadius.xl,
borderRadius: borderRadius.lg,
paddingVertical: spacing.xs + 2,
paddingHorizontal: spacing.xs + 1,
...shadows.md,
...shadows.sm,
},
courseName: {
fontSize: 10,