mirror of
https://github.com/social-tw/social-tw-website.git
synced 2026-01-09 15:38:09 -05:00
[Frontend][UST-281] fix notification (#634)
* feat: enhance notification store with duplicate check and improve ID generation * feat: refactor notification store for duplication test: add unit tests for useNotificationStore to validate notification addition and duplication logic * feat: remove comment * feat: add report id to adjudication notifications * feat: remove .idea * feat: lint fix
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { NotificationData, NotificationType } from '@/types/Notifications'
|
||||
import { useNotificationStore } from './useNotificationStore'
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
length: 0,
|
||||
key: jest.fn(),
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
// Mock storage for persist middleware
|
||||
jest.mock('zustand/middleware', () => ({
|
||||
...jest.requireActual('zustand/middleware'),
|
||||
persist: (config: any) => (set: any, get: any, store: any) => {
|
||||
const state = config(
|
||||
(...args: any) => {
|
||||
set(...args)
|
||||
},
|
||||
get,
|
||||
store,
|
||||
)
|
||||
return {
|
||||
...state,
|
||||
persist: () => {},
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useNotificationStore', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
useNotificationStore.getState().reset()
|
||||
})
|
||||
|
||||
describe('addNotification', () => {
|
||||
it('should add normal notification with unique id', () => {
|
||||
const notification: NotificationData = {
|
||||
id: '123',
|
||||
type: NotificationType.POST_SUCCEEDED,
|
||||
message: 'Test message',
|
||||
time: new Date().toLocaleTimeString(),
|
||||
isRead: false,
|
||||
}
|
||||
|
||||
useNotificationStore.getState().addNotification(notification)
|
||||
const state = useNotificationStore.getState()
|
||||
|
||||
expect(state.entities['123']).toEqual({
|
||||
...notification,
|
||||
id: '123',
|
||||
})
|
||||
expect(state.list).toContain('123')
|
||||
})
|
||||
|
||||
it('should prevent duplicate check-in notifications on the same day', () => {
|
||||
const notification: NotificationData = {
|
||||
id: 'check-in-1',
|
||||
type: NotificationType.LOW_REPUTATION,
|
||||
message: 'Low reputation',
|
||||
time: new Date().toLocaleTimeString(),
|
||||
isRead: false,
|
||||
}
|
||||
|
||||
// Add first notification
|
||||
useNotificationStore.getState().addNotification(notification)
|
||||
|
||||
// Try to add second notification
|
||||
const secondNotification = {
|
||||
...notification,
|
||||
id: 'check-in-1',
|
||||
}
|
||||
useNotificationStore.getState().addNotification(secondNotification)
|
||||
|
||||
const state = useNotificationStore.getState()
|
||||
expect(Object.keys(state.entities).length).toBe(1)
|
||||
expect(state.list.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should prevent duplicate adjudication notifications with same report id', () => {
|
||||
const reportId = 'report-123'
|
||||
const notification: NotificationData = {
|
||||
id: `report_${reportId}`,
|
||||
type: NotificationType.NEW_REPORT_ADJUDICATE,
|
||||
message: 'New report',
|
||||
time: new Date().toLocaleTimeString(),
|
||||
isRead: false,
|
||||
reportId,
|
||||
}
|
||||
|
||||
// Add first notification
|
||||
useNotificationStore.getState().addNotification(notification)
|
||||
|
||||
// Try to add second notification with same report id
|
||||
const secondNotification = {
|
||||
...notification,
|
||||
message: 'Same report, different message',
|
||||
time: new Date().toLocaleTimeString(),
|
||||
}
|
||||
useNotificationStore.getState().addNotification(secondNotification)
|
||||
|
||||
const state = useNotificationStore.getState()
|
||||
expect(Object.keys(state.entities).length).toBe(1)
|
||||
expect(state.list.length).toBe(1)
|
||||
// Check that the notification exists with the correct ID format
|
||||
expect(state.entities[`report_${reportId}`]).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,8 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import { immer } from 'zustand/middleware/immer'
|
||||
import { NotificationData } from '@/types/Notifications'
|
||||
import { NotificationType } from '@/types/Notifications'
|
||||
import { useNotificationConfig } from '../config/NotificationConfig'
|
||||
import { NotificationData, NotificationType } from '@/types/Notifications'
|
||||
import { useCallback } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
import { useNotificationConfig } from '../config/NotificationConfig'
|
||||
|
||||
interface NotificationState {
|
||||
entities: Record<string, NotificationData>
|
||||
@@ -16,7 +14,7 @@ interface NotificationState {
|
||||
clearNotificationDot: () => void
|
||||
}
|
||||
|
||||
const initialState: NotificationState = {
|
||||
export const initialState: NotificationState = {
|
||||
entities: {},
|
||||
list: [],
|
||||
showDot: false,
|
||||
@@ -26,34 +24,98 @@ const initialState: NotificationState = {
|
||||
clearNotificationDot: () => {},
|
||||
}
|
||||
|
||||
const getNotificationId = (notification: NotificationData): string => {
|
||||
switch (notification.type) {
|
||||
case NotificationType.NEW_REPORT_ADJUDICATE:
|
||||
return `report_${notification.reportId || notification.id}`
|
||||
case NotificationType.LOW_REPUTATION:
|
||||
return `${notification.type}_${new Date().toDateString()}`
|
||||
default:
|
||||
return notification.id
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if notification already exists
|
||||
const hasExistingNotification = (
|
||||
entities: Record<string, NotificationData>,
|
||||
notification: NotificationData,
|
||||
): boolean => {
|
||||
switch (notification.type) {
|
||||
case NotificationType.LOW_REPUTATION:
|
||||
const today = new Date().toDateString()
|
||||
return Object.values(entities).some(
|
||||
(n) =>
|
||||
n.type === NotificationType.LOW_REPUTATION &&
|
||||
new Date(n.time).toDateString() === today,
|
||||
)
|
||||
case NotificationType.NEW_REPORT_ADJUDICATE:
|
||||
// use id to check if notification exists
|
||||
return Object.values(entities).some(
|
||||
(n) =>
|
||||
n.type === NotificationType.NEW_REPORT_ADJUDICATE &&
|
||||
n.id === notification.id,
|
||||
)
|
||||
default:
|
||||
return notification.id in entities
|
||||
}
|
||||
}
|
||||
|
||||
export const useNotificationStore = create<NotificationState>()(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
...initialState,
|
||||
(set, get) => ({
|
||||
entities: {},
|
||||
list: [],
|
||||
showDot: false,
|
||||
addNotification: (notification: NotificationData) => {
|
||||
set((state) => {
|
||||
const id = notification.id.toString()
|
||||
if (!state.entities[id]) {
|
||||
state.entities[id] = notification
|
||||
state.list.push(id)
|
||||
state.showDot = true
|
||||
}
|
||||
})
|
||||
const state = get()
|
||||
const id = getNotificationId(notification)
|
||||
|
||||
if (hasExistingNotification(state.entities, notification)) {
|
||||
return
|
||||
}
|
||||
|
||||
const newNotification = {
|
||||
...notification,
|
||||
id,
|
||||
}
|
||||
|
||||
// Only update state if this is a new notification
|
||||
if (!state.list.includes(id)) {
|
||||
set({
|
||||
entities: {
|
||||
...state.entities,
|
||||
[id]: newNotification,
|
||||
},
|
||||
list: [...state.list, id],
|
||||
showDot: true,
|
||||
})
|
||||
}
|
||||
},
|
||||
markAsRead: (id: string) => {
|
||||
set((state) => {
|
||||
if (state.entities[id]) {
|
||||
state.entities[id].isRead = true
|
||||
}
|
||||
})
|
||||
const state = get()
|
||||
if (state.entities[id]) {
|
||||
set({
|
||||
entities: {
|
||||
...state.entities,
|
||||
[id]: {
|
||||
...state.entities[id],
|
||||
isRead: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
reset: () => {
|
||||
set({ ...initialState })
|
||||
set({
|
||||
entities: {},
|
||||
list: [],
|
||||
showDot: false,
|
||||
})
|
||||
},
|
||||
clearNotificationDot: () => {
|
||||
set({ showDot: false })
|
||||
},
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: 'notifications-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
@@ -82,7 +144,7 @@ export function useSendNotification() {
|
||||
)
|
||||
|
||||
const sendNotification = useCallback(
|
||||
(type: NotificationType, link?: string) => {
|
||||
(type: NotificationType, link?: string, reportId?: string) => {
|
||||
const config = notificationConfig[type]
|
||||
if (!config) {
|
||||
console.warn(
|
||||
@@ -92,12 +154,19 @@ export function useSendNotification() {
|
||||
}
|
||||
|
||||
const notification: NotificationData = {
|
||||
id: Date.now().toString(),
|
||||
id:
|
||||
type === NotificationType.NEW_REPORT_ADJUDICATE && reportId
|
||||
? `report_${reportId}`
|
||||
: Date.now().toString(),
|
||||
type,
|
||||
message: config.message,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
isRead: false,
|
||||
link,
|
||||
reportId:
|
||||
type === NotificationType.NEW_REPORT_ADJUDICATE
|
||||
? reportId
|
||||
: undefined,
|
||||
}
|
||||
addNotification(notification)
|
||||
},
|
||||
|
||||
@@ -73,7 +73,11 @@ function useActiveAdjudication() {
|
||||
|
||||
useEffect(() => {
|
||||
if (activeReport) {
|
||||
sendNotification(NotificationType.NEW_REPORT_ADJUDICATE)
|
||||
sendNotification(
|
||||
NotificationType.NEW_REPORT_ADJUDICATE,
|
||||
undefined,
|
||||
activeReport.reportId,
|
||||
)
|
||||
}
|
||||
}, [activeReport, sendNotification])
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface NotificationData {
|
||||
isRead: boolean
|
||||
link?: string
|
||||
txHash?: string
|
||||
reportId?: string
|
||||
}
|
||||
|
||||
interface NotificationAction {
|
||||
|
||||
Reference in New Issue
Block a user