mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-02 18:10:47 +08:00
Merge pull request #511 from nagisa77/codex/support-disabling-message-notification-types
feat: support notification type preferences
This commit is contained in:
@@ -3,6 +3,8 @@ package com.openisle.controller;
|
|||||||
import com.openisle.dto.NotificationDto;
|
import com.openisle.dto.NotificationDto;
|
||||||
import com.openisle.dto.NotificationMarkReadRequest;
|
import com.openisle.dto.NotificationMarkReadRequest;
|
||||||
import com.openisle.dto.NotificationUnreadCountDto;
|
import com.openisle.dto.NotificationUnreadCountDto;
|
||||||
|
import com.openisle.dto.NotificationPreferenceDto;
|
||||||
|
import com.openisle.dto.NotificationPreferenceUpdateRequest;
|
||||||
import com.openisle.mapper.NotificationMapper;
|
import com.openisle.mapper.NotificationMapper;
|
||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -40,4 +42,14 @@ public class NotificationController {
|
|||||||
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
|
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
|
||||||
notificationService.markRead(auth.getName(), req.getIds());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import lombok.Setter;
|
|||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple user entity with basic fields and a role.
|
* Simple user entity with basic fields and a role.
|
||||||
@@ -62,6 +64,12 @@ public class User {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private MedalType displayMedal;
|
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
|
@CreationTimestamp
|
||||||
@Column(nullable = false, updatable = false,
|
@Column(nullable = false, updatable = false,
|
||||||
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openisle.service;
|
package com.openisle.service;
|
||||||
|
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
|
import com.openisle.dto.NotificationPreferenceDto;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
@@ -20,7 +21,9 @@ import java.util.Set;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/** Service for creating and retrieving notifications. */
|
/** Service for creating and retrieving notifications. */
|
||||||
@Service
|
@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) {
|
public List<Notification> listNotifications(String username, Boolean read) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
|
Set<NotificationType> disabled = user.getDisabledNotificationTypes();
|
||||||
|
List<Notification> list;
|
||||||
if (read == null) {
|
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) {
|
public void markRead(String username, List<Long> ids) {
|
||||||
@@ -162,7 +195,9 @@ public class NotificationService {
|
|||||||
public long countUnread(String username) {
|
public long countUnread(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.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) {
|
public void notifyMentions(String content, User fromUser, Post post, Comment comment) {
|
||||||
|
|||||||
@@ -34,11 +34,15 @@
|
|||||||
<div class="message-control-container">
|
<div class="message-control-container">
|
||||||
<div class="message-control-title">通知设置</div>
|
<div class="message-control-title">通知设置</div>
|
||||||
<div class="message-control-push-item-container">
|
<div class="message-control-push-item-container">
|
||||||
<div class="message-control-push-item">已读通知</div>
|
<div
|
||||||
<div class="message-control-push-item select">订阅者发帖通知</div>
|
v-for="pref in notificationPrefs"
|
||||||
<div class="message-control-push-item">订阅者回复通知</div>
|
:key="pref.type"
|
||||||
<div class="message-control-push-item">关注者发帖通知</div>
|
class="message-control-push-item"
|
||||||
<div class="message-control-push-item">评论回复通知</div>
|
:class="{ select: pref.enabled }"
|
||||||
|
@click="togglePref(pref)"
|
||||||
|
>
|
||||||
|
{{ formatType(pref.type) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,7 +486,13 @@ import BaseTimeline from '../components/BaseTimeline.vue'
|
|||||||
import BasePlaceholder from '../components/BasePlaceholder.vue'
|
import BasePlaceholder from '../components/BasePlaceholder.vue'
|
||||||
import NotificationContainer from '../components/NotificationContainer.vue'
|
import NotificationContainer from '../components/NotificationContainer.vue'
|
||||||
import { getToken, authState } from '../utils/auth'
|
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 { toast } from '../main'
|
||||||
import { stripMarkdownLength } from '../utils/markdown'
|
import { stripMarkdownLength } from '../utils/markdown'
|
||||||
import TimeManager from '../utils/time'
|
import TimeManager from '../utils/time'
|
||||||
@@ -496,6 +506,7 @@ export default {
|
|||||||
const notifications = ref([])
|
const notifications = ref([])
|
||||||
const isLoadingMessage = ref(false)
|
const isLoadingMessage = ref(false)
|
||||||
const selectedTab = ref('unread')
|
const selectedTab = ref('unread')
|
||||||
|
const notificationPrefs = ref([])
|
||||||
const filteredNotifications = computed(() =>
|
const filteredNotifications = computed(() =>
|
||||||
selectedTab.value === 'all'
|
selectedTab.value === 'all'
|
||||||
? notifications.value
|
? notifications.value
|
||||||
@@ -568,6 +579,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
isLoadingMessage.value = true
|
isLoadingMessage.value = true
|
||||||
|
notifications.value = []
|
||||||
const res = await fetch(`${API_BASE_URL}/api/notifications`, {
|
const res = await fetch(`${API_BASE_URL}/api/notifications`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
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 approve = async (id, nid) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
@@ -768,7 +795,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchNotifications)
|
onMounted(() => {
|
||||||
|
fetchNotifications()
|
||||||
|
fetchPrefs()
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
@@ -783,6 +813,8 @@ export default {
|
|||||||
filteredNotifications,
|
filteredNotifications,
|
||||||
markAllRead,
|
markAllRead,
|
||||||
authState,
|
authState,
|
||||||
|
notificationPrefs,
|
||||||
|
togglePref,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,3 +46,35 @@ export async function markNotificationsRead(ids) {
|
|||||||
return false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user