Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions functions/src/aggregations/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
exports.user = require('./user.aggregations')
exports.userNotifications = require('./userNotifications.aggregations')
42 changes: 42 additions & 0 deletions functions/src/aggregations/userNotifications.aggregations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as functions from 'firebase-functions'
import { DB_ENDPOINTS, IUserDB } from '../models'
import { handleDBAggregations, VALUE_MODIFIERS } from './common.aggregations'
import type { IAggregation } from './common.aggregations'

interface INotificationAggregation extends IAggregation {
sourceFields: (keyof IUserDB)[]
}

const aggregations: INotificationAggregation[] = [
// When a user's list of notifications changes reflect to aggregation
{
sourceCollection: 'users',
sourceFields: ['notifications', 'notification_settings'],
changeType: 'updated',
targetCollection: 'user_notifications',
targetDocId: 'emails_pending',
process: ({ dbChange }) => {
const user: IUserDB = dbChange.after.data() as any
const { _id, _authID, notification_settings, notifications } = user
const emailFrequency = notification_settings?.emailFrequency || null
const pending = notifications.filter((n) => !n.notified && !n.read)
// remove user from list if they do not have emails enabled or no pending notifications
if (!emailFrequency || pending.length === 0) {
return {
[_id]: VALUE_MODIFIERS.delete(),
}
}
// return list of pending notifications alongside metadata
return {
[_id]: { _authID, emailFrequency, notifications: pending },
}
},
},
]

/** Watch changes to all user docs and apply aggregations */
exports.default = functions.firestore
.document(`${DB_ENDPOINTS.users}/{id}`)
.onUpdate((change) => {
return handleDBAggregations(change, aggregations)
})
1 change: 1 addition & 0 deletions shared/models/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export const generateDBEndpoints = (DB_PREFIX = '') => ({
howtos: `${DB_PREFIX}v3_howtos`,
users: `${DB_PREFIX}v3_users`,
user_notifications: `${DB_PREFIX}user_notifications_rev20221209`,
tags: `${DB_PREFIX}v3_tags`,
categories: `${DB_PREFIX}v3_categories`,
researchCategories: `${DB_PREFIX}research_categories_rev20221224`,
Expand Down
8 changes: 7 additions & 1 deletion src/modules/admin/admin.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export const ADMIN_PAGES: IAdminPageMeta[] = [
moduleName,
disabled: true,
},
{
component: lazy(() => import('./pages/adminNotifications')),
title: 'Notifications',
description: 'Manage Notifications',
path: '/notifications',
moduleName,
},
]

const routes = () => (
Expand All @@ -89,5 +96,4 @@ const routes = () => (
</Switch>
</Suspense>
)

export default withRouter(routes)
99 changes: 99 additions & 0 deletions src/modules/admin/pages/adminNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import styled from '@emotion/styled'
import { observer } from 'mobx-react'
import { NotificationItem } from 'oa-components'
import { useCallback, useEffect, useState } from 'react'
import { Box, Text } from 'theme-ui'
import { useDB } from 'src/App'
import type { INotification, INotificationSettings } from 'src/models'
import { getFormattedNotificationMessage } from 'src/pages/common/Header/getFormattedNotifications'
import Table from '../components/Table/Table'
import type { ICellRenderProps, ITableProps } from '../components/Table/Table'

interface IPendingEmails {
_authID: 'string'
emailFrequency?: INotificationSettings['emailFrequency']
notifications: INotification[]
}
type IPendingEmailsDBDoc = Record<string, IPendingEmails>

const EMAILS_PENDING_COLUMNS: ITableProps<IPendingEmails>['columns'] = [
{
Header: 'Auth ID',
accessor: '_authID',
minWidth: 100,
},
{
Header: 'Frequency',
accessor: 'emailFrequency',
minWidth: 100,
},
{
Header: 'Notifications',
accessor: 'notifications',
minWidth: 140,
},
]
const NotificationListContainer = styled(Box)`
overflow: auto;
height: 6rem;
`

const AdminNotifictions = observer(() => {
const { db } = useDB()
const [emailsPending, setEmailsPending] = useState<IPendingEmails[]>([])

// Load list of pending approvals on mount only, dependencies empty to avoid reloading
useEffect(() => {
getPendingEmails()
}, [])

const getPendingEmails = useCallback(async () => {
db.collection<IPendingEmailsDBDoc>('user_notifications')
.doc('emails_pending')
.stream()
.subscribe((emailsPending) => {
if (emailsPending) {
const values = Object.values(emailsPending)
if (values.length > 0) {
setEmailsPending(values)
}
}
})
}, [])

/** Function applied to render each table row cell */
const RenderContent: React.FC<ICellRenderProps> = (
props: ICellRenderProps,
) => {
const { col } = props
const { field, value } = col
const tableField = field as keyof IPendingEmails
if (!value) return null
if (tableField === 'notifications') {
const notifications = value as INotification[]
return (
<NotificationListContainer>
{notifications.map((n) => (
<NotificationItem key={n._id} type={n.type}>
{getFormattedNotificationMessage(n)}
</NotificationItem>
))}
</NotificationListContainer>
)
}
return <Text>{`${value}`}</Text>
}

return (
<>
<h2>Admin Notifictions</h2>
<h4>Pending Emails</h4>
<Table
data={emailsPending}
columns={EMAILS_PENDING_COLUMNS}
rowComponent={RenderContent}
/>
</>
)
})
export default AdminNotifictions
4 changes: 2 additions & 2 deletions src/pages/common/Header/getFormattedNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { INotification } from 'src/models'
import type { UserNotificationsStore } from 'src/stores/User/notifications.store'
import { Box } from 'theme-ui'

function getFormattedMessage(notification: INotification) {
export function getFormattedNotificationMessage(notification: INotification) {
switch (notification.type) {
case 'new_comment':
return (
Expand Down Expand Up @@ -86,6 +86,6 @@ export function getFormattedNotifications(
.getUnreadNotifications()
.map((notification) => ({
type: notification.type,
children: getFormattedMessage(notification),
children: getFormattedNotificationMessage(notification),
}))
}
5 changes: 4 additions & 1 deletion src/stores/databaseV2/clients/dexie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,12 @@ export class DexieClient implements AbstractDBClient {
/************************************************************************
* Interfaces and constants
***********************************************************************/
// Frontend code does not access all database endpoints, exclude here
type IFrontendEndpoints = Exclude<IDBEndpoint, 'user_notifications'>

// When dexie is initialised it requires explicit knowledge of the database structures and any keys to
// index on. The below interface and constant ensures this is done for the current db api version
type IDexieSchema = { [key in IDBEndpoint]: string }
type IDexieSchema = { [key in IFrontendEndpoints]: string }

// by default _id will serve as primary key and additional index created on _modified for faster querying
const DEFAULT_SCHEMA = '_id,_modified'
Expand Down