Added notification clear, improved notification styling.

This commit is contained in:
DebaucheryLibrarian 2021-04-25 03:08:50 +02:00
parent f8a3bf6a64
commit fc1c2fc2f3
8 changed files with 265 additions and 77 deletions

View File

@ -85,14 +85,14 @@
<Tooltip v-if="me"> <Tooltip v-if="me">
<div <div
class="header-button header-notifications" class="header-button header-notifications"
:class="{ unseen: notifications.length > 0 }" :class="{ unseen: unseenNotifications.length > 0 }"
> >
<Icon icon="bell2" /> <Icon icon="bell2" />
<span <span
v-if="notifications.length > 0" v-if="unseenNotifications.length > 0"
class="notifications-count" class="notifications-count"
>{{ notifications.length }}</span> >{{ unseenNotifications.length }}</span>
</div> </div>
<template v-slot:tooltip> <template v-slot:tooltip>
@ -158,6 +158,10 @@ function notifications() {
return this.$store.state.ui.notifications; return this.$store.state.ui.notifications;
} }
function unseenNotifications() {
return this.$store.state.ui.notifications.filter(notification => !notification.seen);
}
export default { export default {
components: { components: {
Menu, Menu,
@ -175,6 +179,7 @@ export default {
computed: { computed: {
me, me,
notifications, notifications,
unseenNotifications,
}, },
}; };
</script> </script>
@ -324,14 +329,13 @@ export default {
.notifications-count { .notifications-count {
width: 100%; width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: absolute; position: absolute;
bottom: .05rem; bottom: .3rem;
left: .1rem; left: .1rem;
color: var(--text-light); color: var(--primary);
font-size: .6rem; font-size: .6rem;
font-weight: bold; font-weight: bold;
} }

View File

@ -7,8 +7,10 @@
<div class="notifications-actions"> <div class="notifications-actions">
<Icon <Icon
v-if="unseenNotifications.length > 0"
v-tooltip="'Mark all as seen'"
icon="checkmark" icon="checkmark"
@click="clearNotifications" @click="checkNotifications"
/> />
<Icon <Icon
@ -26,7 +28,7 @@
<div class="notifications-body"> <div class="notifications-body">
<span <span
v-if="notifications.length === 0" v-if="notifications.length === 0"
class="notification" class="notifications-empty"
>No notifications</span> >No notifications</span>
<ul <ul
@ -36,7 +38,9 @@
<li <li
v-for="notification in notifications" v-for="notification in notifications"
:key="`notification-${notification.id}`" :key="`notification-${notification.id}`"
:class="{ unseen: !notification.seen }"
class="notification" class="notification"
@click="checkNotification(notification.id)"
> >
<router-link <router-link
:to="`/scene/${notification.scene.id}/${notification.scene.slug}`" :to="`/scene/${notification.scene.id}/${notification.scene.slug}`"
@ -48,11 +52,52 @@
> >
<div class="notification-body"> <div class="notification-body">
New<span <div class="notification-row notification-title">
v-if="notification.alert.tags?.length > 0" <img
class="notification-tidbit" v-if="notification.scene.entity.type === 'network' || notification.scene.entity.independent"
>&nbsp;{{ notification.alert.tags.map(tag => tag.name).join(', ') }}</span> scene<template v-if="notification.alert.actors?.length > 0">&nbsp;with <span class="notification-tidbit">{{ notification.alert.actors.map(actor => actor.name).join(', ') }}</span></template> :src="`/img/logos/${notification.scene.entity.slug}/favicon_dark.png`"
class="notification-favicon"
>
<img
v-else
:src="`/img/logos/${notification.scene.entity.parent.slug}/favicon_dark.png`"
class="notification-favicon"
>
New&nbsp;<ul
v-if="notification.alert.tags.length > 0"
class="nolist notification-tags"
>
<li
v-for="tag in notification.alert.tags"
:key="`notification-tag-${tag.slug}`"
class="notification-tag"
>{{ tag.name }}</li>&nbsp;
</ul>scene
</div>
<div class="notification-row">
<ul
v-if="notification.scene.actors.length > 0"
class="nolist notification-actors"
>
<li
v-for="actor in notification.scene.actors"
:key="`notification-actor-${actor.slug}`"
class="notification-actor"
>{{ actor.name }}</li>
</ul>
</div>
</div> </div>
<Icon
v-if="!notification.seen"
v-tooltip="'Mark as seen'"
icon="checkmark"
class="notification-check"
@click.prevent="checkNotification(notification.id)"
/>
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -67,8 +112,18 @@ function notifications() {
return this.$store.state.ui.notifications; return this.$store.state.ui.notifications;
} }
async function clearNotifications() { function unseenNotifications() {
return this.notifications.filter(notification => !notification.seen);
}
async function checkNotifications() {
await this.$store.dispatch('checkNotifications');
await this.$store.dispatch('fetchNotifications');
}
async function checkNotification(notificationId) {
await this.$store.dispatch('checkNotification', notificationId);
await this.$store.dispatch('fetchNotifications');
} }
export default { export default {
@ -82,9 +137,11 @@ export default {
}, },
computed: { computed: {
notifications, notifications,
unseenNotifications,
}, },
methods: { methods: {
clearNotifications, checkNotifications,
checkNotification,
}, },
}; };
</script> </script>
@ -122,11 +179,23 @@ export default {
box-shadow: 0 0 3px var(--shadow-weak); box-shadow: 0 0 3px var(--shadow-weak);
} }
.notifications-empty {
padding: 1rem;
}
.notification { .notification {
display: block; display: block;
margin: 0 0 -1px 0;
&.unseen {
border-right: solid .5rem var(--primary);
}
&:not(:last-child) { &:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint); .notification-body,
.notification-check {
border-bottom: solid 1px var(--shadow-hint);
}
} }
&:hover { &:hover {
@ -136,18 +205,67 @@ export default {
.notification-link { .notification-link {
display: flex; display: flex;
align-items: stretch;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
.notification-body { .notification-body {
padding: .5rem 1rem; flex-grow: 1;
padding: .4rem 1rem .25rem .5rem;
}
.notification-row {
display: flex;
align-items: center;
&:not(:last-child) {
margin: 0 0 .1rem 0;
}
} }
.notification-tidbit { .notification-tidbit {
font-weight: bold; font-weight: bold;
} }
.notification-favicon {
width: 1rem;
height: 1rem;
margin: 0 .5rem 0 0;
}
.notification-actors {
height: 1.1rem;
display: inline-block;
overflow: hidden;
}
.notification-actor,
.notification-tag {
&:not(:last-child)::after {
content: ',';
padding: 0 .1rem 0 0;
}
}
.notification-actor {
color: var(--shadow-strong);
font-size: .9rem;
}
.notification-tag {
font-weight: bold;
}
.notification-check {
padding: 1rem;
fill: var(--shadow-weak);
&:hover {
fill: var(--primary);
}
}
.poster { .poster {
width: 5rem; width: 5rem;
height: 3rem; height: 3rem;

View File

@ -1,4 +1,4 @@
import { graphql } from '../api'; import { graphql, patch } from '../api';
import { releaseFields, actorStashesFields } from '../fragments'; import { releaseFields, actorStashesFields } from '../fragments';
import { curateRelease, curateActor, curateNotification } from '../curate'; import { curateRelease, curateActor, curateNotification } from '../curate';
@ -39,10 +39,13 @@ function initUiActions(store, _router) {
$hasAuth: Boolean! $hasAuth: Boolean!
$userId: Int $userId: Int
) { ) {
notifications { notifications(
first: 10
) {
id id
sceneId sceneId
userId userId
seen
createdAt createdAt
scene { scene {
${releaseFields} ${releaseFields}
@ -76,6 +79,18 @@ function initUiActions(store, _router) {
return curatedNotifications; return curatedNotifications;
} }
async function checkNotification(context, notificationId) {
await patch(`/users/${store.state.auth.user?.id}/notifications/${notificationId}`, {
seen: true,
});
}
async function checkNotifications() {
await patch(`/users/${store.state.auth.user?.id}/notifications`, {
seen: true,
});
}
async function search({ _commit }, { query, limit = 20 }) { async function search({ _commit }, { query, limit = 20 }) {
const res = await graphql(` const res = await graphql(`
query SearchReleases( query SearchReleases(
@ -217,6 +232,8 @@ function initUiActions(store, _router) {
} }
return { return {
checkNotification,
checkNotifications,
search, search,
setTagFilter, setTagFilter,
setRange, setRange,

View File

@ -6,7 +6,7 @@
}, },
"rules": { "rules": {
"strict": 0, "strict": 0,
"indent": ["error", "tab"], "indent": "off",
"no-tabs": "off", "no-tabs": "off",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}], "no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"no-console": 0, "no-console": 0,

View File

@ -49,7 +49,92 @@ async function removeAlert(alertId) {
await knex('alerts').where('id', alertId).delete(); await knex('alerts').where('id', alertId).delete();
} }
async function notify(scenes, sessionUser) {
const releases = await knex.raw(`
SELECT alerts.id as alert_id, alerts.notify, alerts.email, releases.id as scene_id, users.id as user_id
FROM releases
CROSS JOIN alerts
LEFT JOIN users ON users.id = alerts.user_id
LEFT JOIN releases_tags ON releases_tags.release_id = releases.id
/* match updated IDs from input */
WHERE users.id = :userId
WHERE (releases.id = ANY(:sceneIds))
/* match tags */
AND (NOT EXISTS (SELECT alerts_tags.alert_id
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id)
OR (SELECT array_agg(releases_tags.tag_id)
FROM releases_tags
WHERE releases_tags.release_id = releases.id
GROUP BY releases_tags.release_id)
@> (SELECT array_agg(alerts_tags.tag_id)
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id
GROUP BY alerts_tags.alert_id))
/* match actors */
AND (NOT EXISTS (SELECT alerts_actors.alert_id
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id)
OR (SELECT array_agg(releases_actors.actor_id)
FROM releases_actors
WHERE releases_actors.release_id = releases.id
GROUP BY releases_actors.release_id)
@> (SELECT array_agg(alerts_actors.actor_id)
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id
GROUP BY alerts_actors.alert_id))
/* match entity */
AND ((NOT EXISTS (SELECT alerts_entities.entity_id
FROM alerts_entities
WHERE alerts_entities.alert_id = alerts.id))
OR (releases.entity_id
= ANY(array(SELECT alerts_entities.entity_id
FROM alerts_entities
WHERE alerts_entities.alert_id = alerts.id))))
GROUP BY releases.id, users.id, alerts.id;
`, {
sceneIds: scenes.map(scene => scene.id),
userId: sessionUser.id,
});
const notifications = releases.rows.filter(alert => alert.notify);
await knex('notifications')
.insert(notifications.map(notification => ({
user_id: notification.user_id,
alert_id: notification.alert_id,
scene_id: notification.scene_id,
})));
return releases.rows;
}
async function updateNotification(notificationId, notification, sessionUser) {
console.log(notification, sessionUser.id);
await knex('notifications')
.where('user_id', sessionUser.id)
.where('id', notificationId)
.update({
seen: notification.seen,
});
}
async function updateNotifications(notification, sessionUser) {
console.log(notification, sessionUser.id);
await knex('notifications')
.where('user_id', sessionUser.id)
.update({
seen: notification.seen,
});
}
module.exports = { module.exports = {
addAlert, addAlert,
removeAlert, removeAlert,
notify,
updateNotification,
updateNotifications,
}; };

View File

@ -13,6 +13,7 @@ const { associateActors, associateDirectors, scrapeActors, toBaseActors } = requ
const { associateReleaseTags } = require('./tags'); const { associateReleaseTags } = require('./tags');
const { curateEntity } = require('./entities'); const { curateEntity } = require('./entities');
const { associateReleaseMedia } = require('./media'); const { associateReleaseMedia } = require('./media');
const { notify } = require('./alerts');
async function curateReleaseEntry(release, batchId, existingRelease, type = 'scene') { async function curateReleaseEntry(release, batchId, existingRelease, type = 'scene') {
const slugBase = release.title const slugBase = release.title
@ -211,62 +212,6 @@ async function filterDuplicateReleases(releases) {
}; };
} }
async function notifyAlerts(scenes) {
const releases = await knex.raw(`
SELECT alerts.id as alert_id, alerts.notify, alerts.email, releases.id as scene_id, users.id as user_id
FROM releases
CROSS JOIN alerts
LEFT JOIN users ON users.id = alerts.user_id
LEFT JOIN releases_tags ON releases_tags.release_id = releases.id
/* match updated IDs from input */
WHERE (releases.id = ANY(:sceneIds))
/* match tags */
AND (NOT EXISTS (SELECT alerts_tags.alert_id
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id)
OR (SELECT array_agg(releases_tags.tag_id)
FROM releases_tags
WHERE releases_tags.release_id = releases.id
GROUP BY releases_tags.release_id)
@> (SELECT array_agg(alerts_tags.tag_id)
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id
GROUP BY alerts_tags.alert_id))
/* match actors */
AND (NOT EXISTS (SELECT alerts_actors.alert_id
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id)
OR (SELECT array_agg(releases_actors.actor_id)
FROM releases_actors
WHERE releases_actors.release_id = releases.id
GROUP BY releases_actors.release_id)
@> (SELECT array_agg(alerts_actors.actor_id)
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id
GROUP BY alerts_actors.alert_id))
/* match entity */
AND ((NOT EXISTS (SELECT alerts_entities.entity_id
FROM alerts_entities
WHERE alerts_entities.alert_id = alerts.id))
OR (releases.entity_id
= ANY(array(SELECT alerts_entities.entity_id
FROM alerts_entities
WHERE alerts_entities.alert_id = alerts.id))))
GROUP BY releases.id, users.id, alerts.id;
`, { sceneIds: scenes.map(scene => scene.id) });
const notify = releases.rows.filter(alert => alert.notify);
await knex('notifications')
.insert(notify.map(notification => ({
user_id: notification.user_id,
alert_id: notification.alert_id,
scene_id: notification.scene_id,
})));
return releases.rows;
}
async function updateReleasesSearch(releaseIds) { async function updateReleasesSearch(releaseIds) {
logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`); logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`);
@ -394,7 +339,7 @@ async function storeScenes(releases) {
logger.info(`Stored ${storedReleaseEntries.length} releases`); logger.info(`Stored ${storedReleaseEntries.length} releases`);
await notifyAlerts(releasesWithId); await notify(releasesWithId);
return releasesWithId; return releasesWithId;
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const { addAlert, removeAlert } = require('../alerts'); const { addAlert, removeAlert, updateNotifications, updateNotification } = require('../alerts');
async function addAlertApi(req, res) { async function addAlertApi(req, res) {
const alertId = await addAlert(req.body, req.session.user); const alertId = await addAlert(req.body, req.session.user);
@ -14,7 +14,21 @@ async function removeAlertApi(req, res) {
res.status(204).send(); res.status(204).send();
} }
async function updateNotificationsApi(req, res) {
await updateNotifications(req.body, req.session.user);
res.status(204).send();
}
async function updateNotificationApi(req, res) {
await updateNotification(req.params.notificationId, req.body, req.session.user);
res.status(204).send();
}
module.exports = { module.exports = {
addAlert: addAlertApi, addAlert: addAlertApi,
removeAlert: removeAlertApi, removeAlert: removeAlertApi,
updateNotifications: updateNotificationsApi,
updateNotification: updateNotificationApi,
}; };

View File

@ -58,6 +58,8 @@ const {
const { const {
addAlert, addAlert,
removeAlert, removeAlert,
updateNotifications,
updateNotification,
} = require('./alerts'); } = require('./alerts');
async function initServer() { async function initServer() {
@ -91,6 +93,9 @@ async function initServer() {
router.post('/api/users', signup); router.post('/api/users', signup);
router.patch('/api/users/:userId/notifications', updateNotifications);
router.patch('/api/users/:userId/notifications/:notificationId', updateNotification);
router.post('/api/stashes', createStash); router.post('/api/stashes', createStash);
router.patch('/api/stashes/:stashId', updateStash); router.patch('/api/stashes/:stashId', updateStash);
router.delete('/api/stashes/:stashId', removeStash); router.delete('/api/stashes/:stashId', removeStash);