Files
BRETT/app/composables/useNotifications.ts
FliesentischKingandDevin de0cd6d34f Mass-select grid and bulk delete for moderators
- Select mode in the filter row (upload.delete permission): tiles
  toggle selection in the capture phase, a floating action bar
  offers select-all and delete with confirmation popover
- New POST /api/uploads/bulk-delete endpoint (validated, deduped,
  max 200 ids) removing rows, files and attachment data
- upload-deleted broadcast on bulk and single delete; other clients
  refresh the grid and drop matching notifications
- Also: skeleton tile grid as loading state, dropzone logic moved
  to the layout-level GlobalDropZone, obsolete ytimg preconnect
  removed

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-04 03:06:38 +02:00

66 lines
2.4 KiB
TypeScript

export interface AppNotification {
id: number
uploadId: string
commentId: number
isRead: boolean
isMention: boolean
createdAt: string
actorName: string
actorColor: string | null
content: string
uploadType: 'image' | 'video' | 'audio' | 'youtube' | 'twitch' | 'link'
url: string
thumbUrl: string | null
}
export function useNotifications() {
const notifications = useState<AppNotification[]>('notifications:list', () => [])
const unreadCount = useState<number>('notifications:unread', () => 0)
const unreadUploadIds = computed(() =>
new Set(notifications.value.filter(n => !n.isRead).map(n => n.uploadId)),
)
async function fetchAll() {
const data = await $fetch<{ notifications: AppNotification[], unreadCount: number }>('/api/notifications')
notifications.value = data.notifications
unreadCount.value = data.unreadCount
}
function addLive(notification: AppNotification, count?: number) {
const existing = notifications.value.some(n => n.uploadId === notification.uploadId && !n.isRead)
notifications.value = [notification, ...notifications.value.filter(n => n.uploadId !== notification.uploadId)].slice(0, 30)
unreadCount.value = count ?? (unreadCount.value + (existing ? 0 : 1))
}
async function markAllRead() {
if (!unreadCount.value) return
unreadCount.value = 0
notifications.value = notifications.value.map(n => ({ ...n, isRead: true }))
await $fetch('/api/notifications/read', { method: 'POST' })
}
async function markRead(id: number) {
const target = notifications.value.find(n => n.id === id)
notifications.value = notifications.value.filter(n => target ? n.uploadId !== target.uploadId : n.id !== id)
if (target && !target.isRead) {
unreadCount.value = Math.max(0, unreadCount.value - 1)
}
await $fetch(`/api/notifications/${id}/read`, { method: 'POST' })
}
function removeByUploads(uploadIds: string[]) {
const removed = notifications.value.filter(n => uploadIds.includes(n.uploadId))
if (!removed.length) return
notifications.value = notifications.value.filter(n => !uploadIds.includes(n.uploadId))
unreadCount.value = Math.max(0, unreadCount.value - removed.filter(n => !n.isRead).length)
}
function reset() {
notifications.value = []
unreadCount.value = 0
}
return { notifications, unreadCount, unreadUploadIds, fetchAll, addLive, markAllRead, markRead, removeByUploads, reset }
}