diff --git a/lib/Service/QuickActionsService.php b/lib/Service/QuickActionsService.php
index 7f11e9a5db..00931c9cf7 100644
--- a/lib/Service/QuickActionsService.php
+++ b/lib/Service/QuickActionsService.php
@@ -21,6 +21,7 @@ class QuickActionsService {
public const AVAILABLE_ACTION_STEPS = [
'markAsSpam',
'applyTag',
+ 'removeTags',
'snooze',
'moveThread',
'deleteThread',
diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue
index f10c62ba6c..2e8870f0c0 100644
--- a/src/components/Envelope.vue
+++ b/src/components/Envelope.vue
@@ -602,7 +602,6 @@ import useMainStore from '../store/mainStore.js'
import { mailboxHasRights } from '../util/acl.js'
import { flatRelativeDatetime, groupedRelativeDatetime, messageDateTime } from '../util/relativeDatetime.js'
import { translateTagDisplayName } from '../util/tag.js'
-import { hiddenTags } from './tags.js'
export default {
name: 'Envelope',
@@ -852,7 +851,7 @@ export default {
},
tags() {
- let tags = this.mainStore.getEnvelopeTags(this.data.databaseId).filter((tag) => tag.imapLabel && tag.imapLabel !== '$label1' && !(tag.displayName.toLowerCase() in hiddenTags))
+ let tags = this.mainStore.getEnvelopeExposedTags(this.data.databaseId)
// Don't show follow-up tag in unified mailbox as it has its own section at the top
if (this.mailbox.isUnified) {
@@ -1005,7 +1004,7 @@ export default {
const quickActions = this.mainStore.getQuickActions().filter((action) => action.accountId === this.data.accountId)
for (const action of quickActions) {
const check = action.actionSteps.every((step) => {
- if (['markAsSpam', 'applyTag', 'markAsImportant', 'markAsFavorite'].includes(step.name) && !this.hasWriteAcl) {
+ if (['markAsSpam', 'applyTag', 'removeTags', 'markAsImportant', 'markAsFavorite'].includes(step.name) && !this.hasWriteAcl) {
return false
}
if (['markAsRead', 'markAsUnread'].includes(step.name) && !this.hasSeenAcl) {
@@ -1087,6 +1086,9 @@ export default {
showWarning(t('mail', 'Could not apply tag, configured tag not found'))
}
break
+ case 'removeTags':
+ await this.removeTags()
+ break
case 'markAsImportant':
if (!this.isImportant) {
if (this.layoutMessageViewThreaded) {
@@ -1154,6 +1156,21 @@ export default {
}
},
+ async removeTags() {
+ const threadEnvelopes = this.layoutMessageViewThreaded
+ ? this.mainStore.getEnvelopesByThreadRootId(this.data.accountId, this.data.threadRootId)
+ : [this.data]
+ for (const envelope of threadEnvelopes) {
+ const promises = []
+ const tags = this.mainStore.getEnvelopeExposedTags(envelope.databaseId)
+ for (const tag of tags) {
+ const call = this.mainStore.removeEnvelopeTag({ envelope, imapLabel: tag.imapLabel })
+ promises.push(call)
+ }
+ await Promise.all(promises)
+ }
+ },
+
unselect() {
if (this.selected) {
this.$emit('update:selected', false)
diff --git a/src/components/TagModal.vue b/src/components/TagModal.vue
index 7d163375a0..80dbeb393a 100644
--- a/src/components/TagModal.vue
+++ b/src/components/TagModal.vue
@@ -66,7 +66,6 @@ import TagItem from './TagItem.vue'
import logger from '../logger.js'
import useMainStore from '../store/mainStore.js'
import { validateTag } from '../util/tag.js'
-import { hiddenTags } from './tags.js'
function randomColor() {
let randomHexColor = ((1 << 24) * Math.random() | 0).toString(16)
@@ -114,7 +113,7 @@ export default {
computed: {
...mapStores(useMainStore),
tags() {
- return this.mainStore.getTags.filter((tag) => tag.imapLabel !== '$label1' && !(tag.displayName.toLowerCase() in hiddenTags)).sort((a, b) => {
+ return this.mainStore.getExposedTags().sort((a, b) => {
if (a.isDefaultTag && !b.isDefaultTag) {
return -1
}
diff --git a/src/components/ThreadEnvelope.vue b/src/components/ThreadEnvelope.vue
index 7484e623aa..37d1ca9219 100644
--- a/src/components/ThreadEnvelope.vue
+++ b/src/components/ThreadEnvelope.vue
@@ -435,7 +435,6 @@ import useOutboxStore from '../store/outboxStore.js'
import { mailboxHasRights } from '../util/acl.js'
import { translateTagDisplayName } from '../util/tag.js'
import { Text, toPlain } from '../util/text.js'
-import { hiddenTags } from './tags.js'
// Ternary loading state
const Loading = Object.seal({
@@ -625,7 +624,7 @@ export default {
},
tags() {
- return this.mainStore.getEnvelopeTags(this.envelope.databaseId).filter((tag) => tag.imapLabel !== '$label1' && !(tag.displayName.toLowerCase() in hiddenTags))
+ return this.mainStore.getEnvelopeExposedTags(this.envelope.databaseId)
},
hasChangedSubject() {
diff --git a/src/components/quickActions/Action.vue b/src/components/quickActions/Action.vue
index 3b510ebef1..287a5929e7 100644
--- a/src/components/quickActions/Action.vue
+++ b/src/components/quickActions/Action.vue
@@ -36,7 +36,6 @@ import CloseIcon from 'vue-material-design-icons/Close.vue'
import DragIcon from 'vue-material-design-icons/Drag.vue'
import Icon from './Icon.vue'
import useMainStore from '../../store/mainStore.js'
-import { hiddenTags } from '../tags.js'
export default {
name: 'Action',
@@ -85,7 +84,7 @@ export default {
options() {
if (this.action.name === 'applyTag') {
- return this.mainStore.getTags.filter((tag) => tag.imapLabel !== '$label1' && !(tag.displayName.toLowerCase() in hiddenTags)).map((tag) => ({
+ return this.mainStore.getExposedTags().map((tag) => ({
value: tag.displayName,
id: tag.id,
}))
@@ -109,6 +108,8 @@ export default {
return this.t('mail', 'Mark as spam')
case 'applyTag':
return this.t('mail', 'Tag')
+ case 'removeTags':
+ return this.t('mail', 'Remove tags')
case 'moveThread':
return this.t('mail', 'Move thread')
case 'deleteThread':
diff --git a/src/components/quickActions/Icon.vue b/src/components/quickActions/Icon.vue
index ca6bbbceb5..37f18ca981 100644
--- a/src/components/quickActions/Icon.vue
+++ b/src/components/quickActions/Icon.vue
@@ -14,6 +14,7 @@ import EmailUnread from 'vue-material-design-icons/EmailOutline.vue'
import ImportantIcon from 'vue-material-design-icons/LabelVariant.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import IconFavorite from 'vue-material-design-icons/Star.vue'
+import TagOffIcon from 'vue-material-design-icons/TagOffOutline.vue'
import TagIcon from 'vue-material-design-icons/TagOutline.vue'
import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue'
export default {
@@ -32,6 +33,8 @@ export default {
return AlertOctagonIcon
case 'applyTag':
return TagIcon
+ case 'removeTags':
+ return TagOffIcon
case 'markAsImportant':
return ImportantIcon
case 'markAsFavorite':
diff --git a/src/components/quickActions/Settings.vue b/src/components/quickActions/Settings.vue
index 29a6283365..37a55d6bb5 100644
--- a/src/components/quickActions/Settings.vue
+++ b/src/components/quickActions/Settings.vue
@@ -64,6 +64,12 @@
{{ t('mail', 'Tag') }}
+
+
+
+
+ {{ t('mail', 'Remove tags') }}
+
@@ -129,6 +135,7 @@ import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import IconEdit from 'vue-material-design-icons/PencilOutline.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import IconFavorite from 'vue-material-design-icons/Star.vue'
+import TagOffIcon from 'vue-material-design-icons/TagOffOutline.vue'
import TagIcon from 'vue-material-design-icons/TagOutline.vue'
import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue'
import Action from './Action.vue'
@@ -153,6 +160,7 @@ export default {
Draggable,
AlertOctagonIcon,
TagIcon,
+ TagOffIcon,
OpenInNewIcon,
ImportantIcon,
EmailRead,
diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js
index 825551d174..69840b0ac1 100644
--- a/src/store/mainStore/actions.js
+++ b/src/store/mainStore/actions.js
@@ -32,6 +32,7 @@ import {
where,
} from 'ramda'
import Vue from 'vue'
+import { hiddenTags } from '../../components/tags.js'
import MailboxLockedError from '../../errors/MailboxLockedError.js'
import { matchError } from '../../errors/match.js'
import SyncIncompleteError from '../../errors/SyncIncompleteError.js'
@@ -2489,6 +2490,13 @@ export default function mainStoreActions() {
const tags = this.envelopes[id]?.tags ?? []
return tags.map((tagId) => this.tags[tagId])
},
+ getExposedTags() {
+ return this.getTags.filter((tag) => tag.imapLabel && tag.imapLabel !== '$label1' && !(tag.displayName.toLowerCase() in hiddenTags))
+ },
+ getEnvelopeExposedTags(id) {
+ const exposed = this.getExposedTags().map((t) => t.id)
+ return this.getEnvelopeTags(id).filter((t) => exposed.includes(t.id))
+ },
getTag(id) {
return this.tags[id]
},