Skip to content

Commit

Permalink
Limit the number of labels that can be applied to a single email
Browse files Browse the repository at this point in the history
Close #8225

Co-authored-by: paw <[email protected]>
Co-authored-by: ivk <[email protected]>
  • Loading branch information
3 people committed Jan 9, 2025
1 parent e86dad5 commit e870acc
Show file tree
Hide file tree
Showing 12 changed files with 1,961 additions and 1,894 deletions.
2 changes: 2 additions & 0 deletions src/common/api/common/TutanotaConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,3 +1244,5 @@ export function asPublicKeyIdentifier(maybe: NumberString): PublicKeyIdentifierT
export const CLIENT_ONLY_CALENDAR_BIRTHDAYS_BASE_ID = "clientOnly_birthdays"
export const CLIENT_ONLY_CALENDARS: Map<Id, TranslationKey> = new Map([[CLIENT_ONLY_CALENDAR_BIRTHDAYS_BASE_ID, "birthdayCalendar_label"]])
export const DEFAULT_CLIENT_ONLY_CALENDAR_COLORS: Map<Id, string> = new Map([[CLIENT_ONLY_CALENDAR_BIRTHDAYS_BASE_ID, "FF9933"]])

export const MAX_LABELS_PER_MAIL = 10
1 change: 1 addition & 0 deletions src/common/misc/TranslationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1861,3 +1861,4 @@ export type TranslationKeyType =
| "emptyString_msg"
| "mailImportErrorServiceUnavailable_msg"
| "assignAdminRightsToLocallyAdministratedUserError_msg"
| "maximumLabelsPerMailReached_msg"
14 changes: 10 additions & 4 deletions src/mail-app/mail/model/MailModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ import {
partition,
promiseMap,
splitInChunks,
TypeRef,
} from "@tutao/tutanota-utils"
import {
ImportedMailTypeRef,
ImportMailStateTypeRef,
Mail,
MailboxGroupRoot,
MailboxProperties,
Expand All @@ -40,7 +37,7 @@ import {
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import m from "mithril"
import { createEntityUpdate, WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.js"
import { WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.js"
import { Notifications, NotificationType } from "../../../common/gui/Notifications.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
Expand Down Expand Up @@ -271,6 +268,15 @@ export class MailModel {
})
}

getLabelsForMails(mails: readonly Mail[]): ReadonlyMap<Id, ReadonlyArray<MailFolder>> {
const labelsForMails = new Map<Id, MailFolder[]>()
for (const mail of mails) {
labelsForMails.set(elementIdPart(mail._id), this.getLabelsForMail(mail))
}

return labelsForMails
}

/**
* @return labels that are currently applied to {@param mail}.
*/
Expand Down
72 changes: 61 additions & 11 deletions src/mail-app/mail/view/LabelsPopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,36 @@ import { focusNext, focusPrevious, Shortcut } from "../../../common/misc/KeyMana
import { BaseButton, BaseButtonAttrs } from "../../../common/gui/base/buttons/BaseButton.js"
import { PosRect, showDropdown } from "../../../common/gui/base/Dropdown.js"
import { MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { px, size } from "../../../common/gui/size.js"
import { size } from "../../../common/gui/size.js"
import { AllIcons, Icon, IconSize } from "../../../common/gui/base/Icon.js"
import { Icons } from "../../../common/gui/base/icons/Icons.js"
import { theme } from "../../../common/gui/theme.js"
import { Keys, TabIndex } from "../../../common/api/common/TutanotaConstants.js"
import { getElementId } from "../../../common/api/common/utils/EntityUtils.js"
import { Keys, MAX_LABELS_PER_MAIL, TabIndex } from "../../../common/api/common/TutanotaConstants.js"
import { elementIdPart, getElementId } from "../../../common/api/common/utils/EntityUtils.js"
import { getLabelColor } from "../../../common/gui/base/Label.js"
import { LabelState } from "../model/MailModel.js"
import { AriaRole } from "../../../common/gui/AriaUtils.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { noOp } from "@tutao/tutanota-utils"

/**
* Popup that displays assigned labels and allows changing them
*/
export class LabelsPopup implements ModalComponent {
private dom: HTMLElement | null = null
private isMaxLabelsReached: boolean

constructor(
private readonly sourceElement: HTMLElement,
private readonly origin: PosRect,
private readonly width: number,
private readonly labelsForMails: ReadonlyMap<Id, ReadonlyArray<MailFolder>>,
private readonly labels: { label: MailFolder; state: LabelState }[],
private readonly onLabelsApplied: (addedLabels: MailFolder[], removedLabels: MailFolder[]) => unknown,
) {
this.view = this.view.bind(this)
this.oncreate = this.oncreate.bind(this)
this.isMaxLabelsReached = this.checkIsMaxLabelsReached()
}

async hideAnimation(): Promise<void> {}
Expand Down Expand Up @@ -61,6 +65,9 @@ export class LabelsPopup implements ModalComponent {
this.labels.map((labelState) => {
const { label, state } = labelState
const color = theme.content_button
const canToggleLabel = state === LabelState.Applied || state === LabelState.AppliedToSome || !this.isMaxLabelsReached
const opacity = !canToggleLabel ? 0.5 : undefined

return m(
"label-item.flex.items-center.plr.state-bg.cursor-pointer",

Expand All @@ -69,24 +76,27 @@ export class LabelsPopup implements ModalComponent {
role: AriaRole.MenuItemCheckbox,
tabindex: TabIndex.Default,
"aria-checked": ariaCheckedForState(state),
onclick: () => this.toggleLabel(labelState),
"aria-disabled": !canToggleLabel,
onclick: canToggleLabel ? () => this.toggleLabel(labelState) : noOp,
},
[
m(Icon, {
icon: this.iconForState(state),
size: IconSize.Medium,
style: {
fill: getLabelColor(label.color),
opacity,
},
}),
m(".button-height.flex.items-center.ml.overflow-hidden", { style: { color } }, m(".text-ellipsis", label.name)),
m(".button-height.flex.items-center.ml.overflow-hidden", { style: { color, opacity } }, m(".text-ellipsis", label.name)),
],
)
}),
),
this.isMaxLabelsReached && m(".small.center.pb-s", lang.get("maximumLabelsPerMailReached_msg")),
m(BaseButton, {
label: "Apply",
text: "Apply",
text: lang.get("apply_action"),
class: "limit-width noselect bg-transparent button-height text-ellipsis content-accent-fg flex items-center plr-button button-content justify-center border-top state-bg",
onclick: () => {
this.applyLabels()
Expand Down Expand Up @@ -114,7 +124,34 @@ export class LabelsPopup implements ModalComponent {
}
}

private applyLabels() {
private checkIsMaxLabelsReached(): boolean {
const { addedLabels, removedLabels } = this.getSortedLabels()
if (addedLabels.length >= MAX_LABELS_PER_MAIL) {
return true
}

for (const [, labels] of this.labelsForMails) {
const labelsOnMail = new Set<Id>(labels.map((label) => elementIdPart(label._id)))

for (const label of removedLabels) {
labelsOnMail.delete(elementIdPart(label._id))
}
if (labelsOnMail.size >= MAX_LABELS_PER_MAIL) {
return true
}

for (const label of addedLabels) {
labelsOnMail.add(elementIdPart(label._id))
if (labelsOnMail.size >= MAX_LABELS_PER_MAIL) {
return true
}
}
}

return false
}

private getSortedLabels(): Record<"addedLabels" | "removedLabels", MailFolder[]> {
const removedLabels: MailFolder[] = []
const addedLabels: MailFolder[] = []
for (const { label, state } of this.labels) {
Expand All @@ -124,6 +161,11 @@ export class LabelsPopup implements ModalComponent {
removedLabels.push(label)
}
}
return { addedLabels, removedLabels }
}

private applyLabels() {
const { addedLabels, removedLabels } = this.getSortedLabels()
this.onLabelsApplied(addedLabels, removedLabels)
modal.remove(this)
}
Expand Down Expand Up @@ -199,11 +241,19 @@ export class LabelsPopup implements ModalComponent {
}

private toggleLabel(labelState: { label: MailFolder; state: LabelState }) {
if (labelState.state === LabelState.NotApplied || labelState.state === LabelState.AppliedToSome) {
labelState.state = LabelState.Applied
} else {
labelState.state = LabelState.NotApplied
switch (labelState.state) {
case LabelState.AppliedToSome:
labelState.state = this.isMaxLabelsReached ? LabelState.NotApplied : LabelState.Applied
break
case LabelState.NotApplied:
labelState.state = LabelState.Applied
break
case LabelState.Applied:
labelState.state = LabelState.NotApplied
break
}

this.isMaxLabelsReached = this.checkIsMaxLabelsReached()
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/mail-app/mail/view/MailView.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import m, { Children, Vnode } from "mithril"
import { ViewSlider } from "../../../common/gui/nav/ViewSlider.js"
import { ColumnType, ViewColumn } from "../../../common/gui/base/ViewColumn"
import { lang, TranslationText } from "../../../common/misc/LanguageViewModel"
import { lang } from "../../../common/misc/LanguageViewModel"
import { Dialog } from "../../../common/gui/base/Dialog"
import { FeatureType, HighestTierPlans, getMailFolderType, Keys, MailSetKind } from "../../../common/api/common/TutanotaConstants"
import { FeatureType, getMailFolderType, Keys, MailSetKind } from "../../../common/api/common/TutanotaConstants"
import { AppHeaderAttrs, Header } from "../../../common/gui/Header.js"
import { Mail, MailBox, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { isEmpty, noOp, ofClass } from "@tutao/tutanota-utils"
Expand Down Expand Up @@ -589,6 +589,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
document.activeElement as HTMLElement,
getMoveMailBounds(),
styles.isDesktopLayout() ? 300 : 200,
mailLocator.mailModel.getLabelsForMails(selectedMails),
mailLocator.mailModel.getLabelStatesForMails(selectedMails),
(addedLabels, removedLabels) => mailLocator.mailModel.applyLabels(selectedMails, addedLabels, removedLabels),
)
Expand Down
1 change: 1 addition & 0 deletions src/mail-app/mail/view/MailViewerHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
dom,
dom.getBoundingClientRect(),
styles.isDesktopLayout() ? 300 : 200,
viewModel.mailModel.getLabelsForMails([viewModel.mail]),
viewModel.mailModel.getLabelStatesForMails([viewModel.mail]),
(addedLabels, removedLabels) => viewModel.mailModel.applyLabels([viewModel.mail], addedLabels, removedLabels),
)
Expand Down
1 change: 1 addition & 0 deletions src/mail-app/mail/view/MailViewerToolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
dom,
dom.getBoundingClientRect(),
styles.isDesktopLayout() ? 300 : 200,
mailModel.getLabelsForMails(mails),
mailModel.getLabelStatesForMails(mails),
(addedLabels, removedLabels) => mailModel.applyLabels(mails, addedLabels, removedLabels),
)
Expand Down
1 change: 1 addition & 0 deletions src/mail-app/mail/view/MobileMailActionBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class MobileMailActionBar implements Component<MobileMailActionBarAttrs>
referenceDom,
referenceDom.getBoundingClientRect(),
this.dropdownWidth() ?? 200,
viewModel.mailModel.getLabelsForMails([viewModel.mail]),
viewModel.mailModel.getLabelStatesForMails([viewModel.mail]),
(addedLabels, removedLabels) => viewModel.mailModel.applyLabels([viewModel.mail], addedLabels, removedLabels),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class MobileMailMultiselectionActionBar {
referenceDom,
referenceDom.getBoundingClientRect(),
referenceDom.offsetWidth - DROPDOWN_MARGIN * 2,
mailModel.getLabelsForMails(mails),
mailModel.getLabelStatesForMails(mails),
(addedLabels, removedLabels) => mailModel.applyLabels(mails, addedLabels, removedLabels),
)
Expand Down
1 change: 1 addition & 0 deletions src/mail-app/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1881,5 +1881,6 @@ export default {
"localAdminGroups_label": "Local admin groups",
"localAdminGroup_label": "Local admin group",
"assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.",
"maximumLabelsPerMailReached_msg": "Maximum allowed labels per mail reached."
}
}
1 change: 1 addition & 0 deletions src/mail-app/translations/de_sie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1881,5 +1881,6 @@ export default {
"localAdminGroups_label": "Local admin groups",
"localAdminGroup_label": "Local admin group",
"assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.",
"maximumLabelsPerMailReached_msg": "Maximum allowed labels per mail reached."
}
}
Loading

0 comments on commit e870acc

Please sign in to comment.