Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit the number of labels that can be applied to a single email #8234

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = 5
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(getElementId(mail), this.getLabelsForMail(mail))
}

return labelsForMails
}

/**
* @return labels that are currently applied to {@param mail}.
*/
Expand Down
70 changes: 60 additions & 10 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 { Keys, MAX_LABELS_PER_MAIL, TabIndex } from "../../../common/api/common/TutanotaConstants.js"
import { 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()
hrb-hub marked this conversation as resolved.
Show resolved Hide resolved
if (addedLabels.length >= MAX_LABELS_PER_MAIL) {
return true
}

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

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

for (const label of addedLabels) {
labelsOnMail.add(getElementId(label))
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
Loading