2022-07-21 00:42:36 +02:00
|
|
|
import type { Priority } from '@/models/constants/priorities'
|
|
|
|
|
|
|
|
import AbstractModel from '@/models/abstractModel'
|
|
|
|
import UserModel, { type IUser } from '@/models/user'
|
|
|
|
import LabelModel, { type ILabel } from '@/models/label'
|
|
|
|
import AttachmentModel, {type IAttachment} from '@/models/attachment'
|
|
|
|
import SubscriptionModel, { type ISubscription } from '@/models/subscription'
|
|
|
|
import type { IList } from '@/models/list'
|
2021-04-14 10:24:07 +02:00
|
|
|
|
2021-02-21 16:13:58 +01:00
|
|
|
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
2022-07-21 00:42:36 +02:00
|
|
|
import type { IBucket } from './bucket'
|
2020-12-08 15:43:51 +01:00
|
|
|
|
2021-11-22 20:03:27 +01:00
|
|
|
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
|
2022-06-16 18:11:42 +02:00
|
|
|
export const TASK_DEFAULT_COLOR = '#1973ff'
|
2021-11-22 20:03:27 +01:00
|
|
|
|
2022-06-23 03:22:21 +02:00
|
|
|
export const TASK_REPEAT_MODES = {
|
|
|
|
'REPEAT_MODE_DEFAULT': 0,
|
|
|
|
'REPEAT_MODE_MONTH': 1,
|
|
|
|
'REPEAT_MODE_FROM_CURRENT_DATE': 2,
|
|
|
|
} as const
|
|
|
|
|
|
|
|
export type TaskRepeatMode = typeof TASK_REPEAT_MODES[keyof typeof TASK_REPEAT_MODES]
|
|
|
|
|
|
|
|
export interface RepeatAfter {
|
|
|
|
type: 'hours' | 'weeks' | 'months' | 'years' | 'days'
|
|
|
|
amount: number
|
|
|
|
}
|
|
|
|
|
2022-07-21 18:35:37 +02:00
|
|
|
export interface ITask extends AbstractModel {
|
2022-06-23 03:22:21 +02:00
|
|
|
id: number
|
|
|
|
title: string
|
|
|
|
description: string
|
|
|
|
done: boolean
|
|
|
|
doneAt: Date | null
|
2022-07-20 21:15:35 +02:00
|
|
|
priority: Priority
|
2022-07-21 00:42:36 +02:00
|
|
|
labels: ILabel[]
|
|
|
|
assignees: IUser[]
|
2022-06-23 03:22:21 +02:00
|
|
|
|
|
|
|
dueDate: Date | null
|
|
|
|
startDate: Date | null
|
|
|
|
endDate: Date | null
|
|
|
|
repeatAfter: number | RepeatAfter
|
|
|
|
repeatFromCurrentDate: boolean
|
|
|
|
repeatMode: TaskRepeatMode
|
|
|
|
reminderDates: Date[]
|
2022-07-21 00:42:36 +02:00
|
|
|
parentTaskId: ITask['id']
|
2022-06-23 03:22:21 +02:00
|
|
|
hexColor: string
|
|
|
|
percentDone: number
|
2022-07-21 00:42:36 +02:00
|
|
|
relatedTasks: { [relationKind: string]: ITask } // FIXME: use relationKinds
|
|
|
|
attachments: IAttachment[]
|
2022-06-23 03:22:21 +02:00
|
|
|
identifier: string
|
|
|
|
index: number
|
|
|
|
isFavorite: boolean
|
2022-07-21 00:42:36 +02:00
|
|
|
subscription: ISubscription
|
2022-06-23 03:22:21 +02:00
|
|
|
|
|
|
|
position: number
|
|
|
|
kanbanPosition: number
|
|
|
|
|
2022-07-21 00:42:36 +02:00
|
|
|
createdBy: IUser
|
|
|
|
created: Date
|
|
|
|
updated: Date
|
|
|
|
|
|
|
|
listId: IList['id'] // Meta, only used when creating a new task
|
|
|
|
bucketId: IBucket['id']
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class TaskModel extends AbstractModel implements ITask {
|
|
|
|
id: number
|
|
|
|
title: string
|
|
|
|
declare description: string
|
|
|
|
declare done: boolean
|
|
|
|
doneAt: Date | null
|
|
|
|
declare priority: Priority
|
|
|
|
labels: ILabel[]
|
|
|
|
assignees: IUser[]
|
|
|
|
|
|
|
|
dueDate: Date | null
|
|
|
|
startDate: Date | null
|
|
|
|
endDate: Date | null
|
|
|
|
declare repeatAfter: number | RepeatAfter
|
|
|
|
declare repeatFromCurrentDate: boolean
|
|
|
|
declare repeatMode: TaskRepeatMode
|
|
|
|
reminderDates: Date[]
|
|
|
|
declare parentTaskId: ITask['id']
|
|
|
|
declare hexColor: string
|
|
|
|
declare percentDone: number
|
|
|
|
declare relatedTasks: { [relationKind: string]: ITask } // FIXME: use relationKinds
|
|
|
|
attachments: IAttachment[]
|
|
|
|
declare identifier: string
|
|
|
|
declare index: number
|
|
|
|
declare isFavorite: boolean
|
|
|
|
declare subscription: ISubscription
|
|
|
|
|
|
|
|
declare position: number
|
|
|
|
declare kanbanPosition: number
|
|
|
|
|
|
|
|
createdBy: IUser
|
2022-06-23 03:22:21 +02:00
|
|
|
created: Date
|
|
|
|
updated: Date
|
|
|
|
|
2022-07-21 00:42:36 +02:00
|
|
|
listId: IList['id'] // Meta, only used when creating a new task
|
2022-06-23 03:22:21 +02:00
|
|
|
|
2022-07-21 00:42:36 +02:00
|
|
|
constructor(data: Partial<ITask>) {
|
2019-03-02 11:25:10 +01:00
|
|
|
super(data)
|
2019-11-24 14:16:24 +01:00
|
|
|
|
|
|
|
this.id = Number(this.id)
|
2021-09-10 14:57:59 +02:00
|
|
|
this.title = this.title?.trim()
|
2022-02-13 19:57:12 +01:00
|
|
|
this.doneAt = parseDateOrNull(this.doneAt)
|
|
|
|
|
|
|
|
this.labels = this.labels
|
|
|
|
.map(l => new LabelModel(l))
|
|
|
|
.sort((f, s) => f.title > s.title ? 1 : -1)
|
|
|
|
|
|
|
|
// Parse the assignees into user models
|
|
|
|
this.assignees = this.assignees.map(a => {
|
|
|
|
return new UserModel(a)
|
|
|
|
})
|
|
|
|
|
2021-02-21 16:13:58 +01:00
|
|
|
this.dueDate = parseDateOrNull(this.dueDate)
|
|
|
|
this.startDate = parseDateOrNull(this.startDate)
|
|
|
|
this.endDate = parseDateOrNull(this.endDate)
|
2019-03-02 11:25:10 +01:00
|
|
|
|
2022-02-13 19:57:12 +01:00
|
|
|
// Parse the repeat after into something usable
|
|
|
|
this.parseRepeatAfter()
|
|
|
|
|
2021-10-11 19:37:20 +02:00
|
|
|
this.reminderDates = this.reminderDates.map(d => new Date(d))
|
2022-02-13 19:57:12 +01:00
|
|
|
|
2020-02-08 18:28:17 +01:00
|
|
|
// Cancel all scheduled notifications for this task to be sure to only have available notifications
|
2021-10-11 19:37:20 +02:00
|
|
|
this.cancelScheduledNotifications().then(() => {
|
|
|
|
// Every time we see a reminder, we schedule a notification for it
|
|
|
|
this.reminderDates.forEach(d => this.scheduleNotification(d))
|
|
|
|
})
|
2019-03-02 11:25:10 +01:00
|
|
|
|
2021-04-18 19:16:53 +02:00
|
|
|
if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') {
|
2019-04-30 22:18:06 +02:00
|
|
|
this.hexColor = '#' + this.hexColor
|
|
|
|
}
|
2019-10-28 22:45:37 +01:00
|
|
|
|
|
|
|
// Make all subtasks to task models
|
2020-06-27 19:04:30 +02:00
|
|
|
Object.keys(this.relatedTasks).forEach(relationKind => {
|
2020-04-12 23:54:46 +02:00
|
|
|
this.relatedTasks[relationKind] = this.relatedTasks[relationKind].map(t => {
|
2019-10-28 22:45:37 +01:00
|
|
|
return new TaskModel(t)
|
|
|
|
})
|
|
|
|
})
|
2019-11-24 14:16:24 +01:00
|
|
|
|
|
|
|
// Make all attachments to attachment models
|
2022-02-13 19:57:12 +01:00
|
|
|
this.attachments = this.attachments.map(a => new AttachmentModel(a))
|
|
|
|
|
2020-05-16 13:14:57 +02:00
|
|
|
// Set the task identifier to empty if the list does not have one
|
2020-06-27 19:04:30 +02:00
|
|
|
if (this.identifier === `-${this.index}`) {
|
2020-05-16 13:14:57 +02:00
|
|
|
this.identifier = ''
|
|
|
|
}
|
|
|
|
|
2021-02-21 16:13:58 +01:00
|
|
|
if (typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
2021-02-14 20:18:51 +01:00
|
|
|
this.subscription = new SubscriptionModel(this.subscription)
|
|
|
|
}
|
|
|
|
|
2022-02-13 19:57:12 +01:00
|
|
|
this.createdBy = new UserModel(this.createdBy)
|
2020-02-08 14:16:06 +01:00
|
|
|
this.created = new Date(this.created)
|
|
|
|
this.updated = new Date(this.updated)
|
2022-02-13 19:57:12 +01:00
|
|
|
|
|
|
|
this.listId = Number(this.listId)
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
2022-07-21 00:42:36 +02:00
|
|
|
bucketId: number
|
2020-06-27 19:04:30 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
defaults() {
|
|
|
|
return {
|
|
|
|
id: 0,
|
2020-09-04 22:01:02 +02:00
|
|
|
title: '',
|
2019-03-02 11:25:10 +01:00
|
|
|
description: '',
|
|
|
|
done: false,
|
2020-11-28 15:52:15 +01:00
|
|
|
doneAt: null,
|
2019-03-02 11:25:10 +01:00
|
|
|
priority: 0,
|
|
|
|
labels: [],
|
|
|
|
assignees: [],
|
2020-06-27 19:04:30 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
dueDate: 0,
|
|
|
|
startDate: 0,
|
|
|
|
endDate: 0,
|
|
|
|
repeatAfter: 0,
|
2020-06-14 14:43:01 +02:00
|
|
|
repeatFromCurrentDate: false,
|
2022-06-23 03:22:21 +02:00
|
|
|
repeatMode: TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT,
|
2019-03-02 11:25:10 +01:00
|
|
|
reminderDates: [],
|
2020-04-17 12:19:53 +02:00
|
|
|
parentTaskId: 0,
|
2019-04-30 22:18:06 +02:00
|
|
|
hexColor: '',
|
2019-10-19 18:27:31 +02:00
|
|
|
percentDone: 0,
|
2020-04-12 23:54:46 +02:00
|
|
|
relatedTasks: {},
|
2019-11-24 14:16:24 +01:00
|
|
|
attachments: [],
|
2020-05-16 13:14:57 +02:00
|
|
|
identifier: '',
|
|
|
|
index: 0,
|
2020-09-05 22:16:17 +02:00
|
|
|
isFavorite: false,
|
2021-02-14 20:18:51 +01:00
|
|
|
subscription: null,
|
2021-10-31 12:56:32 +01:00
|
|
|
|
2021-07-28 21:56:29 +02:00
|
|
|
position: 0,
|
|
|
|
kanbanPosition: 0,
|
2019-04-30 22:18:06 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
createdBy: UserModel,
|
2020-02-08 14:16:06 +01:00
|
|
|
created: null,
|
|
|
|
updated: null,
|
2020-06-27 19:04:30 +02:00
|
|
|
|
2020-04-12 23:54:46 +02:00
|
|
|
listId: 0, // Meta, only used when creating a new task
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
}
|
2020-06-27 19:04:30 +02:00
|
|
|
|
2021-02-21 16:13:58 +01:00
|
|
|
getTextIdentifier() {
|
2021-04-14 10:24:07 +02:00
|
|
|
if (this.identifier === '') {
|
2021-02-21 16:13:58 +01:00
|
|
|
return `#${this.index}`
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.identifier
|
|
|
|
}
|
|
|
|
|
2021-04-18 19:16:53 +02:00
|
|
|
getHexColor() {
|
2022-06-16 18:11:42 +02:00
|
|
|
if (this.hexColor === '' || this.hexColor === '#') {
|
|
|
|
return TASK_DEFAULT_COLOR
|
2021-04-18 19:16:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return this.hexColor
|
|
|
|
}
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/////////////////
|
|
|
|
// Helper functions
|
|
|
|
///////////////
|
2020-06-27 19:04:30 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/**
|
|
|
|
* Parses the "repeat after x seconds" from the task into a usable js object inside the task.
|
|
|
|
* This function should only be called from the constructor.
|
|
|
|
*/
|
|
|
|
parseRepeatAfter() {
|
2022-06-23 03:22:21 +02:00
|
|
|
const repeatAfterHours = (this.repeatAfter as number / 60) / 60
|
2019-03-02 11:25:10 +01:00
|
|
|
this.repeatAfter = {type: 'hours', amount: repeatAfterHours}
|
|
|
|
|
|
|
|
// if its dividable by 24, its something with days, otherwise hours
|
|
|
|
if (repeatAfterHours % 24 === 0) {
|
2022-02-15 13:07:34 +01:00
|
|
|
const repeatAfterDays = repeatAfterHours / 24
|
2019-03-02 11:25:10 +01:00
|
|
|
if (repeatAfterDays % 7 === 0) {
|
|
|
|
this.repeatAfter.type = 'weeks'
|
|
|
|
this.repeatAfter.amount = repeatAfterDays / 7
|
|
|
|
} else if (repeatAfterDays % 30 === 0) {
|
|
|
|
this.repeatAfter.type = 'months'
|
|
|
|
this.repeatAfter.amount = repeatAfterDays / 30
|
|
|
|
} else if (repeatAfterDays % 365 === 0) {
|
|
|
|
this.repeatAfter.type = 'years'
|
|
|
|
this.repeatAfter.amount = repeatAfterDays / 365
|
|
|
|
} else {
|
|
|
|
this.repeatAfter.type = 'days'
|
|
|
|
this.repeatAfter.amount = repeatAfterDays
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-30 22:18:06 +02:00
|
|
|
|
2020-02-08 18:28:17 +01:00
|
|
|
async cancelScheduledNotifications() {
|
2021-11-22 20:03:27 +01:00
|
|
|
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
|
2020-02-09 13:12:54 +01:00
|
|
|
console.debug('This browser does not support triggered notifications')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-03 14:30:26 +02:00
|
|
|
if (typeof navigator.serviceWorker === 'undefined') {
|
|
|
|
console.debug('Service Worker not available')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-02-08 18:28:17 +01:00
|
|
|
const registration = await navigator.serviceWorker.getRegistration()
|
|
|
|
if (typeof registration === 'undefined') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all scheduled notifications for this task and cancel them
|
|
|
|
const scheduledNotifications = await registration.getNotifications({
|
|
|
|
tag: `vikunja-task-${this.id}`,
|
|
|
|
includeTriggered: true,
|
|
|
|
})
|
|
|
|
console.debug('Already scheduled notifications:', scheduledNotifications)
|
|
|
|
scheduledNotifications.forEach(n => n.close())
|
|
|
|
}
|
|
|
|
|
|
|
|
async scheduleNotification(date) {
|
2020-10-03 14:30:26 +02:00
|
|
|
if (typeof navigator.serviceWorker === 'undefined') {
|
|
|
|
console.debug('Service Worker not available')
|
|
|
|
return
|
|
|
|
}
|
2020-03-01 17:13:25 +01:00
|
|
|
|
2020-06-27 19:04:30 +02:00
|
|
|
if (date < new Date()) {
|
2020-03-01 17:13:25 +01:00
|
|
|
console.debug('Date is in the past, not scheduling a notification. Date is ', date)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-22 20:03:27 +01:00
|
|
|
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
|
2020-02-08 18:28:17 +01:00
|
|
|
console.debug('This browser does not support triggered notifications')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-05 22:16:17 +02:00
|
|
|
const {state} = await navigator.permissions.request({name: 'notifications'})
|
2020-02-08 18:28:17 +01:00
|
|
|
if (state !== 'granted') {
|
|
|
|
console.debug('Notification permission not granted, not showing notifications')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const registration = await navigator.serviceWorker.getRegistration()
|
|
|
|
if (typeof registration === 'undefined') {
|
2020-03-01 17:13:25 +01:00
|
|
|
console.error('No service worker registration available')
|
2020-02-08 18:28:17 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register the actual notification
|
2021-10-11 19:37:20 +02:00
|
|
|
try {
|
|
|
|
registration.showNotification('Vikunja Reminder', {
|
|
|
|
tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task
|
|
|
|
body: this.title,
|
|
|
|
// eslint-disable-next-line no-undef
|
|
|
|
showTrigger: new TimestampTrigger(date),
|
|
|
|
badge: '/images/icons/badge-monochrome.png',
|
|
|
|
icon: '/images/icons/android-chrome-512x512.png',
|
|
|
|
data: {taskId: this.id},
|
|
|
|
actions: [
|
|
|
|
{
|
|
|
|
action: 'show-task',
|
|
|
|
title: 'Show task',
|
|
|
|
},
|
|
|
|
],
|
2020-06-27 19:04:30 +02:00
|
|
|
})
|
2021-10-11 19:37:20 +02:00
|
|
|
console.debug('Notification scheduled for ' + date)
|
2021-10-31 12:56:32 +01:00
|
|
|
} catch (e) {
|
2021-10-11 19:37:20 +02:00
|
|
|
throw new Error('Error scheduling notification', e)
|
|
|
|
}
|
2020-02-08 18:28:17 +01:00
|
|
|
}
|
2020-04-04 18:26:35 +02:00
|
|
|
}
|
|
|
|
|