Improved alert notifications.

This commit is contained in:
DebaucheryLibrarian 2021-04-22 19:44:23 +02:00
parent 95f3b1c03a
commit c5e74c33b7
9 changed files with 266 additions and 24 deletions

View File

@ -85,17 +85,18 @@
<Tooltip v-if="me"> <Tooltip v-if="me">
<div <div
class="header-button header-notifications" class="header-button header-notifications"
@click="showAddAlert = true" :class="{ unseen: notifications.length > 0 }"
> >
<Icon <Icon icon="bell2" />
icon="bell2"
/> <span
v-if="notifications.length > 0"
class="notifications-count"
>{{ notifications.length }}</span>
</div> </div>
<template v-slot:tooltip> <template v-slot:tooltip>
<div <Notifications />
class="notifications"
>No notifications</div>
</template> </template>
</Tooltip> </Tooltip>
@ -138,19 +139,14 @@
/> />
</template> </template>
</Tooltip> </Tooltip>
<AddAlert
v-if="showAddAlert"
@close="showAddAlert = false"
>Alert</AddAlert>
</div> </div>
</header> </header>
</template> </template>
<script> <script>
import Menu from './menu.vue'; import Menu from './menu.vue';
import Notifications from './notifications.vue';
import Search from './search.vue'; import Search from './search.vue';
import AddAlert from '../alerts/add.vue';
import logo from '../../img/logo.svg'; import logo from '../../img/logo.svg';
@ -158,11 +154,14 @@ function me() {
return this.$store.state.auth.user; return this.$store.state.auth.user;
} }
function notifications() {
return this.$store.state.ui.notifications;
}
export default { export default {
AddAlert,
components: { components: {
AddAlert,
Menu, Menu,
Notifications,
Search, Search,
}, },
emits: ['toggleSidebar', 'showFilters'], emits: ['toggleSidebar', 'showFilters'],
@ -171,11 +170,11 @@ export default {
logo, logo,
searching: false, searching: false,
showFilters: false, showFilters: false,
showAddAlert: false,
}; };
}, },
computed: { computed: {
me, me,
notifications,
}, },
}; };
</script> </script>
@ -315,7 +314,26 @@ export default {
} }
.header-notifications { .header-notifications {
position: relative;
padding: 1rem .75rem 1rem 1rem; padding: 1rem .75rem 1rem 1rem;
&.unseen .icon {
fill: var(--primary);
}
}
.notifications-count {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
bottom: .05rem;
left: .1rem;
color: var(--text-light);
font-size: .6rem;
font-weight: bold;
} }
.account { .account {
@ -360,10 +378,6 @@ export default {
} }
} }
.notifications {
padding: 1rem;
}
@media(max-width: $breakpoint-kilo) { @media(max-width: $breakpoint-kilo) {
.search-full { .search-full {
display: none; display: none;

View File

@ -0,0 +1,157 @@
<template>
<div
class="notifications"
>
<div class="notifications-header">
<h4 class="notifications-title">Notifications</h4>
<div class="notifications-actions">
<Icon
icon="checkmark"
@click="clearNotifications"
/>
<Icon
icon="plus3"
@click="showAddAlert = true"
/>
</div>
</div>
<AddAlert
v-if="showAddAlert"
@close="showAddAlert = false"
>Alert</AddAlert>
<div class="notifications-body">
<span
v-if="notifications.length === 0"
class="notification"
>No notifications</span>
<ul
v-else
class="nolist"
>
<li
v-for="notification in notifications"
:key="`notification-${notification.id}`"
class="notification"
>
<router-link
:to="`/scene/${notification.scene.id}/${notification.scene.slug}`"
class="notification-link"
>
<img
:src="getPath(notification.scene.poster, 'thumbnail')"
class="poster"
>
<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>
</router-link>
</li>
</ul>
</div>
</div>
</template>
<script>
import AddAlert from '../alerts/add.vue';
function notifications() {
return this.$store.state.ui.notifications;
}
async function clearNotifications() {
}
export default {
components: {
AddAlert,
},
data() {
return {
showAddAlert: false,
};
},
computed: {
notifications,
},
methods: {
clearNotifications,
},
};
</script>
<style lang="scss" scoped>
.notifications {
width: 30rem;
}
.notifications-header {
display: flex;
justify-content: space-between;
.icon {
padding: .5rem;
fill: var(--shadow);
:hover {
fill: var(--primary);
cursor: pointer;
}
}
}
.notifications-title {
display: inline-block;
padding: .5rem 1rem;
margin: 0;
color: var(--shadow);
font-size: 1rem;
font-weight: bold;
}
.notifications-body {
box-shadow: 0 0 3px var(--shadow-weak);
}
.notification {
display: block;
&:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint);
}
&:hover {
color: var(--primary);
}
}
.notification-link {
display: flex;
color: inherit;
text-decoration: none;
}
.notification-body {
padding: .5rem 1rem;
}
.notification-tidbit {
font-weight: bold;
}
.poster {
width: 5rem;
height: 3rem;
object-fit: cover;
object-position: center;
}
</style>

View File

@ -196,10 +196,20 @@ function curateUser(user) {
return curatedUser; return curatedUser;
} }
function curateNotification(notification) {
const curatedNotification = notification;
curatedNotification.scene = curateRelease(notification.scene);
curatedNotification.alert = curateAlert(notification.alert.alert || notification.alert);
return curatedNotification;
}
export { export {
curateActor, curateActor,
curateEntity, curateEntity,
curateRelease, curateRelease,
curateNotification,
curateTag, curateTag,
curateStash, curateStash,
curateUser, curateUser,

View File

@ -1,6 +1,6 @@
import { graphql } from '../api'; import { graphql } from '../api';
import { releaseFields, actorStashesFields } from '../fragments'; import { releaseFields, actorStashesFields } from '../fragments';
import { curateRelease, curateActor } from '../curate'; import { curateRelease, curateActor, curateNotification } from '../curate';
function initUiActions(store, _router) { function initUiActions(store, _router) {
function setTagFilter({ commit }, filter) { function setTagFilter({ commit }, filter) {
@ -29,6 +29,53 @@ function initUiActions(store, _router) {
localStorage.setItem('sfw', sfw); localStorage.setItem('sfw', sfw);
} }
async function fetchNotifications({ commit }) {
if (!store.state.auth.user) {
return [];
}
const { notifications } = await graphql(`
query Notifications(
$hasAuth: Boolean!
$userId: Int
) {
notifications {
id
sceneId
userId
createdAt
scene {
${releaseFields}
}
alert {
tags: alertsTags {
tag {
id
name
slug
}
}
actors: alertsActors {
actor {
id
name
slug
}
}
}
}
}
`, {
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
});
const curatedNotifications = notifications.map(notification => curateNotification(notification));
commit('setNotifications', curatedNotifications);
return curatedNotifications;
}
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(
@ -176,6 +223,7 @@ function initUiActions(store, _router) {
setBatch, setBatch,
setSfw, setSfw,
setTheme, setTheme,
fetchNotifications,
fetchStats, fetchStats,
}; };
} }

View File

@ -1,3 +1,7 @@
function setNotifications(state, notifications) {
state.notifications = notifications;
}
function setTagFilter(state, tagFilter) { function setTagFilter(state, tagFilter) {
state.tagFilter = tagFilter; state.tagFilter = tagFilter;
} }
@ -19,6 +23,7 @@ function setTheme(state, theme) {
} }
export default { export default {
setNotifications,
setTagFilter, setTagFilter,
setRange, setRange,
setBatch, setBatch,

View File

@ -1,4 +1,4 @@
function initUiObservers(store, _router) { async function initUiObservers(store, _router) {
const body = document.querySelector('body'); const body = document.querySelector('body');
body.classList.add(store.state.ui.theme); body.classList.add(store.state.ui.theme);
@ -29,6 +29,8 @@ function initUiObservers(store, _router) {
store.dispatch('setTheme', 'light'); store.dispatch('setTheme', 'light');
} }
}); });
await store.dispatch('fetchNotifications');
} }
export default initUiObservers; export default initUiObservers;

View File

@ -11,4 +11,5 @@ export default {
batch: storedBatch || 'all', batch: storedBatch || 'all',
sfw: storedSfw === 'true' || false, sfw: storedSfw === 'true' || false,
theme: storedTheme || deviceTheme, theme: storedTheme || deviceTheme,
notifications: [],
}; };

View File

@ -1567,6 +1567,13 @@ exports.up = knex => Promise.resolve()
WHERE alerts.id = alerts_stashes.alert_id WHERE alerts.id = alerts_stashes.alert_id
AND alerts.user_id = current_user_id() AND alerts.user_id = current_user_id()
)); ));
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
CREATE POLICY notifications_policy_select ON notifications FOR SELECT USING (notifications.user_id = current_user_id());
CREATE POLICY notifications_policy_update ON notifications FOR UPDATE USING (notifications.user_id = current_user_id());
CREATE POLICY notifications_policy_delete ON notifications FOR DELETE USING (notifications.user_id = current_user_id());
CREATE POLICY notifications_policy_insert ON notifications FOR INSERT WITH CHECK (true);
`, { `, {
visitor: knex.raw(config.database.query.user), visitor: knex.raw(config.database.query.user),
}); });

View File

@ -264,8 +264,6 @@ async function notifyAlerts(scenes) {
scene_id: notification.scene_id, scene_id: notification.scene_id,
}))); })));
console.log(releases.rows);
return releases.rows; return releases.rows;
} }