feat: separate read and done statuses

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-01-26 09:00:29 +02:00
parent c3a22686dd
commit 8724ed1fdf
11 changed files with 214 additions and 114 deletions
@@ -25,6 +25,7 @@ exports.up = async function up(knex) {
table.text('image').nullable();
table.datetime('created').notNullable();
table.datetime('read').nullable();
table.datetime('done').nullable();
table.boolean('saved').defaultTo(false).notNullable();
});
};
@@ -60,12 +60,14 @@ export class DatabaseNotificationsStore implements NotificationsStore {
options: NotificationGetOptions | NotificationModifyOptions,
) => {
const { user_ref, type } = options;
const query = this.db('notifications').where('userRef', user_ref);
const query = this.db('notifications')
.where('userRef', user_ref)
.orderBy('created', 'desc');
if (type === 'unread') {
query.whereNull('read');
} else if (type === 'read') {
query.whereNotNull('read');
if (type === 'undone') {
query.whereNull('done');
} else if (type === 'done') {
query.whereNotNull('done');
} else if (type === 'saved') {
query.where('saved', true);
}
@@ -116,6 +118,16 @@ export class DatabaseNotificationsStore implements NotificationsStore {
await notificationQuery.update({ read: null });
}
async markDone(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ done: new Date(), read: new Date() });
}
async markUndone(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ done: null, read: null });
}
async markSaved(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ saved: true });
@@ -43,6 +43,10 @@ export interface NotificationsStore {
markUnread(options: NotificationModifyOptions): Promise<void>;
markDone(options: NotificationModifyOptions): Promise<void>;
markUndone(options: NotificationModifyOptions): Promise<void>;
markSaved(options: NotificationModifyOptions): Promise<void>;
markUnsaved(options: NotificationModifyOptions): Promise<void>;
@@ -168,10 +168,47 @@ export async function createRouter(
router.get('/status', async (req, res) => {
const user = await getUser(req);
const status = await store.getStatus({ user_ref: user });
const status = await store.getStatus({ user_ref: user, type: 'undone' });
res.send(status);
});
router.post('/done', async (req, res) => {
const user = await getUser(req);
const { ids } = req.body;
if (!ids || !Array.isArray(ids)) {
res.status(400).send();
return;
}
await store.markDone({ user_ref: user, ids });
if (signalService) {
await signalService.publish({
recipients: [user],
message: { action: 'refresh' },
channel: 'notifications',
});
}
res.status(200).send({ ids });
});
router.post('/undo', async (req, res) => {
const user = await getUser(req);
const { ids } = req.body;
if (!ids || !Array.isArray(ids)) {
res.status(400).send();
return;
}
await store.markUndone({ user_ref: user, ids });
if (signalService) {
await signalService.publish({
recipients: [user],
message: { action: 'refresh' },
channel: 'notifications',
});
}
res.status(200).send({ ids });
});
router.post('/read', async (req, res) => {
const user = await getUser(req);
const { ids } = req.body;
+1 -1
View File
@@ -28,5 +28,5 @@ export type NotificationStatus = {
};
// @public (undocumented)
export type NotificationType = 'read' | 'unread' | 'saved';
export type NotificationType = 'undone' | 'done' | 'saved';
```
+1 -1
View File
@@ -15,7 +15,7 @@
*/
/** @public */
export type NotificationType = 'read' | 'unread' | 'saved';
export type NotificationType = 'undone' | 'done' | 'saved';
/** @public */
export type Notification = {
+8 -1
View File
@@ -31,10 +31,14 @@ export interface NotificationsApi {
// (undocumented)
getStatus(): Promise<NotificationStatus>;
// (undocumented)
markDone(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markRead(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markSaved(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUndone(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUnread(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUnsaved(ids: string[]): Promise<NotificationIds>;
@@ -53,10 +57,14 @@ export class NotificationsClient implements NotificationsApi {
// (undocumented)
getStatus(): Promise<NotificationStatus>;
// (undocumented)
markDone(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markRead(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markSaved(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUndone(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUnread(ids: string[]): Promise<NotificationIds>;
// (undocumented)
markUnsaved(ids: string[]): Promise<NotificationIds>;
@@ -80,7 +88,6 @@ export const NotificationsSidebarItem: () => React_2.JSX.Element;
export const NotificationsTable: (props: {
onUpdate: () => void;
type: NotificationType;
loading?: boolean;
notifications?: Notification_2[];
}) => React_2.JSX.Element;
@@ -37,6 +37,10 @@ export interface NotificationsApi {
getStatus(): Promise<NotificationStatus>;
markDone(ids: string[]): Promise<NotificationIds>;
markUndone(ids: string[]): Promise<NotificationIds>;
markRead(ids: string[]): Promise<NotificationIds>;
markUnread(ids: string[]): Promise<NotificationIds>;
@@ -52,6 +52,22 @@ export class NotificationsClient implements NotificationsApi {
return await this.request<NotificationStatus>('status');
}
async markDone(ids: string[]): Promise<NotificationIds> {
return await this.request<NotificationIds>('done', {
method: 'POST',
body: JSON.stringify({ ids: ids }),
headers: { 'Content-Type': 'application/json' },
});
}
async markUndone(ids: string[]): Promise<NotificationIds> {
return await this.request<NotificationIds>('undone', {
method: 'POST',
body: JSON.stringify({ ids: ids }),
headers: { 'Content-Type': 'application/json' },
});
}
async markRead(ids: string[]): Promise<NotificationIds> {
return await this.request<NotificationIds>('read', {
method: 'POST',
@@ -37,7 +37,7 @@ const useStyles = makeStyles(_theme => ({
}));
export const NotificationsPage = () => {
const [type, setType] = useState<NotificationType>('unread');
const [type, setType] = useState<NotificationType>('undone');
const [refresh, setRefresh] = React.useState(false);
const { error, value, retry } = useNotificationsApi(
@@ -77,16 +77,16 @@ export const NotificationsPage = () => {
<Button
className={styles.filterButton}
startIcon={<Inbox />}
variant={type === 'unread' ? 'contained' : 'text'}
onClick={() => setType('unread')}
variant={type === 'undone' ? 'contained' : 'text'}
onClick={() => setType('undone')}
>
Inbox
</Button>
<Button
className={styles.filterButton}
startIcon={<Check />}
variant={type === 'read' ? 'contained' : 'text'}
onClick={() => setType('read')}
variant={type === 'done' ? 'contained' : 'text'}
onClick={() => setType('done')}
>
Done
</Button>
@@ -20,6 +20,7 @@ import {
IconButton,
makeStyles,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
@@ -49,9 +50,13 @@ const useStyles = makeStyles(theme => ({
header: {
borderBottom: `1px solid ${theme.palette.divider}`,
},
notificationRow: {
cursor: 'pointer',
'&.hideOnHover': {
'&.unread': {
border: '1px solid rgba(255, 255, 255, .3)',
},
'& .hideOnHover': {
display: 'initial',
},
'& .showOnHover': {
@@ -138,12 +143,12 @@ export const NotificationsTable = (props: {
type !== 'saved' &&
'Select all'}
{selected.length > 0 && `${selected.length} selected`}
{type === 'read' && selected.length > 0 && (
{type === 'done' && selected.length > 0 && (
<Button
startIcon={<Inbox fontSize="small" />}
onClick={() => {
notificationsApi
.markUnread(selected)
.markUndone(selected)
.then(() => props.onUpdate());
setSelected([]);
}}
@@ -152,12 +157,12 @@ export const NotificationsTable = (props: {
</Button>
)}
{type === 'unread' && selected.length > 0 && (
{type === 'undone' && selected.length > 0 && (
<Button
startIcon={<Check fontSize="small" />}
onClick={() => {
notificationsApi
.markRead(selected)
.markDone(selected)
.then(() => props.onUpdate());
setSelected([]);
}}
@@ -168,107 +173,121 @@ export const NotificationsTable = (props: {
</TableCell>
</TableRow>
</TableHead>
{props.notifications?.map(notification => {
return (
<TableRow
key={notification.id}
className={styles.notificationRow}
hover
>
<TableCell
width="60px"
style={{ verticalAlign: 'center', paddingRight: '0px' }}
<TableBody>
{props.notifications?.map(notification => {
return (
<TableRow
key={notification.id}
className={`${styles.notificationRow} ${
!notification.read ? 'unread' : ''
}`}
hover
>
<Checkbox
className={styles.checkBox}
size="small"
checked={isChecked(notification.id)}
onClick={() => onCheckBoxClick(notification.id)}
/>
</TableCell>
<TableCell
onClick={() => navigate(notification.link)}
style={{ paddingLeft: 0 }}
>
<Typography variant="subtitle2">{notification.title}</Typography>
<Typography variant="body2">
{notification.description}
</Typography>
</TableCell>
<TableCell style={{ textAlign: 'right' }}>
<Box className="hideOnHover">
<RelativeTime value={notification.created} />
</Box>
<Box className="showOnHover">
<Tooltip title={notification.link}>
<IconButton
className={styles.actionButton}
onClick={() => navigate(notification.link)}
>
<ArrowForwardIcon />
</IconButton>
</Tooltip>
<Tooltip
title={notification.read ? 'Move to inbox' : 'Mark as done'}
>
<IconButton
className={styles.actionButton}
onClick={() => {
if (notification.read) {
notificationsApi
.markUnread([notification.id])
.then(() => {
props.onUpdate();
});
} else {
<TableCell
width="60px"
style={{ verticalAlign: 'center', paddingRight: '0px' }}
>
<Checkbox
className={styles.checkBox}
size="small"
checked={isChecked(notification.id)}
onClick={() => onCheckBoxClick(notification.id)}
/>
</TableCell>
<TableCell
onClick={() =>
notificationsApi
.markRead([notification.id])
.then(() => navigate(notification.link))
}
style={{ paddingLeft: 0 }}
>
<Typography variant="subtitle2">
{notification.title}
</Typography>
<Typography variant="body2">
{notification.description}
</Typography>
</TableCell>
<TableCell style={{ textAlign: 'right' }}>
<Box className="hideOnHover">
<RelativeTime value={notification.created} />
</Box>
<Box className="showOnHover">
<Tooltip title={notification.link}>
<IconButton
className={styles.actionButton}
onClick={() =>
notificationsApi
.markRead([notification.id])
.then(() => {
props.onUpdate();
});
.then(() => navigate(notification.link))
}
}}
>
<ArrowForwardIcon />
</IconButton>
</Tooltip>
<Tooltip
title={notification.read ? 'Move to inbox' : 'Mark as done'}
>
{notification.read ? (
<Inbox fontSize="small" />
) : (
<Check fontSize="small" />
)}
</IconButton>
</Tooltip>
<Tooltip
title={notification.saved ? 'Remove from saved' : 'Save'}
>
<IconButton
className={styles.actionButton}
onClick={() => {
if (notification.saved) {
notificationsApi
.markUnsaved([notification.id])
.then(() => {
props.onUpdate();
});
} else {
notificationsApi
.markSaved([notification.id])
.then(() => {
props.onUpdate();
});
}
}}
<IconButton
className={styles.actionButton}
onClick={() => {
if (notification.read) {
notificationsApi
.markUndone([notification.id])
.then(() => {
props.onUpdate();
});
} else {
notificationsApi
.markDone([notification.id])
.then(() => {
props.onUpdate();
});
}
}}
>
{notification.read ? (
<Inbox fontSize="small" />
) : (
<Check fontSize="small" />
)}
</IconButton>
</Tooltip>
<Tooltip
title={notification.saved ? 'Remove from saved' : 'Save'}
>
{notification.saved ? (
<CloseIcon fontSize="small" />
) : (
<Bookmark fontSize="small" />
)}
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
);
})}
<IconButton
className={styles.actionButton}
onClick={() => {
if (notification.saved) {
notificationsApi
.markUnsaved([notification.id])
.then(() => {
props.onUpdate();
});
} else {
notificationsApi
.markSaved([notification.id])
.then(() => {
props.onUpdate();
});
}
}}
>
{notification.saved ? (
<CloseIcon fontSize="small" />
) : (
<Bookmark fontSize="small" />
)}
</IconButton>
</Tooltip>
</Box>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};