This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/models/task.ts

260 lines
7.9 KiB
TypeScript

import {PRIORITIES, type Priority} from '@/constants/priorities'
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MONTH, SECONDS_A_WEEK, SECONDS_A_YEAR} from '@/constants/date'
import type {ITask} from '@/modelTypes/ITask'
import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IProject} from '@/modelTypes/IProject'
import type {ISubscription} from '@/modelTypes/ISubscription'
import type {IBucket} from '@/modelTypes/IBucket'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {IRelationKind} from '@/types/IRelationKind'
import {TASK_REPEAT_MODES, type IRepeatMode} from '@/types/IRepeatMode'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import AbstractModel from './abstractModel'
import LabelModel from './label'
import UserModel from './user'
import AttachmentModel from './attachment'
import SubscriptionModel from './subscription'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import TaskReminderModel from '@/models/taskReminder'
export const TASK_DEFAULT_COLOR = '#1973ff'
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
console.debug('This browser does not support triggered notifications')
}
export function getHexColor(hexColor: string): string {
if (hexColor === '' || hexColor === '#') {
return TASK_DEFAULT_COLOR
}
return hexColor
}
/**
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
}
return repeatAfter
}
export default class TaskModel extends AbstractModel<ITask> implements ITask {
id = 0
title = ''
description = ''
done = false
doneAt: Date | null = null
priority: Priority = PRIORITIES.UNSET
labels: ILabel[] = []
assignees: IUser[] = []
dueDate: Date | null = 0
startDate: Date | null = 0
endDate: Date | null = 0
repeatAfter: number | IRepeatAfter = 0
repeatFromCurrentDate = false
repeatMode: IRepeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
/* Make sure to not return reminderDates to the server.
The server currently supports both reminderDates (old API) and reminder (new API) and assumes the old logic
if it still receives reminderDates.
This line and reminderDates attributes will be removed after https://kolaente.dev/vikunja/api/pulls/1448 was merged.
*/
reminderDates = null
reminders: ITaskReminder[] = []
parentTaskId: ITask['id'] = 0
hexColor = ''
percentDone = 0
relatedTasks: Partial<Record<IRelationKind, ITask[]>> = {}
attachments: IAttachment[] = []
coverImageAttachmentId: IAttachment['id'] = null
identifier = ''
index = 0
isFavorite = false
subscription: ISubscription = null
coverImageAttachmentId: IAttachment['id'] = null
position = 0
kanbanPosition = 0
createdBy: IUser = UserModel
created: Date = null
updated: Date = null
projectId: IProject['id'] = 0
bucketId: IBucket['id'] = 0
constructor(data: Partial<ITask> = {}) {
super()
this.assignData(data)
this.id = Number(this.id)
this.title = this.title?.trim()
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)
})
this.dueDate = parseDateOrNull(this.dueDate)
this.startDate = parseDateOrNull(this.startDate)
this.endDate = parseDateOrNull(this.endDate)
// Parse the repeat after into something usable
this.repeatAfter = parseRepeatAfter(this.repeatAfter as number)
this.reminders = this.reminders.map(r => new TaskReminderModel(r))
// Cancel all scheduled notifications for this task to be sure to only have available notifications
this.cancelScheduledNotifications().then(() => {
// Every time we see a reminder, we schedule a notification for it
this.reminders.forEach(d => this.scheduleNotification(d.reminder))
})
if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') {
this.hexColor = '#' + this.hexColor
}
// Convert all subtasks to task models
Object.keys(this.relatedTasks).forEach(relationKind => {
this.relatedTasks[relationKind] = this.relatedTasks[relationKind].map(t => {
return new TaskModel(t)
})
})
// Make all attachments to attachment models
this.attachments = this.attachments.map(a => new AttachmentModel(a))
// Set the task identifier to empty if the project does not have one
if (this.identifier === `-${this.index}`) {
this.identifier = ''
}
if (typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
this.createdBy = new UserModel(this.createdBy)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
this.projectId = Number(this.projectId)
}
getTextIdentifier() {
if (this.identifier === '') {
return `#${this.index}`
}
return this.identifier
}
getHexColor() {
return getHexColor(this.hexColor)
}
/////////////////
// Helper functions
///////////////
async cancelScheduledNotifications() {
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
return
}
if (typeof navigator.serviceWorker === 'undefined') {
console.debug('Service Worker not available')
return
}
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) {
if (typeof navigator.serviceWorker === 'undefined') {
console.debug('Service Worker not available')
return
}
if (date < new Date()) {
console.debug('Date is in the past, not scheduling a notification. Date is ', date)
return
}
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
return
}
const {state} = await navigator.permissions.request({name: 'notifications'})
if (state !== 'granted') {
console.debug('Notification permission not granted, not showing notifications')
return
}
const registration = await navigator.serviceWorker.getRegistration()
if (typeof registration === 'undefined') {
console.error('No service worker registration available')
return
}
// Register the actual notification
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',
},
],
})
console.debug('Notification scheduled for ' + date)
} catch (e) {
throw new Error('Error scheduling notification', e)
}
}
}