Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/activity.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCarl Schwan <carl@carlschwan.eu>2022-08-01 17:00:58 +0300
committerCarl Schwan <carl@carlschwan.eu>2022-08-19 21:03:30 +0300
commit757b97e5674b712f8af8050d5e3903cda45b4258 (patch)
tree367e486ddae1423ed7a0c3bf1cb4d50bcafdc763 /src
parentcc3befff4c27506ac98d53fa3defac64c6786af5 (diff)
Port settings to vue
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Diffstat (limited to 'src')
-rw-r--r--src/components/Activity.vue4
-rw-r--r--src/components/ActivityGrid.vue136
-rw-r--r--src/components/Checkbox.vue273
-rw-r--r--src/components/EmailSettings.vue87
-rw-r--r--src/helpers/settings.js52
-rw-r--r--src/models/ActivityModel.js21
-rw-r--r--src/models/ActivitySettings.js35
-rw-r--r--src/models/EmailFrequency.js30
-rw-r--r--src/settings-admin.js51
-rw-r--r--src/settings-personal.js51
-rw-r--r--src/settings-store.js322
-rw-r--r--src/tests/setup.js2
-rw-r--r--src/views/AdminSettings.vue68
-rw-r--r--src/views/DailySummary.vue38
-rw-r--r--src/views/DefaultActivitySettings.vue58
-rw-r--r--src/views/UserSettings.vue68
16 files changed, 1279 insertions, 17 deletions
diff --git a/src/components/Activity.vue b/src/components/Activity.vue
index 88c6b34c..34139887 100644
--- a/src/components/Activity.vue
+++ b/src/components/Activity.vue
@@ -122,7 +122,7 @@ export default {
/**
* Map an collection of rich text objects to rich arguments for the RichText component
*
- * @param {Array.<object.<string, RichObject>>} richObjects - The rich text object
+ * @param {Array.<object<string, RichObject>>} richObjects - The rich text object
* @return {object<string, object>}
*/
mapRichObjectsToRichArguments(richObjects) {
@@ -138,7 +138,7 @@ export default {
/**
* Map rich text object to rich argument for the RichText component
*
- * @param {object.<string, RichObject>} richObject - The rich text object
+ * @param {object<string, RichObject>} richObject - The rich text object
* @return {object}}
*/
mapRichObjectToRichArgument(richObject) {
diff --git a/src/components/ActivityGrid.vue b/src/components/ActivityGrid.vue
new file mode 100644
index 00000000..61b18bbf
--- /dev/null
+++ b/src/components/ActivityGrid.vue
@@ -0,0 +1,136 @@
+<!--
+ - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <table class="grid activitysettings">
+ <caption class="sr-only">
+ {{ t('activity', 'Activity notification configuration') }}
+ </caption>
+ <tbody v-for="(group, groupKey) in activityGroups" :key="groupKey">
+ <tr class="group-header">
+ <th>
+ <h3>{{ group.name }}</h3>
+ </th>
+ <th v-for="(methodName, methodKey) in methods"
+ :key="methodKey"
+ class="activity_select_group"
+ aria-hidden="true">
+ {{ methodName }}
+ </th>
+ </tr>
+ <tr v-for="(activity, activityKey) in group.activities" :key="activityKey">
+ <th scope="row">
+ <!-- eslint-disable vue/no-v-html -->
+ <label @click="toggleMethodsForActivity({groupKey, activityKey})" v-html="activity.desc" />
+ <!-- eslint-enable vue/no-v-html -->
+ </th>
+ <td v-for="(methodName, methodKey) in methods" :key="methodKey">
+ <Checkbox :id="`${activityKey}_${methodKey}`"
+ :disabled="!isActivityEnabled(activity, methodKey)"
+ :checked="checkedActivities"
+ :value="`${activityKey}_${methodKey}`"
+ @update:checked="toggleMethodForMethodAndActivity({groupKey, activityKey, methodKey})">
+ {{ actionName(methodKey) }}
+ </Checkbox>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</template>
+
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex'
+import Checkbox from './Checkbox'
+import { isActivityEnabled } from '../helpers/settings'
+
+export default {
+ name: 'ActivityGrid',
+ components: {
+ Checkbox,
+ },
+ computed: {
+ ...mapGetters([
+ 'checkedActivities',
+ ]),
+ ...mapState([
+ 'methods',
+ 'activityGroups',
+ 'emailEnabled',
+ 'isEmailSet',
+ 'settingBatchtime',
+ ]),
+ },
+ methods: {
+ isActivityEnabled,
+ ...mapActions([
+ 'toggleMethodForMethodAndActivity',
+ 'toggleMethodForGroup',
+ 'toggleMethodsForActivity',
+ ]),
+ actionName(method) {
+ if (method === 'email') {
+ return t('activity', 'Send email')
+ } else {
+ return t('activity', 'Send push notification')
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+
+table.grid {
+ // Hack: align content of the table with the rest of the page
+ margin-left: -0.8em;
+
+ h3 {
+ font-weight: bold;
+ }
+}
+
+table.grid th {
+ color: var(--color-text-light);
+ height: 44px;
+}
+
+table.grid .group-header {
+ th {
+ padding-top: 16px;
+ height: 60px;
+ &.activity_select_group {
+ padding-left: 20px;
+ }
+ }
+}
+
+table.grid th.activity_select_group {
+ color: var(--color-main-text);
+}
+
+.sr-only {
+ position:absolute;
+ left:-10000px;
+ top:auto;
+ width:1px;
+ height:1px;
+ overflow:hidden;
+}
+</style>
diff --git a/src/components/Checkbox.vue b/src/components/Checkbox.vue
new file mode 100644
index 00000000..f8439ce2
--- /dev/null
+++ b/src/components/Checkbox.vue
@@ -0,0 +1,273 @@
+<!--
+ - @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <element :is="wrapperElement"
+ :class="{
+ 'checkbox-radio-switch--checked': isChecked,
+ 'checkbox-radio-switch--disabled': disabled,
+ 'checkbox-radio-switch--indeterminate': indeterminate,
+ }"
+ :style="cssVars"
+ class="checkbox-radio-switch checkbox-radio-switch-checkbox">
+ <input :id="id"
+ :checked="isChecked"
+ :disabled="disabled"
+ :indeterminate="indeterminate"
+ :name="name"
+ type="checkbox"
+ :value="value"
+ class="checkbox-radio-switch__input"
+ @change="onToggle">
+
+ <label :for="id" class="checkbox-radio-switch__label">
+ <icon :is="checkboxRadioIconElement"
+ :size="size"
+ class="checkbox-radio-switch__icon"
+ title=""
+ decorative />
+ <span class="sr-only">
+ <slot />
+ </span>
+ </label>
+ </element>
+</template>
+
+<script>
+import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline'
+import CheckboxBlank from 'vue-material-design-icons/CheckboxBlank'
+import MinusBox from 'vue-material-design-icons/MinusBox'
+import CheckboxMarked from 'vue-material-design-icons/CheckboxMarked'
+
+export default {
+ name: 'Checkbox',
+
+ props: {
+
+ /**
+ * Unique id attribute of the input
+ */
+ id: {
+ type: String,
+ required: true,
+ validator: id => id.trim() !== '',
+ },
+
+ /**
+ * Checked state. To be used with `:value.sync`
+ */
+ checked: {
+ type: [Boolean, Array, String],
+ default: false,
+ },
+
+ /**
+ * Value to be synced on check
+ */
+ value: {
+ type: String,
+ default: null,
+ },
+
+ /**
+ * Disabled state
+ */
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+
+ /**
+ * Indeterminate state
+ */
+ indeterminate: {
+ type: Boolean,
+ default: false,
+ },
+
+ /**
+ * Wrapping element tag
+ */
+ wrapperElement: {
+ type: String,
+ default: 'span',
+ },
+
+ /**
+ * Input name. Required for radio, optional for checkbox
+ */
+ name: {
+ type: String,
+ default: null,
+ },
+ },
+
+ computed: {
+ /**
+ * Icon size
+ *
+ @return {number}
+ */
+ size() {
+ return 24
+ },
+
+ /**
+ * Css local variables for this component
+ *
+ * @return {object}
+ */
+ cssVars() {
+ return {
+ '--icon-size': this.size + 'px',
+ }
+ },
+
+ isChecked() {
+ return [...this.checked].indexOf(this.value) > -1
+ },
+
+ /**
+ * Returns the proper Material icon depending on the select case
+ *
+ * @return {Component}
+ */
+ checkboxRadioIconElement() {
+ if (this.indeterminate) {
+ return MinusBox
+ }
+ if (this.disabled && !this.isChecked) {
+ return CheckboxBlank
+ }
+ if (this.isChecked) {
+ return CheckboxMarked
+ }
+ return CheckboxBlankOutline
+ },
+ },
+
+ methods: {
+ onToggle() {
+ if (this.disabled) {
+ return
+ }
+
+ // If the initial value was a boolean, let's keep it that way
+ if (typeof this.checked === 'boolean') {
+ this.$emit('update:checked', !this.isChecked)
+ return
+ }
+
+ // Dispatch the checked values as an array if multiple, or single value otherwise
+ const values = this.getInputsSet()
+ .filter(input => input.checked)
+ .map(input => input.value)
+ this.$emit('update:checked', values)
+ },
+
+ /**
+ * Get the input set based on this name
+ *
+ * @return {Node[]}
+ */
+ getInputsSet() {
+ return [...document.getElementsByName(this.name)]
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+$spacing: 4px;
+
+.checkbox-radio-switch {
+ display: flex;
+
+ &__input {
+ position: fixed;
+ z-index: -1;
+ top: -5000px;
+ left: -5000px;
+ opacity: 0;
+ }
+
+ &__label {
+ display: flex;
+ align-items: center;
+ user-select: none;
+ height: 32px;
+ width: 32px;
+ border-radius: 44px;
+ padding: 0;
+ margin: 2px;
+
+ &, * {
+ cursor: pointer;
+ }
+ }
+
+ &__icon {
+ margin-right: $spacing;
+ margin-left: $spacing;
+ color: var(--color-primary-element);
+ width: var(--icon-size);
+ height: var(--icon-size);
+ }
+
+ &--disabled &__label {
+ opacity: 0.7;
+ .checkbox-radio-switch__icon {
+ color: var(--color-text-light)
+ }
+ }
+
+ &:not(&--disabled) &__input:hover + &__label,
+ &:not(&--disabled) &__input:focus + &__label {
+ background-color: var(--color-primary-light);
+ }
+
+ // Increase focus effect
+ &:not(&--disabled) &__input:focus + &__label {
+ box-shadow: 0 0 0 2px var(--color-primary);
+ }
+
+ // Switch specific rules
+ &-switch:not(&--checked) &__icon {
+ color: var(--color-text-lighter);
+ }
+
+ // If switch is checked AND disabled, use the fade primary colour
+ &-switch.checkbox-radio-switch--disabled.checkbox-radio-switch--checked &__icon {
+ color: var(--color-primary-element-light);
+ }
+
+ .sr-only {
+ position:absolute;
+ left:-10000px;
+ top:auto;
+ width:1px;
+ height:1px;
+ overflow:hidden;
+ }
+}
+
+</style>
diff --git a/src/components/EmailSettings.vue b/src/components/EmailSettings.vue
new file mode 100644
index 00000000..e7f38549
--- /dev/null
+++ b/src/components/EmailSettings.vue
@@ -0,0 +1,87 @@
+<!--
+ - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div v-if="emailEnabled">
+ <p v-if="!isEmailSet">
+ <strong>{{ t('activity', 'You need to set up your email address before you can receive notification emails.') }}</strong>
+ </p>
+ <p>
+ <label for="activity_setting_batchtime" class="activity-frequency__label">
+ {{ t('activity', 'Send activity emails') }}
+ </label>
+ <select class="notification-frequency__select"
+ name="notify_setting_batchtime"
+ @change="setSettingBatchtime({settingBatchtime: $event.target.value})">
+ <option :value="EmailFrequency.EMAIL_SEND_ASAP"
+ :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_ASAP">
+ {{ t('activity', 'As soon as possible') }}
+ </option>
+ <option :value="EmailFrequency.EMAIL_SEND_HOURLY"
+ :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_HOURLY">
+ {{ t('activity', 'Hourly') }}
+ </option>
+ <option :value="EmailFrequency.EMAIL_SEND_DAILY"
+ :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_DAILY">
+ {{ t('activity', 'Daily') }}
+ </option>
+ <option :value="EmailFrequency.EMAIL_SEND_WEEKLY"
+ :selected="settingBatchtime === EmailFrequency.EMAIL_SEND_WEEKLY">
+ {{ t('activity', 'Weekly') }}
+ </option>
+ </select>
+ </p>
+ </div>
+</template>
+
+<script>
+import { mapActions, mapState } from 'vuex'
+import EmailFrequency from '../models/EmailFrequency'
+
+export default {
+ name: 'EmailSettings',
+
+ data() {
+ return {
+ EmailFrequency: EmailFrequency.EmailFrequency,
+ }
+ },
+ computed: {
+ ...mapState([
+ 'emailEnabled',
+ 'isEmailSet',
+ 'settingBatchtime',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'setSettingBatchtime',
+ ]),
+ },
+}
+
+</script>
+
+<style lang="scss" scoped>
+.activity-frequency__label {
+ margin-top: 24px;
+ display: inline-block;
+}
+</style>
diff --git a/src/helpers/settings.js b/src/helpers/settings.js
new file mode 100644
index 00000000..0e6a6cb2
--- /dev/null
+++ b/src/helpers/settings.js
@@ -0,0 +1,52 @@
+/**
+ * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * Return wether the notification method can be checked for the activity
+ *
+ * @param {ActivityType} activity - the concerned activity
+ * @param {string} methodKey - the concerned method
+ * @return {boolean}
+ */
+function isActivityEnabled(activity, methodKey) {
+ return activity.methods.includes(methodKey)
+}
+
+/**
+ * @param {Array<ActivityType>} activities - List of the activities to check
+ * @param {string} methodKey - the method key for which to verify the checked value
+ * @return {boolean} wether at least one input is checked for the given set of activities
+ */
+function isOneInputUnChecked(activities, methodKey) {
+ for (const activity of activities) {
+ if (isActivityEnabled(activity, methodKey) && !activity[methodKey]) {
+ return true
+ }
+ }
+
+ return false
+}
+
+export {
+ isActivityEnabled,
+ isOneInputUnChecked,
+}
diff --git a/src/models/ActivityModel.js b/src/models/ActivityModel.js
index 07ae0f25..e8c99f08 100644
--- a/src/models/ActivityModel.js
+++ b/src/models/ActivityModel.js
@@ -3,7 +3,7 @@
*
* @author Louis Chemineau <louis@chmn.me>
*
- * @license GPL-3.0-or-later
+ * @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -22,13 +22,6 @@
import moment from '@nextcloud/moment'
-/**
- * @typedef RichObject
- * @type {object}
- * @property {string} id - The id of the riche object.
- * @property {string} type - The type of the file object.
- */
-
export default class ActivityModel {
_activity
@@ -67,7 +60,7 @@ export default class ActivityModel {
/**
* get the activity id
*
- * @return {number}
+ * @return {int}
* @readonly
* @memberof ActivityModel
*/
@@ -89,7 +82,7 @@ export default class ActivityModel {
/**
* Get the activity type
*
- * @return {number}
+ * @return {int}
* @readonly
* @memberof ActivityModel
*/
@@ -133,7 +126,7 @@ export default class ActivityModel {
/**
* Get the activity subject_rich objects
*
- * @return {object.<string, RichObject>}
+ * @return {object<string, RichObject>}
* @readonly
* @memberof ActivityModel
*/
@@ -170,7 +163,7 @@ export default class ActivityModel {
/**
* Get the activity message_rich objects
*
- * @return {object.<string, RichObject>}
+ * @return {object<string, RichObject>}
* @readonly
* @memberof ActivityModel
*/
@@ -196,7 +189,7 @@ export default class ActivityModel {
/**
* Get the activity object_id
*
- * @return {number}
+ * @return {int}
* @readonly
* @memberof ActivityModel
*/
@@ -273,7 +266,7 @@ export default class ActivityModel {
/**
* Get the activity timestamp
*
- * @return {number}
+ * @return {string}
* @readonly
* @memberof ActivityModel
*/
diff --git a/src/models/ActivitySettings.js b/src/models/ActivitySettings.js
new file mode 100644
index 00000000..93eca87f
--- /dev/null
+++ b/src/models/ActivitySettings.js
@@ -0,0 +1,35 @@
+/**
+ * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ *
+ * @typedef {object} ActivityGroup
+ * @property {string} name - The name of the activity group
+ * @property {object<string, ActivityType>} activities - List off activities
+ */
+
+/**
+ *
+ * @typedef {object} ActivityType
+ * @property {string} desc - The activity's description
+ * @property {Array<string>} methods - List of available methods to send a notification
+ */
diff --git a/src/models/EmailFrequency.js b/src/models/EmailFrequency.js
new file mode 100644
index 00000000..6465aa18
--- /dev/null
+++ b/src/models/EmailFrequency.js
@@ -0,0 +1,30 @@
+/**
+ * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export default {
+ EmailFrequency: {
+ EMAIL_SEND_HOURLY: 0,
+ EMAIL_SEND_DAILY: 1,
+ EMAIL_SEND_WEEKLY: 2,
+ EMAIL_SEND_ASAP: 3,
+ },
+}
diff --git a/src/settings-admin.js b/src/settings-admin.js
new file mode 100644
index 00000000..48307279
--- /dev/null
+++ b/src/settings-admin.js
@@ -0,0 +1,51 @@
+/**
+ * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import AdminSettings from './views/AdminSettings'
+import DefaultActivtiySettings from './views/DefaultActivitySettings'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { generateFilePath } from '@nextcloud/router'
+import store from './settings-store'
+
+Vue.prototype.t = t
+Vue.prototype.n = n
+
+// eslint-disable-next-line no-undef, camelcase
+__webpack_public_path__ = generateFilePath(appName, '', 'js/')
+
+Vue.use(Vuex)
+
+export default {
+ adminSetting: new Vue({
+ el: '#activity-admin-settings',
+ store,
+ name: 'ActivityPersonalSettings',
+ render: h => h(AdminSettings),
+ }),
+ defaultSetting: new Vue({
+ el: '#activity-default-settings',
+ store,
+ name: 'ActivityDefaultSettings',
+ render: h => h(DefaultActivtiySettings),
+ }),
+}
diff --git a/src/settings-personal.js b/src/settings-personal.js
new file mode 100644
index 00000000..748dbc83
--- /dev/null
+++ b/src/settings-personal.js
@@ -0,0 +1,51 @@
+/**
+ * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import UserSettings from './views/UserSettings'
+import DailySummary from './views/DailySummary'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { generateFilePath } from '@nextcloud/router'
+import store from './settings-store'
+
+Vue.prototype.t = t
+Vue.prototype.n = n
+
+// eslint-disable-next-line no-undef, camelcase
+__webpack_public_path__ = generateFilePath(appName, '', 'js/')
+
+Vue.use(Vuex)
+
+export default {
+ userSetting: new Vue({
+ el: '#activity-user-settings',
+ store,
+ name: 'ActivityPersonalSettings',
+ render: h => h(UserSettings),
+ }),
+ digestSetting: new Vue({
+ el: '#activity-digest-user-settings',
+ name: 'ActivityDigestPersonalSettings',
+ store,
+ render: h => h(DailySummary),
+ }),
+}
diff --git a/src/settings-store.js b/src/settings-store.js
new file mode 100644
index 00000000..61a9d5ef
--- /dev/null
+++ b/src/settings-store.js
@@ -0,0 +1,322 @@
+/**
+ * @copyright Copyright (c) 2021 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+import Vuex from 'vuex'
+import { translate as t } from '@nextcloud/l10n'
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import { showSuccess, showError } from '@nextcloud/dialogs'
+import '@nextcloud/dialogs/styles/toast.scss'
+
+import { isActivityEnabled, isOneInputUnChecked } from './helpers/settings'
+import logger from './logger'
+
+Vue.use(Vuex)
+
+/**
+ * @typedef {object} SettingsState
+ * @property {object} setting
+ * @property {object<string, ActivityGroup>} activityGroups
+ * @property {boolean} isEmailSet
+ * @property {boolean} emailEnabled
+ * @property {boolean} activityDigestEnabled
+ * @property {0|1|2|3} settingBatchtime
+ * @property {Array<string>} methods
+ * @property {string} endpoint
+ */
+
+const store = new Vuex.Store({
+ strict: true,
+ /** @type {SettingsState} */
+ state: {
+ setting: loadState('activity', 'setting'),
+ activityGroups: loadState('activity', 'activity_groups'),
+ isEmailSet: loadState('activity', 'is_email_set'),
+ emailEnabled: loadState('activity', 'email_enabled'),
+ activityDigestEnabled: loadState('activity', 'activity_digest_enabled', false),
+ settingBatchtime: loadState('activity', 'setting_batchtime'),
+ methods: loadState('activity', 'methods'),
+ endpoint: '',
+ },
+ getters: {
+ /**
+ * Return an array of checked activities.
+ *
+ * @param {SettingsState} state - The current state.
+ * @return {Array<string>}
+ */
+ checkedActivities(state) {
+ const methodsEnabled = (activityKey, activity) => {
+ const methods = []
+ if (activity.email) {
+ methods.push({ activityKey, method: 'email', activity })
+ }
+ if (activity.notification) {
+ methods.push({ activityKey, method: 'notification', activity })
+ }
+ return methods
+ }
+
+ return Object.values(state.activityGroups)
+ .map(group => Object.entries(group.activities)) // [[[activityKey, activity], ...], [[activityKey, activity], ...]]
+ .reduce((acc, val) => acc.concat(val), []) // [[activityKey, activity], ...]
+ .map(([activityKey, activity]) => methodsEnabled(activityKey, activity)) // [[{activityKey, method, activity}, ...], ...]
+ .reduce((acc, val) => acc.concat(val), [])
+ .filter(({ activity, method }) => activity[method])
+ .map(({ activityKey, method }) => `${activityKey}_${method}`) // ['enabled_activity_key', ...]
+ },
+ },
+ mutations: {
+ /**
+ * Update the 'enabled' state of a notification method for a given group/activity/method tuple
+ *
+ * @param {SettingsState} state - The current state.
+ * @param {object} payload - The payload.
+ * @param {string} payload.groupKey - The targeted group
+ * @param {string} payload.activityKey - The targeted activity
+ * @param {string} payload.methodKey - The targeted method
+ * @param {string} payload.value - The value to set
+ */
+ SET_METHOD_FOR_METHOD_AND_ACTIVITY(state, { groupKey, activityKey, methodKey, value }) {
+ const group = state.activityGroups[groupKey]
+ const activity = group.activities[activityKey]
+
+ if (isActivityEnabled(activity, methodKey)) {
+ activity[methodKey] = value
+ }
+ },
+ /**
+ * Set the endpoint used to save the settings.
+ *
+ * @param {SettingsState} state - The current state.
+ * @param {object} payload - The payload.
+ * @param {string} payload.endpoint - Where to POST the saveSettings request.
+ */
+ SET_ENDPOINT(state, { endpoint }) {
+ state.endpoint = endpoint
+ },
+ /**
+ * Set the batch time.
+ *
+ * @param {SettingsState} state - The current state.
+ * @param {object} payload - The payload.
+ * @param {0|1|2|3} payload.settingBatchtime - The selected batch time.
+ */
+ SET_SETTING_BATCHTIME(state, { settingBatchtime }) {
+ state.settingBatchtime = settingBatchtime
+ },
+ /**
+ * Toggle activity digest.
+ *
+ * @param {SettingsState} state - The current state.
+ * @param {object} payload - The payload.
+ * @param {boolean} payload.activityDigestEnabled - Enabled status of the activity digest.
+ */
+ TOGGLE_ACTIVITY_DIGEST(state, { activityDigestEnabled }) {
+ state.activityDigestEnabled = activityDigestEnabled
+ },
+ /**
+ * Toggle the availability of mail notifications
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {boolean} payload.emailEnabled - Enabled status of the email notifications.
+ * @param state
+ */
+ TOGGLE_EMAIL_ENABLED(state, { emailEnabled }) {
+ state.emailEnabled = emailEnabled
+ },
+ },
+ actions: {
+ /**
+ * Set the endpoint used to save the settings.
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {string} payload.endpoint - Where to POST the saveSettings request.
+ */
+ setEndpoint({ commit }, { endpoint }) {
+ commit('SET_ENDPOINT', { endpoint })
+ },
+ /**
+ * Toggle the 'enabled' state of a notification method for a given group/activity/method tuple
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {string} payload.groupKey - The targeted group
+ * @param {string} payload.activityKey - The targeted activity
+ * @param {string} payload.methodKey - The targeted method
+ */
+ toggleMethodForMethodAndActivity({ commit, state, dispatch }, { groupKey, activityKey, methodKey }) {
+ const activity = state.activityGroups[groupKey].activities[activityKey]
+ const oneInputIsChecked = isOneInputUnChecked([activity], methodKey)
+
+ commit(
+ 'SET_METHOD_FOR_METHOD_AND_ACTIVITY',
+ {
+ groupKey,
+ activityKey,
+ methodKey,
+ value: oneInputIsChecked,
+ })
+
+ dispatch('saveSettings')
+ },
+ /**
+ * Toggle the 'enabled' state of a notification method for a given group/method tuple
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {string} payload.groupKey - The targeted group
+ * @param {string} payload.methodKey - The targeted method
+ */
+ toggleMethodForGroup({ commit, state, dispatch }, { groupKey, methodKey }) {
+ const activities = Object.values(state.activityGroups[groupKey].activities)
+ const oneInputIsChecked = isOneInputUnChecked(activities, methodKey)
+
+ for (const activityKey in state.activityGroups[groupKey].activities) {
+ commit(
+ 'SET_METHOD_FOR_METHOD_AND_ACTIVITY',
+ {
+ groupKey,
+ activityKey,
+ methodKey,
+ value: oneInputIsChecked,
+ })
+ }
+
+ dispatch('saveSettings')
+ },
+ /**
+ * Toggle the 'enabled' state of a notification method for a given group/activity tuple
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {string} payload.groupKey - The targeted group
+ * @param {string} payload.activityKey - The targeted activity
+ */
+ toggleMethodsForActivity({ commit, state, dispatch }, { groupKey, activityKey }) {
+ const activity = state.activityGroups[groupKey].activities[activityKey]
+ const oneInputIsChecked = activity.methods.map(method => isOneInputUnChecked([activity], method)).includes(true)
+
+ for (const methodKey of activity.methods) {
+ commit(
+ 'SET_METHOD_FOR_METHOD_AND_ACTIVITY',
+ {
+ groupKey,
+ activityKey,
+ methodKey,
+ value: oneInputIsChecked,
+ })
+ }
+
+ dispatch('saveSettings')
+ },
+ /**
+ * Set the batch time.
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {0|1|2|3} payload.settingBatchtime - The selected batch time.
+ */
+ setSettingBatchtime({ commit, dispatch }, { settingBatchtime }) {
+ commit(
+ 'SET_SETTING_BATCHTIME',
+ {
+ settingBatchtime,
+ })
+
+ dispatch('saveSettings')
+ },
+ /**
+ * Toggle the activity digest.
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {boolean} payload.activityDigestEnabled - Enabled status of the activity digest.
+ */
+ toggleActivityDigestEnabled({ commit, dispatch }, { activityDigestEnabled }) {
+ commit(
+ 'TOGGLE_ACTIVITY_DIGEST',
+ {
+ activityDigestEnabled,
+ })
+
+ dispatch('saveSettings')
+ },
+ /**
+ * Toggle the availability of mail notifications
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} store -
+ * @param {object} payload - The payload.
+ * @param {boolean} payload.emailEnabled - Enabled status of the email notifications.
+ */
+ toggleEmailEnabled({ commit }, { emailEnabled }) {
+ commit(
+ 'TOGGLE_EMAIL_ENABLED',
+ {
+ emailEnabled,
+ })
+
+ try {
+
+ OCP.AppConfig.setValue(
+ 'activity', 'enable_email',
+ emailEnabled ? 'yes' : 'no'
+ )
+
+ showSuccess(t('activity', 'Your settings have been updated.'))
+ } catch (error) {
+ showError(t('activity', 'Unable to save the settings'))
+ logger.error('An error occurred while saving the activity settings', error)
+ }
+ },
+ /**
+ * Save the currently displayed settings
+ *
+ * @param {import('vuex').ActionContext<SettingsState, SettingsState>} _ -
+ */
+ async saveSettings({ state, getters }) {
+ try {
+ const form = new FormData()
+ getters.checkedActivities.forEach(activity => {
+ form.append(activity, '1')
+ })
+
+ form.append('notify_setting_batchtime', `${state.settingBatchtime}`)
+ form.append('activity_digest', `${state.activityDigestEnabled ? 1 : 0}`)
+
+ const response = await axios.post(generateUrl(state.endpoint), form)
+
+ showSuccess(response.data.data.message)
+ } catch (error) {
+ showError(t('activity', 'Unable to save the settings'))
+ logger.error('An error occurred while saving the activity settings', error)
+ }
+ },
+ },
+})
+
+export default store
diff --git a/src/tests/setup.js b/src/tests/setup.js
index b29e9938..96b67d34 100644
--- a/src/tests/setup.js
+++ b/src/tests/setup.js
@@ -3,7 +3,7 @@
*
* @author Louis Chemineau <louis@chmn.me>
*
- * @license GPL-3.0-or-later
+ * @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue
new file mode 100644
index 00000000..c31f335a
--- /dev/null
+++ b/src/views/AdminSettings.vue
@@ -0,0 +1,68 @@
+<!--
+ - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <SettingsSection :title="t('activity', 'Notification')">
+ <CheckboxRadioSwitch type="checkbox"
+ :checked="emailEnabled"
+ @update:checked="toggleEmailEnabled({emailEnabled: $event})">
+ {{ t('activity', 'Enable notification emails') }}
+ </CheckboxRadioSwitch>
+ </SettingsSection>
+</template>
+
+<script>
+import { mapActions, mapState } from 'vuex'
+import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
+import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
+
+export default {
+ name: 'AdminSettings',
+ components: {
+ CheckboxRadioSwitch,
+ SettingsSection,
+ },
+
+ mounted() {
+ this.setEndpoint({ endpoint: '/apps/activity/settings/admin' })
+ },
+
+ methods: {
+ ...mapActions([
+ 'setEndpoint',
+ 'toggleEmailEnabled',
+ ]),
+ },
+
+ computed: {
+ ...mapState({
+ emailEnabled: 'emailEnabled',
+ }),
+ settingDescription() {
+ if (this.emailEnabled) {
+ return t('activity', 'Choose for which activities you want to get an email or push notification.')
+ } else {
+ return t('activity', 'Choose for which activities you want to get a push notification.')
+ }
+ },
+ },
+}
+
+</script>
diff --git a/src/views/DailySummary.vue b/src/views/DailySummary.vue
new file mode 100644
index 00000000..a7dc4f4f
--- /dev/null
+++ b/src/views/DailySummary.vue
@@ -0,0 +1,38 @@
+<template>
+ <SettingsSection :title="t('activity', 'Daily activtiy summary')">
+ <CheckboxRadioSwitch :checked="activityDigestEnabled" @update:checked="toggleActivityDigestEnabled({activityDigestEnabled: $event})">
+ {{ t('activity', 'Send daily activity summary in the morning') }}
+ </CheckboxRadioSwitch>
+ </SettingsSection>
+</template>
+
+<script>
+import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
+import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
+import { mapActions, mapState } from 'vuex'
+
+export default {
+ name: 'DailySummary',
+ components: {
+ CheckboxRadioSwitch,
+ SettingsSection,
+ },
+
+ computed: {
+ ...mapState([
+ 'activityDigestEnabled',
+ ]),
+ },
+
+ mounted() {
+ this.setEndpoint({ endpoint: '/apps/activity/settings' })
+ },
+
+ methods: {
+ ...mapActions([
+ 'setEndpoint',
+ 'toggleActivityDigestEnabled',
+ ]),
+ },
+}
+</script>
diff --git a/src/views/DefaultActivitySettings.vue b/src/views/DefaultActivitySettings.vue
new file mode 100644
index 00000000..fc2c0c52
--- /dev/null
+++ b/src/views/DefaultActivitySettings.vue
@@ -0,0 +1,58 @@
+<!--
+ - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <SettingsSection :title="t('activity', 'Default settings')"
+ :description="t('activity', 'Configure the default notification settings for new users.')">
+ <ActivityGrid />
+ </SettingsSection>
+</template>
+
+<script>
+import { mapActions, mapState } from 'vuex'
+import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
+import ActivityGrid from '../components/ActivityGrid'
+
+export default {
+ name: 'DefaultActivitySettings',
+ components: {
+ ActivityGrid,
+ SettingsSection,
+ },
+
+ mounted() {
+ this.setEndpoint({ endpoint: '/apps/activity/settings/admin' })
+ },
+
+ methods: {
+ ...mapActions([
+ 'setEndpoint',
+ 'toggleEmailEnabled',
+ ]),
+ },
+
+ computed: {
+ ...mapState({
+ emailEnabled: 'emailEnabled',
+ }),
+ },
+}
+
+</script>
diff --git a/src/views/UserSettings.vue b/src/views/UserSettings.vue
new file mode 100644
index 00000000..b9d6beeb
--- /dev/null
+++ b/src/views/UserSettings.vue
@@ -0,0 +1,68 @@
+<!--
+ - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <SettingsSection :title="t('activity', 'Activity')"
+ :description="settingDescription">
+ <ActivityGrid />
+ <EmailSettings />
+ </SettingsSection>
+</template>
+
+<script>
+import { mapActions, mapState } from 'vuex'
+import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
+import EmailSettings from '../components/EmailSettings'
+import ActivityGrid from '../components/ActivityGrid'
+
+export default {
+ name: 'UserSettings',
+ components: {
+ SettingsSection,
+ EmailSettings,
+ ActivityGrid,
+ },
+
+ mounted() {
+ this.setEndpoint({ endpoint: '/apps/activity/settings' })
+ },
+
+ methods: {
+ ...mapActions([
+ 'setEndpoint',
+ 'toggleEmailEnabled',
+ ]),
+ },
+
+ computed: {
+ ...mapState({
+ emailEnabled: 'emailEnabled',
+ }),
+ settingDescription() {
+ if (this.emailEnabled) {
+ return t('activity', 'Choose for which activities you want to get an email or push notification.')
+ } else {
+ return t('activity', 'Choose for which activities you want to get a push notification.')
+ }
+ },
+ },
+}
+
+</script>