feat: separate read and done statuses
Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -28,5 +28,5 @@ export type NotificationStatus = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationType = 'read' | 'unread' | 'saved';
|
||||
export type NotificationType = 'undone' | 'done' | 'saved';
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type NotificationType = 'read' | 'unread' | 'saved';
|
||||
export type NotificationType = 'undone' | 'done' | 'saved';
|
||||
|
||||
/** @public */
|
||||
export type Notification = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user