Subscriptions and notifications for namespaces, tasks and lists (#410)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/410
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-02-14 19:18:51 +00:00
parent b384c8ecde
commit 3f20ae89a8
10 changed files with 223 additions and 1 deletions

View file

@ -54,6 +54,14 @@
>
Archive
</dropdown-item>
<task-subscription
class="dropdown-item has-no-shadow"
:is-button="false"
entity="list"
:entity-id="list.id"
:subscription="subscription"
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
icon="trash-alt"
@ -69,10 +77,17 @@
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
import TaskSubscription from '@/components/misc/subscription'
export default {
name: 'list-settings-dropdown',
data() {
return {
subscription: null,
}
},
components: {
TaskSubscription,
DropdownItem,
Dropdown,
},
@ -81,6 +96,9 @@ export default {
required: true,
},
},
mounted() {
this.subscription = this.list.subscription
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders.length > 0

View file

@ -0,0 +1,121 @@
<template>
<x-button
type="secondary"
:icon="icon"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled"
v-if="isButton"
>
{{ buttonText }}
</x-button>
<a
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
v-else
>
<span class="icon">
<icon :icon="icon"/>
</span>
{{ buttonText }}
</a>
</template>
<script>
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
export default {
name: 'task-subscription',
data() {
return {
subscriptionService: SubscriptionService,
}
},
props: {
entity: {
required: true,
type: String,
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
},
created() {
this.subscriptionService = new SubscriptionService()
},
computed: {
tooltipText() {
if(this.disabled) {
return `You can't unsubscribe here because you are subscribed to this ${this.entity} through its ${this.subscription.entity}.`
}
return this.subscription !== null ?
`You are currently subscribed to this ${this.entity} and will receive notifications for changes.` :
`You are not subscribed to this ${this.entity} and won't receive notifications for changes.`
},
buttonText() {
return this.subscription !== null ? 'Unsubscribe' : 'Subscribe'
},
icon() {
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
},
disabled() {
if (this.subscription === null) {
return false
}
return this.subscription.entity !== this.entity
},
},
methods: {
changeSubscription() {
if(this.disabled) {
return
}
if (this.subscription === null) {
this.subscribe()
} else {
this.unsubscribe()
}
},
subscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
this.subscriptionService.create(subscription)
.then(() => {
this.$emit('change', subscription)
this.success({message: `You are now subscribed to this ${this.entity}`}, this)
})
.catch(e => {
this.error(e, this)
})
},
unsubscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
this.subscriptionService.delete(subscription)
.then(() => {
this.$emit('change', null)
this.success({message: `You are now unsubscribed to this ${this.entity}`}, this)
})
.catch(e => {
this.error(e, this)
})
}
},
}
</script>

View file

@ -33,6 +33,14 @@
>
Archive
</dropdown-item>
<task-subscription
class="dropdown-item has-no-shadow"
:is-button="false"
entity="namespace"
:entity-id="namespace.id"
:subscription="subscription"
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
icon="trash-alt"
@ -47,17 +55,27 @@
<script>
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
import TaskSubscription from '@/components/misc/subscription'
export default {
name: 'namespace-settings-dropdown',
data() {
return {
subscription: null,
}
},
components: {
DropdownItem,
Dropdown,
TaskSubscription,
},
props: {
namespace: {
required: true,
},
},
mounted() {
this.subscription = this.namespace.subscription
},
}
</script>

View file

@ -61,8 +61,9 @@ import {
faArchive,
faShareAlt,
faImage,
faBell,
} from '@fortawesome/free-solid-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun, faBellSlash} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
// PWA
import './registerServiceWorker'
@ -152,6 +153,8 @@ library.add(faEllipsisH)
library.add(faArchive)
library.add(faShareAlt)
library.add(faImage)
library.add(faBell)
library.add(faBellSlash)
Vue.component('icon', FontAwesomeIcon)

View file

@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
import TaskModel from './task'
import UserModel from './user'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import SubscriptionModel from '@/models/subscription'
export default class ListModel extends AbstractModel {
@ -19,6 +20,10 @@ export default class ListModel extends AbstractModel {
this.owner = new UserModel(this.owner)
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
@ -37,6 +42,7 @@ export default class ListModel extends AbstractModel {
identifier: '',
backgroundInformation: null,
isFavorite: false,
subscription: null,
created: null,
updated: null,

View file

@ -1,6 +1,7 @@
import AbstractModel from './abstractModel'
import ListModel from './list'
import UserModel from './user'
import SubscriptionModel from '@/models/subscription'
export default class NamespaceModel extends AbstractModel {
constructor(data) {
@ -13,8 +14,13 @@ export default class NamespaceModel extends AbstractModel {
this.lists = this.lists.map(l => {
return new ListModel(l)
})
this.owner = new UserModel(this.owner)
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
@ -29,6 +35,7 @@ export default class NamespaceModel extends AbstractModel {
lists: [],
isArchived: false,
hexColor: '',
subscription: null,
created: null,
updated: null,

View file

@ -0,0 +1,20 @@
import AbstractModel from '@/models/abstractModel'
import UserModel from '@/models/user'
export default class SubscriptionModel extends AbstractModel {
constructor(data) {
super(data)
this.user = new UserModel(this.user)
this.created = new Date(this.created)
}
defaults() {
return {
id: 0,
entity: '',
entityId: 0,
created: null,
user: {},
}
}
}

View file

@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
import UserModel from './user'
import LabelModel from './label'
import AttachmentModel from './attachment'
import SubscriptionModel from '@/models/subscription'
const parseDate = date => {
if (date && !date.startsWith('0001')) {
@ -75,6 +76,10 @@ export default class TaskModel extends AbstractModel {
this.identifier = ''
}
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
@ -104,6 +109,7 @@ export default class TaskModel extends AbstractModel {
identifier: '',
index: 0,
isFavorite: false,
subscription: null,
createdBy: UserModel,
created: null,

View file

@ -0,0 +1,15 @@
import AbstractService from '@/services/abstractService'
import SubscriptionModel from '@/models/subscription'
export default class SubscriptionService extends AbstractService {
constructor() {
super({
create: '/subscriptions/{entity}/{entityId}',
delete: '/subscriptions/{entity}/{entityId}',
})
}
modelFactory(data) {
return new SubscriptionModel(data)
}
}

View file

@ -255,6 +255,12 @@
>
{{ task.done ? 'Mark as undone' : 'Done!' }}
</x-button>
<task-subscription
entity="task"
:entity-id="task.id"
:subscription="task.subscription"
@change="sub => task.subscription = sub"
/>
<x-button
@click="setFieldActive('assignees')"
@shortkey="setFieldActive('assignees')"
@ -422,10 +428,12 @@ import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
import heading from '@/components/tasks/partials/heading'
import Datepicker from '@/components/input/datepicker'
import {playPop} from '@/helpers/playPop'
import TaskSubscription from '@/components/misc/subscription'
export default {
name: 'TaskDetailView',
components: {
TaskSubscription,
Datepicker,
ColorPicker,
ListSearch,