Skip to content

Commit

Permalink
Fix site announcement notifications (#985)
Browse files Browse the repository at this point in the history
* Fix site update publishing
* Sanitize ids
  • Loading branch information
sceuick authored Jul 30, 2024
1 parent 988ddf5 commit 37dd2bb
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 23 deletions.
2 changes: 1 addition & 1 deletion common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ export function clamp(toClamp: number, max: number, min?: number) {
}

export function now() {
return new Date().toISOString()
return new Date(Date.now()).toISOString()
}

export function parseStops(stops?: string[]) {
Expand Down
6 changes: 6 additions & 0 deletions srv/api/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { store } from '../db'
import { handle } from './wrap'
import { assertValid } from '/common/valid'
import { isAdmin } from './auth'
import { sendAll } from './ws'

const valid = {
title: 'string',
Expand Down Expand Up @@ -36,6 +37,11 @@ const updateAnnouncement = handle(async (req) => {
const createAnnouncement = handle(async (req) => {
assertValid(valid, req.body)
const next = await store.announce.createAnnouncement(req.body)

if (next.showAt <= new Date().toISOString() && !next.hide) {
sendAll({ type: 'announcement', announcement: next })
}

return next
})

Expand Down
4 changes: 3 additions & 1 deletion srv/api/chat/characters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ export const upsertTempCharacter = handle(async ({ body, params, userId }) => {
const tempCharacters = chat.tempCharacters || {}
const prev = body._id ? tempCharacters[body._id] : null

const id = body._id && tempCharacters[body._id] ? body._id : `temp-${v4().slice(0, 8)}`

const upserted: AppSchema.Character = {
_id: body._id || `temp-${v4().slice(0, 8)}`,
_id: id,
kind: 'character',
createdAt: now(),
updatedAt: now(),
Expand Down
4 changes: 4 additions & 0 deletions srv/db/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export async function updateAnnouncement(id: string, update: Partial<AppSchema.A

export async function createAnnouncement(create: OmitId<AppSchema.Announcement, Dates>) {
const id = v4()
if (create.showAt <= new Date().toISOString()) {
create.showAt = new Date(Date.now() - 30000).toISOString()
}

const insert = {
_id: id,
kind: 'announcement',
Expand Down
13 changes: 12 additions & 1 deletion srv/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as uuid from 'uuid'
import pino from 'pino'
import type { NextFunction, Response } from 'express'
import { errors } from './api/wrap'
import { StatusError, errors } from './api/wrap'
import { verifyApiKey } from './db/oauth'
import { verifyJwt } from './db/user'
import { config } from './config'
Expand Down Expand Up @@ -49,8 +49,19 @@ function parentLogger(name: string) {
return pino(opts) as any as AppLog
}

const VALID_ID = /^[a-z0-9-]+$/g

const ID_KEYS = ['id', 'charId', 'inviteId', 'userId']

export function logMiddleware() {
const middleware = async (req: any, res: Response, next: NextFunction) => {
for (const prop in ID_KEYS) {
const value = req.params[prop]
if (value && !VALID_ID.test(value)) {
return next(new StatusError('Bad request: Invalid ID', 400))
}
}

const requestId = uuid.v4()
const log = logger.child({ requestId, url: req.url, method: req.method })

Expand Down
9 changes: 8 additions & 1 deletion web/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import AvatarIcon, { CharacterAvatar } from './shared/AvatarIcon'
import {
UserState,
announceStore,
audioStore,
characterStore,
inviteStore,
Expand Down Expand Up @@ -325,9 +326,15 @@ const NavIcons: Component<{
}> = (props) => {
const invites = inviteStore()
const toasts = toastStore()
const announce = announceStore()

const count = createMemo(() => {
return toasts.unseen + invites.invites.length
const threshold = new Date(props.user.user?.announcement || 0).toISOString()
const unseen = announce.list.filter(
(l) => l.location === 'notification' && l.showAt > threshold
)

return unseen.length + toasts.unseen + invites.invites.length
})

return (
Expand Down
42 changes: 34 additions & 8 deletions web/Toasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,50 @@ const Notifications: Component = () => {
const user = userStore()
const state = toastStore()

const [bookmark] = createSignal(new Date(user.user?.announcement || 0))
const bookmark = createMemo(() => new Date(user.user?.announcement || Date.now()))
const [read, setRead] = createSignal('')

onMount(() => {
announceStore.getAll()
})

createEffect(
on(
() => state.modal,
(open) => {
if (!open) {
const readTime = read()
if (!readTime) return
setRead('')
userStore.updatePartialConfig({ announcement: readTime }, true)
return
}
announceStore.getAll()
setRead(new Date().toISOString())
return open
}
)
)

const [tab, setTab] = createSignal(1)

const unseen = createMemo(() => {
const all = createMemo(() => {
return announce.list
.slice(0, 5)
.filter((ann) => ann.location === 'notification' && ann.updatedAt > bookmark().toISOString())
.filter((ann) => ann.location === 'notification')
.slice(0, 8)
.sort((l, r) => r.showAt.localeCompare(l.showAt))
})

const unseen = createMemo(() => {
return all().filter(
(ann) => ann.location === 'notification' && ann.showAt > bookmark().toISOString()
)
})

const seen = createMemo(() => {
return announce.list
.slice(0, 5)
.filter((ann) => ann.location === 'notification' && ann.updatedAt <= bookmark().toISOString())
return all().filter(
(ann) => ann.location === 'notification' && ann.showAt <= bookmark().toISOString()
)
})

createEffect(
Expand All @@ -51,7 +77,7 @@ const Notifications: Component = () => {
if (id !== 1) return
if (unseen().length === 0) return

userStore.updatePartialConfig({ announcement: new Date().toISOString() })
userStore.updatePartialConfig({ announcement: new Date().toISOString() }, true)
}
)
)
Expand Down
40 changes: 32 additions & 8 deletions web/pages/Admin/Announcements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import PageHeader from '/web/shared/PageHeader'
import { Eye, EyeOff, Plus, Save } from 'lucide-solid'
import Button from '/web/shared/Button'
import TextInput from '/web/shared/TextInput'
import TextInput, { ButtonInput } from '/web/shared/TextInput'
import { useNavigate, useParams } from '@solidjs/router'
import { announceStore, toastStore } from '/web/store'
import { elapsedSince, now } from '/common/util'
Expand Down Expand Up @@ -81,10 +81,15 @@ const AnnoucementList: Component = (props) => {
}}
onClick={() => nav(`/admin/announcements/${item._id}`)}
>
<div class="font-bold">{item.title}</div>
<div class="font-bold">
{item.title}{' '}
<span class="text-500 text-xs font-light italic">
{item.location === 'notification' ? 'notify' : 'home'}
</span>
</div>
<div class="flex gap-1">
<Pill>Created: {new Date(item.showAt).toLocaleString()}</Pill>
<Pill>{elapsedSince(new Date(item.showAt))} ago</Pill>
<Pill inverse>Created: {new Date(item.showAt).toLocaleString()}</Pill>
<Pill inverse>{elapsedSince(new Date(item.showAt))} ago</Pill>
{Label(item)}
</div>
</div>
Expand Down Expand Up @@ -115,11 +120,16 @@ function Label(item: AppSchema.Announcement) {
if (item.deletedAt) return <Pill type="rose">Deleted</Pill>
if (item.hide) return <Pill type="coolgray">Hidden</Pill>
if (date.valueOf() >= Date.now()) return <Pill type="premium">Pending</Pill>
return <Pill type="green">Active</Pill>
return (
<Pill inverse type="green">
Active
</Pill>
)
}

const Announcement: Component<{}> = (props) => {
let ref: HTMLFormElement
let showAtRef: HTMLInputElement

const nav = useNavigate()
const params = useParams()
Expand Down Expand Up @@ -206,13 +216,27 @@ const Announcement: Component<{}> = (props) => {
onInput={(ev) => setContent(ev.currentTarget.value)}
/>
<Toggle fieldName="hide" label="Hide Announcement" value={state.item?.hide} />
<TextInput
<ButtonInput
ref={(r) => (showAtRef = r)}
fieldName="showAt"
type="datetime-local"
label="Display At"
fieldName="showAt"
value={state.item?.showAt ? toLocalTime(state.item.showAt) : toLocalTime(now())}
onChange={(ev) => setShowAt(new Date(ev.currentTarget.value))}
/>
>
<Button
size="sm"
class="mr-20 text-xs"
schema="clear"
onClick={() => {
const time = toLocalTime(new Date(Date.now() - 60000).toISOString())
setShowAt(new Date(time))
showAtRef.value = time
}}
>
Now
</Button>
</ButtonInput>

<div class="flex justify-end gap-2">
<Button onClick={onSave}>
Expand Down
4 changes: 3 additions & 1 deletion web/pages/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ const HomePage: Component = () => {
<RecentChats />

<Show when={announce.list.length > 0}>
<Announcements list={announce.list} />
<Announcements
list={announce.list.filter((ann) => ann.location !== 'notification').slice(0, 4)}
/>
</Show>

<div class="home-cards">
Expand Down
8 changes: 8 additions & 0 deletions web/store/announce.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { api } from './api'
import { createStore } from './create'
import { subscribe } from './socket'
import { toastStore } from './toasts'
import { AppSchema } from '/common/types'

Expand Down Expand Up @@ -77,3 +78,10 @@ export const announceStore = createStore<AnnounceState>(
},
}
})

subscribe('announcement', { announcement: 'any' }, (body) => {
const { list } = announceStore.getState()

const next = list.concat(body.announcement)
announceStore.setState({ list: next })
})
7 changes: 5 additions & 2 deletions web/store/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,15 @@ export const userStore = createStore<UserState>(
}
},

async updatePartialConfig(_, config: ConfigUpdate) {
async updatePartialConfig(_, config: ConfigUpdate, quiet?: boolean) {
const res = await usersApi.updatePartialConfig(config)
if (res.error) toastStore.error(`Failed to update config: ${res.error}`)
if (res.result) {
window.usePipeline = res.result.useLocalPipeline
toastStore.success(`Updated settings`)

if (!quiet) {
toastStore.success(`Updated settings`)
}
return { user: res.result }
}
},
Expand Down

0 comments on commit 37dd2bb

Please sign in to comment.