[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:
MorrisLin
2025-01-25 21:25:59 +08:00
committed by GitHub
parent 77ba797340
commit ba4f2e18b3
4 changed files with 217 additions and 27 deletions

View File

@@ -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()
})
})
})

View File

@@ -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)
},

View File

@@ -73,7 +73,11 @@ function useActiveAdjudication() {
useEffect(() => {
if (activeReport) {
sendNotification(NotificationType.NEW_REPORT_ADJUDICATE)
sendNotification(
NotificationType.NEW_REPORT_ADJUDICATE,
undefined,
activeReport.reportId,
)
}
}, [activeReport, sendNotification])

View File

@@ -29,6 +29,7 @@ export interface NotificationData {
isRead: boolean
link?: string
txHash?: string
reportId?: string
}
interface NotificationAction {