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]>
  • Loading branch information
hrb-hub and paw-hub committed Jan 8, 2025
1 parent 7a88a5d commit 999ef3b
Show file tree
Hide file tree
Showing 12 changed files with 75 additions and 16 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 @@ -1863,3 +1863,4 @@ export type TranslationKeyType =
| "yourMessage_label"
| "you_label"
| "emptyString_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
56 changes: 49 additions & 7 deletions src/mail-app/mail/view/LabelsPopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ 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
Expand All @@ -25,6 +26,7 @@ export class LabelsPopup implements ModalComponent {
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,
) {
Expand Down Expand Up @@ -55,12 +57,17 @@ export class LabelsPopup implements ModalComponent {
}

view(): void | Children {
const isMaxLabelsReached = this.isMaxLabelsReached()

return m(".flex.col.elevated-bg.abs.dropdown-shadow.pt-s.border-radius", { tabindex: TabIndex.Programmatic, role: AriaRole.Menu }, [
m(
".pb-s.scroll",
this.labels.map((labelState) => {
const { label, state } = labelState
const color = theme.content_button
const canToggleLabel = state === LabelState.Applied || !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)),
],
)
}),
),
isMaxLabelsReached && m(".small.center.statusTextColor.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 isMaxLabelsReached(): 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
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 @@ -588,6 +588,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
3 changes: 2 additions & 1 deletion src/mail-app/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,7 @@ export default {
"yourCalendars_label": "Deine Kalender",
"yourFolders_action": "DEINE ORDNER",
"yourMessage_label": "Deine Nachricht",
"you_label": "Du"
"you_label": "Du",
"maximumLabelsPerMailReached_msg": "Maximum allowed labels per mail reached."
}
}
3 changes: 2 additions & 1 deletion src/mail-app/translations/de_sie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1882,6 +1882,7 @@ export default {
"yourCalendars_label": "Deine Kalender",
"yourFolders_action": "Ihre ORDNER",
"yourMessage_label": "Ihre Nachricht",
"you_label": "Sie"
"you_label": "Sie",
"maximumLabelsPerMailReached_msg": "Maximum allowed labels per mail reached."
}
}
3 changes: 2 additions & 1 deletion src/mail-app/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,7 @@ export default {
"yourCalendars_label": "Your calendars",
"yourFolders_action": "YOUR FOLDERS",
"yourMessage_label": "Your message",
"you_label": "You"
"you_label": "You",
"maximumLabelsPerMailReached_msg": "Maximum allowed labels per mail reached."
}
}

0 comments on commit 999ef3b

Please sign in to comment.