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

View File

@ -7,8 +7,10 @@
<div class="notifications-actions">
<Icon
v-if="unseenNotifications.length > 0"
v-tooltip="'Mark all as seen'"
icon="checkmark"
@click="clearNotifications"
@click="checkNotifications"
/>
<Icon
@ -26,7 +28,7 @@
<div class="notifications-body">
<span
v-if="notifications.length === 0"
class="notification"
class="notifications-empty"
>No notifications</span>
<ul
@ -36,7 +38,9 @@
<li
v-for="notification in notifications"
:key="`notification-${notification.id}`"
:class="{ unseen: !notification.seen }"
class="notification"
@click="checkNotification(notification.id)"
>
<router-link
:to="`/scene/${notification.scene.id}/${notification.scene.slug}`"
@ -48,11 +52,52 @@
>
<div class="notification-body">
New<span
v-if="notification.alert.tags?.length > 0"
class="notification-tidbit"
>&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>
<div class="notification-row notification-title">
<img
v-if="notification.scene.entity.type === 'network' || notification.scene.entity.independent"
: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>
<Icon
v-if="!notification.seen"
v-tooltip="'Mark as seen'"
icon="checkmark"
class="notification-check"
@click.prevent="checkNotification(notification.id)"
/>
</router-link>
</li>
</ul>
@ -67,8 +112,18 @@ function 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 {
@ -82,9 +137,11 @@ export default {
},
computed: {
notifications,
unseenNotifications,
},
methods: {
clearNotifications,
checkNotifications,
checkNotification,
},
};
</script>
@ -122,11 +179,23 @@ export default {
box-shadow: 0 0 3px var(--shadow-weak);
}
.notifications-empty {
padding: 1rem;
}
.notification {
display: block;
margin: 0 0 -1px 0;
&.unseen {
border-right: solid .5rem var(--primary);
}
&:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint);
.notification-body,
.notification-check {
border-bottom: solid 1px var(--shadow-hint);
}
}
&:hover {
@ -136,18 +205,67 @@ export default {
.notification-link {
display: flex;
align-items: stretch;
color: inherit;
text-decoration: none;
}
.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 {
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 {
width: 5rem;
height: 3rem;

View File

@ -1,4 +1,4 @@
import { graphql } from '../api';
import { graphql, patch } from '../api';
import { releaseFields, actorStashesFields } from '../fragments';
import { curateRelease, curateActor, curateNotification } from '../curate';
@ -39,10 +39,13 @@ function initUiActions(store, _router) {
$hasAuth: Boolean!
$userId: Int
) {
notifications {
notifications(
first: 10
) {
id
sceneId
userId
seen
createdAt
scene {
${releaseFields}
@ -76,6 +79,18 @@ function initUiActions(store, _router) {
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 }) {
const res = await graphql(`
query SearchReleases(
@ -217,6 +232,8 @@ function initUiActions(store, _router) {
}
return {
checkNotification,
checkNotifications,
search,
setTagFilter,
setRange,

View File

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

View File

@ -49,7 +49,92 @@ async function removeAlert(alertId) {
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 = {
addAlert,
removeAlert,
notify,
updateNotification,
updateNotifications,
};

View File

@ -13,6 +13,7 @@ const { associateActors, associateDirectors, scrapeActors, toBaseActors } = requ
const { associateReleaseTags } = require('./tags');
const { curateEntity } = require('./entities');
const { associateReleaseMedia } = require('./media');
const { notify } = require('./alerts');
async function curateReleaseEntry(release, batchId, existingRelease, type = 'scene') {
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) {
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`);
await notifyAlerts(releasesWithId);
await notify(releasesWithId);
return releasesWithId;
}

View File

@ -1,6 +1,6 @@
'use strict';
const { addAlert, removeAlert } = require('../alerts');
const { addAlert, removeAlert, updateNotifications, updateNotification } = require('../alerts');
async function addAlertApi(req, res) {
const alertId = await addAlert(req.body, req.session.user);
@ -14,7 +14,21 @@ async function removeAlertApi(req, res) {
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 = {
addAlert: addAlertApi,
removeAlert: removeAlertApi,
updateNotifications: updateNotificationsApi,
updateNotification: updateNotificationApi,
};

View File

@ -58,6 +58,8 @@ const {
const {
addAlert,
removeAlert,
updateNotifications,
updateNotification,
} = require('./alerts');
async function initServer() {
@ -91,6 +93,9 @@ async function initServer() {
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.patch('/api/stashes/:stashId', updateStash);
router.delete('/api/stashes/:stashId', removeStash);