aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_reminders/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_reminders/src')
-rw-r--r--apps/files_reminders/src/actions/clearReminderAction.ts54
-rw-r--r--apps/files_reminders/src/actions/reminderStatusAction.ts45
-rw-r--r--apps/files_reminders/src/actions/setReminderCustomAction.ts43
-rw-r--r--apps/files_reminders/src/actions/setReminderMenuAction.ts37
-rw-r--r--apps/files_reminders/src/actions/setReminderSuggestionActions.scss23
-rw-r--r--apps/files_reminders/src/actions/setReminderSuggestionActions.ts143
-rw-r--r--apps/files_reminders/src/components/SetCustomReminderModal.vue195
-rw-r--r--apps/files_reminders/src/init.ts19
-rw-r--r--apps/files_reminders/src/services/customPicker.ts29
-rw-r--r--apps/files_reminders/src/services/reminderService.ts38
-rw-r--r--apps/files_reminders/src/shared/logger.ts11
-rw-r--r--apps/files_reminders/src/shared/types.ts10
-rw-r--r--apps/files_reminders/src/shared/utils.ts162
13 files changed, 809 insertions, 0 deletions
diff --git a/apps/files_reminders/src/actions/clearReminderAction.ts b/apps/files_reminders/src/actions/clearReminderAction.ts
new file mode 100644
index 00000000000..148861999f4
--- /dev/null
+++ b/apps/files_reminders/src/actions/clearReminderAction.ts
@@ -0,0 +1,54 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Vue from 'vue'
+import { FileAction, type Node } from '@nextcloud/files'
+import { emit } from '@nextcloud/event-bus'
+import { translate as t } from '@nextcloud/l10n'
+
+import AlarmOffSvg from '@mdi/svg/svg/alarm-off.svg?raw'
+
+import { clearReminder } from '../services/reminderService.ts'
+import { getVerboseDateString } from '../shared/utils.ts'
+
+export const action = new FileAction({
+ id: 'clear-reminder',
+
+ displayName: () => t('files_reminders', 'Clear reminder'),
+
+ title: (nodes: Node[]) => {
+ const node = nodes.at(0)!
+ const dueDate = new Date(node.attributes['reminder-due-date'])
+ return `${t('files_reminders', 'Clear reminder')} – ${getVerboseDateString(dueDate)}`
+ },
+
+ iconSvgInline: () => AlarmOffSvg,
+
+ enabled: (nodes: Node[]) => {
+ // Only allow on a single node
+ if (nodes.length !== 1) {
+ return false
+ }
+ const node = nodes.at(0)!
+ const dueDate = node.attributes['reminder-due-date']
+ return Boolean(dueDate)
+ },
+
+ async exec(node: Node) {
+ if (node.fileid) {
+ try {
+ await clearReminder(node.fileid)
+ Vue.set(node.attributes, 'reminder-due-date', '')
+ emit('files:node:updated', node)
+ return true
+ } catch (error) {
+ return false
+ }
+ }
+ return null
+ },
+
+ order: 19,
+})
diff --git a/apps/files_reminders/src/actions/reminderStatusAction.ts b/apps/files_reminders/src/actions/reminderStatusAction.ts
new file mode 100644
index 00000000000..6a2c9943d3b
--- /dev/null
+++ b/apps/files_reminders/src/actions/reminderStatusAction.ts
@@ -0,0 +1,45 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { FileAction, type Node } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+
+import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
+
+import { pickCustomDate } from '../services/customPicker.ts'
+import { getVerboseDateString } from '../shared/utils.ts'
+
+export const action = new FileAction({
+ id: 'reminder-status',
+
+ inline: () => true,
+
+ displayName: () => '',
+
+ title: (nodes: Node[]) => {
+ const node = nodes.at(0)!
+ const dueDate = new Date(node.attributes['reminder-due-date'])
+ return `${t('files_reminders', 'Reminder set')} – ${getVerboseDateString(dueDate)}`
+ },
+
+ iconSvgInline: () => AlarmSvg,
+
+ enabled: (nodes: Node[]) => {
+ // Only allow on a single node
+ if (nodes.length !== 1) {
+ return false
+ }
+ const node = nodes.at(0)!
+ const dueDate = node.attributes['reminder-due-date']
+ return Boolean(dueDate)
+ },
+
+ async exec(node: Node) {
+ pickCustomDate(node)
+ return null
+ },
+
+ order: -15,
+})
diff --git a/apps/files_reminders/src/actions/setReminderCustomAction.ts b/apps/files_reminders/src/actions/setReminderCustomAction.ts
new file mode 100644
index 00000000000..ea21293ee52
--- /dev/null
+++ b/apps/files_reminders/src/actions/setReminderCustomAction.ts
@@ -0,0 +1,43 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { Node, View } from '@nextcloud/files'
+
+import { FileAction } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import CalendarClockSvg from '@mdi/svg/svg/calendar-clock.svg?raw'
+
+import { SET_REMINDER_MENU_ID } from './setReminderMenuAction'
+import { pickCustomDate } from '../services/customPicker'
+
+export const action = new FileAction({
+ id: 'set-reminder-custom',
+ displayName: () => t('files_reminders', 'Custom reminder'),
+ title: () => t('files_reminders', 'Reminder at custom date & time'),
+ iconSvgInline: () => CalendarClockSvg,
+
+ enabled: (nodes: Node[], view: View) => {
+ if (view.id === 'trashbin') {
+ return false
+ }
+ // Only allow on a single node
+ if (nodes.length !== 1) {
+ return false
+ }
+ const node = nodes.at(0)!
+ const dueDate = node.attributes['reminder-due-date']
+ return dueDate !== undefined
+ },
+
+ parent: SET_REMINDER_MENU_ID,
+
+ async exec(file: Node) {
+ pickCustomDate(file)
+ return null
+ },
+
+ // After presets
+ order: 22,
+})
diff --git a/apps/files_reminders/src/actions/setReminderMenuAction.ts b/apps/files_reminders/src/actions/setReminderMenuAction.ts
new file mode 100644
index 00000000000..d6ddcd90677
--- /dev/null
+++ b/apps/files_reminders/src/actions/setReminderMenuAction.ts
@@ -0,0 +1,37 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { Node, View } from '@nextcloud/files'
+
+import { FileAction } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
+
+export const SET_REMINDER_MENU_ID = 'set-reminder-menu'
+
+export const action = new FileAction({
+ id: SET_REMINDER_MENU_ID,
+ displayName: () => t('files_reminders', 'Set reminder'),
+ iconSvgInline: () => AlarmSvg,
+
+ enabled: (nodes: Node[], view: View) => {
+ if (view.id === 'trashbin') {
+ return false
+ }
+ // Only allow on a single node
+ if (nodes.length !== 1) {
+ return false
+ }
+ const node = nodes.at(0)!
+ const dueDate = node.attributes['reminder-due-date']
+ return dueDate !== undefined
+ },
+
+ async exec() {
+ return null
+ },
+
+ order: 20,
+})
diff --git a/apps/files_reminders/src/actions/setReminderSuggestionActions.scss b/apps/files_reminders/src/actions/setReminderSuggestionActions.scss
new file mode 100644
index 00000000000..1327500c964
--- /dev/null
+++ b/apps/files_reminders/src/actions/setReminderSuggestionActions.scss
@@ -0,0 +1,23 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+ // TODO: remove when/if the actions API supports a separator
+ // This the last preset action, so we need to add a separator
+.files-list__row-action-set-reminder-custom {
+ margin-top: 13px;
+ position: relative;
+
+ &::before {
+ content: "";
+ margin-block: 3px;
+ margin-inline: 15px 10px;
+ border-bottom: 1px solid var(--color-border-dark);
+ cursor: default;
+ display: flex;
+ height: 0;
+ position: absolute;
+ inset-inline: 0;
+ top: -10px;
+ }
+}
diff --git a/apps/files_reminders/src/actions/setReminderSuggestionActions.ts b/apps/files_reminders/src/actions/setReminderSuggestionActions.ts
new file mode 100644
index 00000000000..f92b2f89108
--- /dev/null
+++ b/apps/files_reminders/src/actions/setReminderSuggestionActions.ts
@@ -0,0 +1,143 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Vue from 'vue'
+import type { Node, View } from '@nextcloud/files'
+
+import { FileAction } from '@nextcloud/files'
+import { emit } from '@nextcloud/event-bus'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+
+import { DateTimePreset, getDateString, getDateTime, getVerboseDateString } from '../shared/utils'
+import { logger } from '../shared/logger'
+import { SET_REMINDER_MENU_ID } from './setReminderMenuAction'
+import { setReminder } from '../services/reminderService'
+import './setReminderSuggestionActions.scss'
+
+interface ReminderOption {
+ dateTimePreset: DateTimePreset
+ label: string
+ ariaLabel: string
+ dateString?: string
+ verboseDateString?: string
+ action?: () => Promise<void>
+}
+
+const laterToday: ReminderOption = {
+ dateTimePreset: DateTimePreset.LaterToday,
+ label: t('files_reminders', 'Later today'),
+ ariaLabel: t('files_reminders', 'Set reminder for later today'),
+ dateString: '',
+ verboseDateString: '',
+}
+
+const tomorrow: ReminderOption = {
+ dateTimePreset: DateTimePreset.Tomorrow,
+ label: t('files_reminders', 'Tomorrow'),
+ ariaLabel: t('files_reminders', 'Set reminder for tomorrow'),
+ dateString: '',
+ verboseDateString: '',
+}
+
+const thisWeekend: ReminderOption = {
+ dateTimePreset: DateTimePreset.ThisWeekend,
+ label: t('files_reminders', 'This weekend'),
+ ariaLabel: t('files_reminders', 'Set reminder for this weekend'),
+ dateString: '',
+ verboseDateString: '',
+}
+
+const nextWeek: ReminderOption = {
+ dateTimePreset: DateTimePreset.NextWeek,
+ label: t('files_reminders', 'Next week'),
+ ariaLabel: t('files_reminders', 'Set reminder for next week'),
+ dateString: '',
+ verboseDateString: '',
+}
+
+/**
+ * Generate a file action for the given option
+ *
+ * @param option The option to generate the action for
+ * @return The file action or null if the option should not be shown
+ */
+const generateFileAction = (option: ReminderOption): FileAction|null => {
+
+ return new FileAction({
+ id: `set-reminder-${option.dateTimePreset}`,
+ displayName: () => `${option.label} – ${option.dateString}`,
+ title: () => `${option.ariaLabel} – ${option.verboseDateString}`,
+
+ // Empty svg to hide the icon
+ iconSvgInline: () => '<svg></svg>',
+
+ enabled: (nodes: Node[], view: View) => {
+ if (view.id === 'trashbin') {
+ return false
+ }
+ // Only allow on a single node
+ if (nodes.length !== 1) {
+ return false
+ }
+ const node = nodes.at(0)!
+ const dueDate = node.attributes['reminder-due-date']
+ return dueDate !== undefined && Boolean(getDateTime(option.dateTimePreset))
+ },
+
+ parent: SET_REMINDER_MENU_ID,
+
+ async exec(node: Node) {
+ // Can't really happen, but just in case™
+ if (!node.fileid) {
+ logger.error('Failed to set reminder, missing file id')
+ showError(t('files_reminders', 'Failed to set reminder'))
+ return null
+ }
+
+ // Set the reminder
+ try {
+ const dateTime = getDateTime(option.dateTimePreset)!
+ await setReminder(node.fileid, dateTime)
+ Vue.set(node.attributes, 'reminder-due-date', dateTime.toISOString())
+ emit('files:node:updated', node)
+ showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: node.basename }))
+ } catch (error) {
+ logger.error('Failed to set reminder', { error })
+ showError(t('files_reminders', 'Failed to set reminder'))
+ }
+ // Silent success as we display our own notification
+ return null
+ },
+
+ order: 21,
+ })
+}
+
+[laterToday, tomorrow, thisWeekend, nextWeek].forEach((option) => {
+ // Generate the initial date string
+ const dateTime = getDateTime(option.dateTimePreset)
+ if (!dateTime) {
+ return
+ }
+ option.dateString = getDateString(dateTime)
+ option.verboseDateString = getVerboseDateString(dateTime)
+
+ // Update the date string every 30 minutes
+ setInterval(() => {
+ const dateTime = getDateTime(option.dateTimePreset)
+ if (!dateTime) {
+ return
+ }
+
+ // update the submenu remind options strings
+ option.dateString = getDateString(dateTime)
+ option.verboseDateString = getVerboseDateString(dateTime)
+ }, 1000 * 30 * 60)
+})
+
+// Generate the default preset actions
+export const actions = [laterToday, tomorrow, thisWeekend, nextWeek]
+ .map(generateFileAction) as FileAction[]
diff --git a/apps/files_reminders/src/components/SetCustomReminderModal.vue b/apps/files_reminders/src/components/SetCustomReminderModal.vue
new file mode 100644
index 00000000000..59c0886a009
--- /dev/null
+++ b/apps/files_reminders/src/components/SetCustomReminderModal.vue
@@ -0,0 +1,195 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcDialog v-if="opened"
+ :name="name"
+ :out-transition="true"
+ size="small"
+ close-on-click-outside
+ @closing="onClose">
+ <form id="set-custom-reminder-form"
+ class="custom-reminder-modal"
+ @submit.prevent="setCustom">
+ <NcDateTimePickerNative id="set-custom-reminder"
+ v-model="customDueDate"
+ :label="label"
+ :min="nowDate"
+ :required="true"
+ type="datetime-local"
+ @input="onInput" />
+
+ <NcNoteCard v-if="isValid" type="info">
+ {{ t('files_reminders', 'We will remind you of this file') }}
+ <NcDateTime :timestamp="customDueDate" />
+ </NcNoteCard>
+
+ <NcNoteCard v-else type="error">
+ {{ t('files_reminders', 'Please choose a valid date & time') }}
+ </NcNoteCard>
+ </form>
+ <template #actions>
+ <!-- Cancel pick -->
+ <NcButton type="tertiary" @click="onClose">
+ {{ t('files_reminders', 'Cancel') }}
+ </NcButton>
+
+ <!-- Clear reminder -->
+ <NcButton v-if="hasDueDate" @click="clear">
+ {{ t('files_reminders', 'Clear reminder') }}
+ </NcButton>
+
+ <!-- Set reminder -->
+ <NcButton :disabled="!isValid"
+ type="primary"
+ form="set-custom-reminder-form"
+ native-type="submit">
+ {{ t('files_reminders', 'Set reminder') }}
+ </NcButton>
+ </template>
+ </NcDialog>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import type { Node } from '@nextcloud/files'
+import { emit } from '@nextcloud/event-bus'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDateTime from '@nextcloud/vue/components/NcDateTime'
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+
+import { getDateString, getInitialCustomDueDate } from '../shared/utils.ts'
+import { logger } from '../shared/logger.ts'
+import { clearReminder, setReminder } from '../services/reminderService.ts'
+
+export default Vue.extend({
+ name: 'SetCustomReminderModal',
+
+ components: {
+ NcButton,
+ NcDateTime,
+ NcDateTimePickerNative,
+ NcDialog,
+ NcNoteCard,
+ },
+
+ data() {
+ return {
+ node: undefined as Node | undefined,
+ hasDueDate: false,
+ opened: false,
+ isValid: true,
+
+ customDueDate: null as null | Date,
+ nowDate: new Date(),
+ }
+ },
+
+ computed: {
+ fileId(): number|undefined {
+ return this.node?.fileid
+ },
+
+ fileName(): string|undefined {
+ return this.node?.basename
+ },
+
+ name() {
+ return this.fileName ? t('files_reminders', 'Set reminder for "{fileName}"', { fileName: this.fileName }) : ''
+ },
+
+ label(): string {
+ return t('files_reminders', 'Reminder at custom date & time')
+ },
+
+ clearAriaLabel(): string {
+ return t('files_reminders', 'Clear reminder')
+ },
+ },
+
+ methods: {
+ t,
+ getDateString,
+
+ /**
+ * Open the modal to set a custom reminder
+ * and reset the state.
+ * @param node The node to set a reminder for
+ */
+ open(node: Node): void {
+ const dueDate = node.attributes['reminder-due-date'] ? new Date(node.attributes['reminder-due-date']) : null
+
+ this.node = node
+ this.hasDueDate = Boolean(dueDate)
+ this.isValid = true
+ this.opened = true
+ this.customDueDate = dueDate ?? getInitialCustomDueDate()
+ this.nowDate = new Date()
+
+ // Focus the input and show the picker after the animation
+ setTimeout(() => {
+ const input = document.getElementById('set-custom-reminder') as HTMLInputElement
+ input.focus()
+ if (!this.hasDueDate) {
+ input.showPicker()
+ }
+ }, 300)
+ },
+
+ async setCustom(): Promise<void> {
+ // Handle input cleared or invalid date
+ if (!(this.customDueDate instanceof Date) || isNaN(this.customDueDate)) {
+ showError(t('files_reminders', 'Please choose a valid date & time'))
+ return
+ }
+
+ try {
+ await setReminder(this.fileId, this.customDueDate)
+ Vue.set(this.node.attributes, 'reminder-due-date', this.customDueDate.toISOString())
+ emit('files:node:updated', this.node)
+ showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: this.fileName }))
+ this.onClose()
+ } catch (error) {
+ logger.error('Failed to set reminder', { error })
+ showError(t('files_reminders', 'Failed to set reminder'))
+ }
+ },
+
+ async clear(): Promise<void> {
+ try {
+ await clearReminder(this.fileId)
+ Vue.set(this.node.attributes, 'reminder-due-date', '')
+ emit('files:node:updated', this.node)
+ showSuccess(t('files_reminders', 'Reminder cleared for "{fileName}"', { fileName: this.fileName }))
+ this.onClose()
+ } catch (error) {
+ logger.error('Failed to clear reminder', { error })
+ showError(t('files_reminders', 'Failed to clear reminder'))
+ }
+ },
+
+ onClose(): void {
+ this.opened = false
+ this.$emit('close')
+ },
+
+ onInput(): void {
+ const input = document.getElementById('set-custom-reminder') as HTMLInputElement
+ this.isValid = input.checkValidity()
+ },
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+.custom-reminder-modal {
+ margin: 0 12px;
+}
+</style>
diff --git a/apps/files_reminders/src/init.ts b/apps/files_reminders/src/init.ts
new file mode 100644
index 00000000000..17da254d0f2
--- /dev/null
+++ b/apps/files_reminders/src/init.ts
@@ -0,0 +1,19 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { registerDavProperty, registerFileAction } from '@nextcloud/files'
+import { action as statusAction } from './actions/reminderStatusAction'
+import { action as clearAction } from './actions/clearReminderAction'
+import { action as menuAction } from './actions/setReminderMenuAction'
+import { actions as suggestionActions } from './actions/setReminderSuggestionActions'
+import { action as customAction } from './actions/setReminderCustomAction'
+
+registerDavProperty('nc:reminder-due-date', { nc: 'http://nextcloud.org/ns' })
+
+registerFileAction(statusAction)
+registerFileAction(clearAction)
+registerFileAction(menuAction)
+registerFileAction(customAction)
+suggestionActions.forEach((action) => registerFileAction(action))
diff --git a/apps/files_reminders/src/services/customPicker.ts b/apps/files_reminders/src/services/customPicker.ts
new file mode 100644
index 00000000000..5cefffe39a5
--- /dev/null
+++ b/apps/files_reminders/src/services/customPicker.ts
@@ -0,0 +1,29 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { Node } from '@nextcloud/files'
+import Vue from 'vue'
+
+import SetCustomReminderModal from '../components/SetCustomReminderModal.vue'
+
+const View = Vue.extend(SetCustomReminderModal)
+const mount = document.createElement('div')
+mount.id = 'set-custom-reminder-modal'
+document.body.appendChild(mount)
+
+// Create a new Vue instance and mount it to our modal container
+const CustomReminderModal = new View({
+ name: 'SetCustomReminderModal',
+ el: mount,
+})
+
+export const pickCustomDate = (node: Node): Promise<void> => {
+ CustomReminderModal.open(node)
+
+ // Wait for the modal to close
+ return new Promise((resolve) => {
+ CustomReminderModal.$once('close', resolve)
+ })
+}
diff --git a/apps/files_reminders/src/services/reminderService.ts b/apps/files_reminders/src/services/reminderService.ts
new file mode 100644
index 00000000000..9f58d1bdae3
--- /dev/null
+++ b/apps/files_reminders/src/services/reminderService.ts
@@ -0,0 +1,38 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import axios from '@nextcloud/axios'
+import { generateOcsUrl } from '@nextcloud/router'
+
+interface Reminder {
+ dueDate: null | Date
+}
+
+export const getReminder = async (fileId: number): Promise<Reminder> => {
+ const url = generateOcsUrl('/apps/files_reminders/api/v1/{fileId}', { fileId })
+ const response = await axios.get(url)
+ const dueDate = response.data.ocs.data.dueDate ? new Date(response.data.ocs.data.dueDate) : null
+
+ return {
+ dueDate,
+ }
+}
+
+export const setReminder = async (fileId: number, dueDate: Date): Promise<[]> => {
+ const url = generateOcsUrl('/apps/files_reminders/api/v1/{fileId}', { fileId })
+
+ const response = await axios.put(url, {
+ dueDate: dueDate.toISOString(), // timezone of string is always UTC
+ })
+
+ return response.data.ocs.data
+}
+
+export const clearReminder = async (fileId: number): Promise<[]> => {
+ const url = generateOcsUrl('/apps/files_reminders/api/v1/{fileId}', { fileId })
+ const response = await axios.delete(url)
+
+ return response.data.ocs.data
+}
diff --git a/apps/files_reminders/src/shared/logger.ts b/apps/files_reminders/src/shared/logger.ts
new file mode 100644
index 00000000000..79d663cca16
--- /dev/null
+++ b/apps/files_reminders/src/shared/logger.ts
@@ -0,0 +1,11 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+
+export const logger = getLoggerBuilder()
+ .setApp('files_reminders')
+ .detectUser()
+ .build()
diff --git a/apps/files_reminders/src/shared/types.ts b/apps/files_reminders/src/shared/types.ts
new file mode 100644
index 00000000000..f8da6f6aed0
--- /dev/null
+++ b/apps/files_reminders/src/shared/types.ts
@@ -0,0 +1,10 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export interface FileAttributes {
+ [key: string]: unknown
+ id: number
+ name: string
+}
diff --git a/apps/files_reminders/src/shared/utils.ts b/apps/files_reminders/src/shared/utils.ts
new file mode 100644
index 00000000000..5d583ad3ddd
--- /dev/null
+++ b/apps/files_reminders/src/shared/utils.ts
@@ -0,0 +1,162 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCanonicalLocale } from '@nextcloud/l10n'
+
+export enum DateTimePreset {
+ LaterToday = 'later-today',
+ Tomorrow = 'tomorrow',
+ ThisWeekend = 'this-weekend',
+ NextWeek = 'next-week',
+}
+
+const getFirstWorkdayOfWeek = () => {
+ const now = new Date()
+ now.setHours(0, 0, 0, 0)
+ now.setDate(now.getDate() - now.getDay() + 1)
+ return new Date(now)
+}
+
+const getWeek = (date: Date) => {
+ const dateClone = new Date(date)
+ dateClone.setHours(0, 0, 0, 0)
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0)
+ const daysFromFirstDay = (date.getTime() - firstDayOfYear.getTime()) / 86400000
+ return Math.ceil((daysFromFirstDay + firstDayOfYear.getDay() + 1) / 7)
+}
+
+const isSameWeek = (a: Date, b: Date) => {
+ return getWeek(a) === getWeek(b)
+ && a.getFullYear() === b.getFullYear()
+}
+
+const isSameDate = (a: Date, b: Date) => {
+ return a.getDate() === b.getDate()
+ && a.getMonth() === b.getMonth()
+ && a.getFullYear() === b.getFullYear()
+}
+
+export const getDateTime = (dateTime: DateTimePreset): null | Date => {
+ const matchPreset: Record<DateTimePreset, () => null | Date> = {
+ [DateTimePreset.LaterToday]: () => {
+ const now = new Date()
+ const evening = new Date()
+ evening.setHours(18, 0, 0, 0)
+ const cutoff = new Date()
+ cutoff.setHours(17, 0, 0, 0)
+ if (now >= cutoff) {
+ return null
+ }
+ return evening
+ },
+
+ [DateTimePreset.Tomorrow]: () => {
+ const now = new Date()
+ const day = new Date()
+ day.setDate(now.getDate() + 1)
+ day.setHours(8, 0, 0, 0)
+ return day
+ },
+
+ [DateTimePreset.ThisWeekend]: () => {
+ const today = new Date()
+ if (
+ [
+ 5, // Friday
+ 6, // Saturday
+ 0, // Sunday
+ ].includes(today.getDay())
+ ) {
+ return null
+ }
+ const saturday = new Date()
+ const firstWorkdayOfWeek = getFirstWorkdayOfWeek()
+ saturday.setDate(firstWorkdayOfWeek.getDate() + 5)
+ saturday.setHours(8, 0, 0, 0)
+ return saturday
+ },
+
+ [DateTimePreset.NextWeek]: () => {
+ const today = new Date()
+ if (today.getDay() === 0) { // Sunday
+ return null
+ }
+ const workday = new Date()
+ const firstWorkdayOfWeek = getFirstWorkdayOfWeek()
+ workday.setDate(firstWorkdayOfWeek.getDate() + 7)
+ workday.setHours(8, 0, 0, 0)
+ return workday
+ },
+ }
+
+ return matchPreset[dateTime]()
+}
+
+export const getInitialCustomDueDate = (): Date => {
+ const now = new Date()
+ const dueDate = new Date()
+ dueDate.setHours(now.getHours() + 2, 0, 0, 0)
+ return dueDate
+}
+
+export const getDateString = (dueDate: Date): string => {
+ let formatOptions: Intl.DateTimeFormatOptions = {
+ hour: 'numeric',
+ minute: '2-digit',
+ }
+
+ const today = new Date()
+
+ if (!isSameDate(dueDate, today)) {
+ formatOptions = {
+ ...formatOptions,
+ weekday: 'short',
+ }
+ }
+
+ if (!isSameWeek(dueDate, today)) {
+ formatOptions = {
+ ...formatOptions,
+ month: 'short',
+ day: 'numeric',
+ }
+ }
+
+ if (dueDate.getFullYear() !== today.getFullYear()) {
+ formatOptions = {
+ ...formatOptions,
+ year: 'numeric',
+ }
+ }
+
+ return dueDate.toLocaleString(
+ getCanonicalLocale(),
+ formatOptions,
+ )
+}
+
+export const getVerboseDateString = (dueDate: Date): string => {
+ let formatOptions: Intl.DateTimeFormatOptions = {
+ month: 'long',
+ day: 'numeric',
+ weekday: 'long',
+ hour: 'numeric',
+ minute: '2-digit',
+ }
+
+ const today = new Date()
+
+ if (dueDate.getFullYear() !== today.getFullYear()) {
+ formatOptions = {
+ ...formatOptions,
+ year: 'numeric',
+ }
+ }
+
+ return dueDate.toLocaleString(
+ getCanonicalLocale(),
+ formatOptions,
+ )
+}