Merge pull request #511 from nagisa77/codex/support-disabling-message-notification-types

feat: support notification type preferences
This commit is contained in:
Tim
2025-08-12 14:29:30 +08:00
committed by GitHub
7 changed files with 151 additions and 10 deletions

View File

@@ -3,6 +3,8 @@ package com.openisle.controller;
import com.openisle.dto.NotificationDto;
import com.openisle.dto.NotificationMarkReadRequest;
import com.openisle.dto.NotificationUnreadCountDto;
import com.openisle.dto.NotificationPreferenceDto;
import com.openisle.dto.NotificationPreferenceUpdateRequest;
import com.openisle.mapper.NotificationMapper;
import com.openisle.service.NotificationService;
import lombok.RequiredArgsConstructor;
@@ -40,4 +42,14 @@ public class NotificationController {
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds());
}
@GetMapping("/prefs")
public List<NotificationPreferenceDto> prefs(Authentication auth) {
return notificationService.listPreferences(auth.getName());
}
@PostMapping("/prefs")
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
}
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import com.openisle.model.NotificationType;
import lombok.Data;
/** User notification preference DTO. */
@Data
public class NotificationPreferenceDto {
private NotificationType type;
private boolean enabled;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import com.openisle.model.NotificationType;
import lombok.Data;
/** Request to update a single notification preference. */
@Data
public class NotificationPreferenceUpdateRequest {
private NotificationType type;
private boolean enabled;
}

View File

@@ -7,6 +7,8 @@ import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
/**
* Simple user entity with basic fields and a role.
@@ -62,6 +64,12 @@ public class User {
@Enumerated(EnumType.STRING)
private MedalType displayMedal;
@ElementCollection(targetClass = NotificationType.class)
@CollectionTable(name = "user_disabled_notification_types", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "notification_type")
@Enumerated(EnumType.STRING)
private Set<NotificationType> disabledNotificationTypes = new HashSet<>();
@CreationTimestamp
@Column(nullable = false, updatable = false,
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")

View File

@@ -1,6 +1,7 @@
package com.openisle.service;
import com.openisle.model.*;
import com.openisle.dto.NotificationPreferenceDto;
import com.openisle.repository.NotificationRepository;
import com.openisle.repository.ReactionRepository;
import com.openisle.repository.UserRepository;
@@ -20,7 +21,9 @@ import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/** Service for creating and retrieving notifications. */
@Service
@@ -138,13 +141,43 @@ public class NotificationService {
}
}
public List<NotificationPreferenceDto> listPreferences(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
List<NotificationPreferenceDto> prefs = new ArrayList<>();
for (NotificationType nt : NotificationType.values()) {
NotificationPreferenceDto dto = new NotificationPreferenceDto();
dto.setType(nt);
dto.setEnabled(!disabled.contains(nt));
prefs.add(dto);
}
return prefs;
}
public void updatePreference(String username, NotificationType type, boolean enabled) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
if (enabled) {
disabled.remove(type);
} else {
disabled.add(type);
}
userRepository.save(user);
}
public List<Notification> listNotifications(String username, Boolean read) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
List<Notification> list;
if (read == null) {
return notificationRepository.findByUserOrderByCreatedAtDesc(user);
list = notificationRepository.findByUserOrderByCreatedAtDesc(user);
} else {
list = notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read);
}
return notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read);
return list.stream().filter(n -> !disabled.contains(n.getType())).collect(Collectors.toList());
}
public void markRead(String username, List<Long> ids) {
@@ -162,7 +195,9 @@ public class NotificationService {
public long countUnread(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
return notificationRepository.countByUserAndRead(user, false);
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
return notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, false).stream()
.filter(n -> !disabled.contains(n.getType())).count();
}
public void notifyMentions(String content, User fromUser, Post post, Comment comment) {

View File

@@ -34,11 +34,15 @@
<div class="message-control-container">
<div class="message-control-title">通知设置</div>
<div class="message-control-push-item-container">
<div class="message-control-push-item">已读通知</div>
<div class="message-control-push-item select">订阅者发帖通知</div>
<div class="message-control-push-item">订阅者回复通知</div>
<div class="message-control-push-item">关注者发帖通知</div>
<div class="message-control-push-item">评论回复通知</div>
<div
v-for="pref in notificationPrefs"
:key="pref.type"
class="message-control-push-item"
:class="{ select: pref.enabled }"
@click="togglePref(pref)"
>
{{ formatType(pref.type) }}
</div>
</div>
</div>
</div>
@@ -482,7 +486,13 @@ import BaseTimeline from '../components/BaseTimeline.vue'
import BasePlaceholder from '../components/BasePlaceholder.vue'
import NotificationContainer from '../components/NotificationContainer.vue'
import { getToken, authState } from '../utils/auth'
import { markNotificationsRead, fetchUnreadCount, notificationState } from '../utils/notification'
import {
markNotificationsRead,
fetchUnreadCount,
notificationState,
fetchNotificationPreferences,
updateNotificationPreference,
} from '../utils/notification'
import { toast } from '../main'
import { stripMarkdownLength } from '../utils/markdown'
import TimeManager from '../utils/time'
@@ -496,6 +506,7 @@ export default {
const notifications = ref([])
const isLoadingMessage = ref(false)
const selectedTab = ref('unread')
const notificationPrefs = ref([])
const filteredNotifications = computed(() =>
selectedTab.value === 'all'
? notifications.value
@@ -568,6 +579,7 @@ export default {
return
}
isLoadingMessage.value = true
notifications.value = []
const res = await fetch(`${API_BASE_URL}/api/notifications`, {
headers: {
Authorization: `Bearer ${token}`,
@@ -705,6 +717,21 @@ export default {
}
}
const fetchPrefs = async () => {
notificationPrefs.value = await fetchNotificationPreferences()
}
const togglePref = async (pref) => {
const ok = await updateNotificationPreference(pref.type, !pref.enabled)
if (ok) {
pref.enabled = !pref.enabled
await fetchNotifications()
await fetchUnreadCount()
} else {
toast.error('操作失败')
}
}
const approve = async (id, nid) => {
const token = getToken()
if (!token) return
@@ -768,7 +795,10 @@ export default {
}
}
onMounted(fetchNotifications)
onMounted(() => {
fetchNotifications()
fetchPrefs()
})
return {
notifications,
@@ -783,6 +813,8 @@ export default {
filteredNotifications,
markAllRead,
authState,
notificationPrefs,
togglePref,
}
},
}

View File

@@ -46,3 +46,35 @@ export async function markNotificationsRead(ids) {
return false
}
}
export async function fetchNotificationPreferences() {
try {
const token = getToken()
if (!token) return []
const res = await fetch(`${API_BASE_URL}/api/notifications/prefs`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) return []
return await res.json()
} catch (e) {
return []
}
}
export async function updateNotificationPreference(type, enabled) {
try {
const token = getToken()
if (!token) return false
const res = await fetch(`${API_BASE_URL}/api/notifications/prefs`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type, enabled }),
})
return res.ok
} catch (e) {
return false
}
}