mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
fix minor issues with new MailSet implementation after review #7429
This commit is contained in:
parent
0d14919a20
commit
e0ce2ac678
11 changed files with 86 additions and 104 deletions
|
@ -396,7 +396,9 @@ export class CalendarSearchViewModel {
|
||||||
// note in case of refactor: the fact that the list updates the URL every time it changes
|
// note in case of refactor: the fact that the list updates the URL every time it changes
|
||||||
// its state is a major source of complexity and makes everything very order-dependent
|
// its state is a major source of complexity and makes everything very order-dependent
|
||||||
return new ListModel<SearchResultListEntry>({
|
return new ListModel<SearchResultListEntry>({
|
||||||
fetch: async (startId: Id, count: number) => {
|
fetch: async (lastFetchedEntity: SearchResultListEntry, count: number) => {
|
||||||
|
const startId = lastFetchedEntity == null ? GENERATED_MAX_ID : getElementId(lastFetchedEntity)
|
||||||
|
|
||||||
const lastResult = this._searchResult
|
const lastResult = this._searchResult
|
||||||
if (lastResult !== this._searchResult) {
|
if (lastResult !== this._searchResult) {
|
||||||
console.warn("got a fetch request for outdated results object, ignoring")
|
console.warn("got a fetch request for outdated results object, ignoring")
|
||||||
|
|
|
@ -378,10 +378,6 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const range = await this.storage.getRangeForList(typeRef, listId)
|
const range = await this.storage.getRangeForList(typeRef, listId)
|
||||||
if (getTypeId(typeRef) == "tutanota/MailSetEntry") {
|
|
||||||
console.log(getTypeId(typeRef), listId, start, count, reverse)
|
|
||||||
console.log("range", range)
|
|
||||||
}
|
|
||||||
if (range == null) {
|
if (range == null) {
|
||||||
await this.populateNewListWithRange(typeRef, listId, start, count, reverse)
|
await this.populateNewListWithRange(typeRef, listId, start, count, reverse)
|
||||||
} else if (isStartIdWithinRange(range, start, typeModel)) {
|
} else if (isStartIdWithinRange(range, start, typeModel)) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { elementIdPart, GENERATED_MAX_ID, getElementId, isSameId, ListElement } from "../api/common/utils/EntityUtils.js"
|
import { elementIdPart, getElementId, isSameId, ListElement } from "../api/common/utils/EntityUtils.js"
|
||||||
import { ListLoadingState, ListState } from "../gui/base/List.js"
|
import { ListLoadingState, ListState } from "../gui/base/List.js"
|
||||||
|
|
||||||
import { OperationType } from "../api/common/TutanotaConstants.js"
|
import { OperationType } from "../api/common/TutanotaConstants.js"
|
||||||
|
@ -29,14 +29,7 @@ export type ListModelConfig<ListElementType> = {
|
||||||
/**
|
/**
|
||||||
* Get the given number of entities starting after the given id. May return more elements than requested, e.g. if all elements are available on first fetch.
|
* Get the given number of entities starting after the given id. May return more elements than requested, e.g. if all elements are available on first fetch.
|
||||||
*/
|
*/
|
||||||
fetch(startId: Id, count: number): Promise<ListFetchResult<ListElementType>>
|
fetch(lastFetchedEntity: ListElementType | null | undefined, count: number): Promise<ListFetchResult<ListElementType>>
|
||||||
|
|
||||||
/**
|
|
||||||
* some lists load elements via an index indirection,
|
|
||||||
* so they use a different list to load than is actually displayed.
|
|
||||||
* in fact, the displayed entities might not even be stored in the same list
|
|
||||||
*/
|
|
||||||
getLoadIdForElement?: (element: ListElementType | null | undefined) => Id
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns null if the given element could not be loaded
|
* Returns null if the given element could not be loaded
|
||||||
|
@ -58,14 +51,7 @@ type PrivateListState<ElementType> = Omit<ListState<ElementType>, "items" | "act
|
||||||
|
|
||||||
/** ListModel that does the state upkeep for the List, including loading state, loaded items, selection and filters*/
|
/** ListModel that does the state upkeep for the List, including loading state, loaded items, selection and filters*/
|
||||||
export class ListModel<ElementType extends ListElement> {
|
export class ListModel<ElementType extends ListElement> {
|
||||||
private readonly config: Required<ListModelConfig<ElementType>>
|
constructor(private readonly config: ListModelConfig<ElementType>) {}
|
||||||
|
|
||||||
constructor(config: ListModelConfig<ElementType>) {
|
|
||||||
if (config.getLoadIdForElement == null) {
|
|
||||||
config.getLoadIdForElement = (element: ElementType) => (element != null ? getElementId(element) : GENERATED_MAX_ID)
|
|
||||||
}
|
|
||||||
this.config = config as Required<ListModelConfig<ElementType>>
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadState: "created" | "initialized" = "created"
|
private loadState: "created" | "initialized" = "created"
|
||||||
private loading: Promise<unknown> = Promise.resolve()
|
private loading: Promise<unknown> = Promise.resolve()
|
||||||
|
@ -163,10 +149,9 @@ export class ListModel<ElementType extends ListElement> {
|
||||||
private async doLoad() {
|
private async doLoad() {
|
||||||
this.updateLoadingStatus(ListLoadingState.Loading)
|
this.updateLoadingStatus(ListLoadingState.Loading)
|
||||||
this.loading = Promise.resolve().then(async () => {
|
this.loading = Promise.resolve().then(async () => {
|
||||||
const lastItem = last(this.rawState.unfilteredItems)
|
const lastFetchedItem = last(this.rawState.unfilteredItems)
|
||||||
try {
|
try {
|
||||||
const idToLoadFrom = this.config.getLoadIdForElement(lastItem)
|
const { items: newItems, complete } = await this.config.fetch(lastFetchedItem, PageSize)
|
||||||
const { items: newItems, complete } = await this.config.fetch(idToLoadFrom, PageSize)
|
|
||||||
// if the loading was cancelled in the meantime, don't insert anything so that it's not confusing
|
// if the loading was cancelled in the meantime, don't insert anything so that it's not confusing
|
||||||
if (this.state.loadingStatus === ListLoadingState.ConnectionLost) {
|
if (this.state.loadingStatus === ListLoadingState.ConnectionLost) {
|
||||||
return
|
return
|
||||||
|
@ -206,10 +191,6 @@ export class ListModel<ElementType extends ListElement> {
|
||||||
this.updateState({ filteredItems: newFilteredItems, selectedItems: newSelectedItems })
|
this.updateState({ filteredItems: newFilteredItems, selectedItems: newSelectedItems })
|
||||||
}
|
}
|
||||||
|
|
||||||
isFiltered(): boolean {
|
|
||||||
return this.filter != null
|
|
||||||
}
|
|
||||||
|
|
||||||
async entityEventReceived(listId: Id, elementId: Id, operation: OperationType): Promise<void> {
|
async entityEventReceived(listId: Id, elementId: Id, operation: OperationType): Promise<void> {
|
||||||
if (operation === OperationType.CREATE || operation === OperationType.UPDATE) {
|
if (operation === OperationType.CREATE || operation === OperationType.UPDATE) {
|
||||||
// load the element without range checks for now
|
// load the element without range checks for now
|
||||||
|
@ -251,7 +232,7 @@ export class ListModel<ElementType extends ListElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLoadedEntity(entity: ElementType) {
|
private updateLoadedEntity(entity: ElementType) {
|
||||||
// We cannot use binary search here because the sort order of items can change based on the entity update and we need to find the position of the
|
// We cannot use binary search here because the sort order of items can change based on the entity update, and we need to find the position of the
|
||||||
// old entity by id in order to remove it.
|
// old entity by id in order to remove it.
|
||||||
|
|
||||||
// Since every element id is unique and there's no scenario where the same item appears twice but in different lists, we can safely sort just
|
// Since every element id is unique and there's no scenario where the same item appears twice but in different lists, we can safely sort just
|
||||||
|
@ -591,7 +572,7 @@ export class ListModel<ElementType extends ListElement> {
|
||||||
|
|
||||||
stopLoading() {
|
stopLoading() {
|
||||||
if (this.state.loadingStatus === ListLoadingState.Loading) {
|
if (this.state.loadingStatus === ListLoadingState.Loading) {
|
||||||
// We can't really cancel ongoing requests but we can prevent more requests from happening
|
// We can't really cancel ongoing requests, but we can prevent more requests from happening
|
||||||
this.updateState({ loadingStatus: ListLoadingState.ConnectionLost })
|
this.updateState({ loadingStatus: ListLoadingState.ConnectionLost })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Icons } from "../gui/base/icons/Icons.js"
|
||||||
import { BootIcons } from "../gui/base/icons/BootIcons.js"
|
import { BootIcons } from "../gui/base/icons/BootIcons.js"
|
||||||
|
|
||||||
import { compareGroupInfos } from "../api/common/utils/GroupUtils.js"
|
import { compareGroupInfos } from "../api/common/utils/GroupUtils.js"
|
||||||
import { elementIdPart, GENERATED_MAX_ID } from "../api/common/utils/EntityUtils.js"
|
import { elementIdPart } from "../api/common/utils/EntityUtils.js"
|
||||||
import { ListColumnWrapper } from "../gui/ListColumnWrapper.js"
|
import { ListColumnWrapper } from "../gui/ListColumnWrapper.js"
|
||||||
import { assertMainOrNode } from "../api/common/Env.js"
|
import { assertMainOrNode } from "../api/common/Env.js"
|
||||||
import { locator } from "../api/main/CommonLocator.js"
|
import { locator } from "../api/main/CommonLocator.js"
|
||||||
|
@ -217,10 +217,7 @@ export class UserListView implements UpdatableSettingsViewer {
|
||||||
private makeListModel(): ListModel<GroupInfo> {
|
private makeListModel(): ListModel<GroupInfo> {
|
||||||
const listModel = new ListModel<GroupInfo>({
|
const listModel = new ListModel<GroupInfo>({
|
||||||
sortCompare: compareGroupInfos,
|
sortCompare: compareGroupInfos,
|
||||||
fetch: async (startId) => {
|
fetch: async (_lastFetchedEntity) => {
|
||||||
if (startId !== GENERATED_MAX_ID) {
|
|
||||||
throw new Error("fetch user group infos called for specific start id")
|
|
||||||
}
|
|
||||||
await this.loadAdmins()
|
await this.loadAdmins()
|
||||||
const listId = await this.listId.getAsync()
|
const listId = await this.listId.getAsync()
|
||||||
const allUserGroupInfos = await locator.entityClient.loadAll(GroupInfoTypeRef, listId)
|
const allUserGroupInfos = await locator.entityClient.loadAll(GroupInfoTypeRef, listId)
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.
|
||||||
|
|
||||||
export interface MailFolderViewAttrs {
|
export interface MailFolderViewAttrs {
|
||||||
mailboxDetail: MailboxDetail
|
mailboxDetail: MailboxDetail
|
||||||
mailFolderToSelectedMail: ReadonlyMap<MailFolder, Id>
|
mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id>
|
||||||
onFolderClick: (folder: MailFolder) => unknown
|
onFolderClick: (folder: MailFolder) => unknown
|
||||||
onFolderDrop: (mailId: string, folder: MailFolder) => unknown
|
onFolderDrop: (mailId: string, folder: MailFolder) => unknown
|
||||||
expandedFolders: ReadonlySet<Id>
|
expandedFolders: ReadonlySet<Id>
|
||||||
|
@ -93,12 +93,12 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
||||||
if (attrs.inEditMode) {
|
if (attrs.inEditMode) {
|
||||||
return m.route.get()
|
return m.route.get()
|
||||||
} else {
|
} else {
|
||||||
const mailId = attrs.mailFolderToSelectedMail.get(system.folder)
|
const folderElementId = getElementId(system.folder)
|
||||||
const folderId = getElementId(system.folder)
|
const mailId = attrs.mailFolderElementIdToSelectedMailId.get(folderElementId)
|
||||||
if (mailId) {
|
if (mailId) {
|
||||||
return `${MAIL_PREFIX}/${folderId}/${mailId}`
|
return `${MAIL_PREFIX}/${folderElementId}/${mailId}`
|
||||||
} else {
|
} else {
|
||||||
return `${MAIL_PREFIX}/${folderId}`
|
return `${MAIL_PREFIX}/${folderElementId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -593,7 +593,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
||||||
return m(MailFoldersView, {
|
return m(MailFoldersView, {
|
||||||
mailboxDetail,
|
mailboxDetail,
|
||||||
expandedFolders: this.expandedState,
|
expandedFolders: this.expandedState,
|
||||||
mailFolderToSelectedMail: this.mailViewModel.getMailFolderToSelectedMail(),
|
mailFolderElementIdToSelectedMailId: this.mailViewModel.getMailFolderToSelectedMail(),
|
||||||
onFolderClick: () => {
|
onFolderClick: () => {
|
||||||
if (!inEditMode) {
|
if (!inEditMode) {
|
||||||
this.viewSlider.focus(this.listColumn)
|
this.viewSlider.focus(this.listColumn)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { ListModel, ListModelConfig } from "../../../common/misc/ListModel.js"
|
import { ListModel } from "../../../common/misc/ListModel.js"
|
||||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||||
import { Mail, MailFolder, MailSetEntry, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
import { Mail, MailFolder, MailSetEntry, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||||
import {
|
import {
|
||||||
constructMailSetEntryId,
|
constructMailSetEntryId,
|
||||||
CUSTOM_MAX_ID,
|
CUSTOM_MAX_ID,
|
||||||
customIdToUint8array,
|
|
||||||
elementIdPart,
|
elementIdPart,
|
||||||
firstBiggerThanSecond,
|
firstBiggerThanSecond,
|
||||||
|
GENERATED_MAX_ID,
|
||||||
getElementId,
|
getElementId,
|
||||||
isSameId,
|
isSameId,
|
||||||
listIdPart,
|
listIdPart,
|
||||||
|
@ -15,7 +15,6 @@ import {
|
||||||
} from "../../../common/api/common/utils/EntityUtils.js"
|
} from "../../../common/api/common/utils/EntityUtils.js"
|
||||||
import {
|
import {
|
||||||
assertNotNull,
|
assertNotNull,
|
||||||
compare,
|
|
||||||
count,
|
count,
|
||||||
debounce,
|
debounce,
|
||||||
groupByAndMap,
|
groupByAndMap,
|
||||||
|
@ -51,11 +50,23 @@ export interface MailOpenedListener {
|
||||||
onEmailOpened(mail: Mail): unknown
|
onEmailOpened(mail: Mail): unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/** sort mail set mails in descending order according to their receivedDate, not their element id */
|
/** sort mail set mails in descending order (**reversed**: newest to oldest) according to their receivedDate, not their elementId */
|
||||||
function sortCompareMailSetMails(firstMail: Mail, secondMail: Mail): number {
|
function sortCompareMailSetMails(firstMail: Mail, secondMail: Mail): number {
|
||||||
const firstMailEntryId = constructMailSetEntryId(firstMail.receivedDate, getElementId(firstMail))
|
const firstMailReceivedTimestamp = firstMail.receivedDate.getTime()
|
||||||
const secondMailEntryId = constructMailSetEntryId(secondMail.receivedDate, getElementId(secondMail))
|
const secondMailReceivedTimestamp = secondMail.receivedDate.getTime()
|
||||||
return compare(customIdToUint8array(secondMailEntryId), customIdToUint8array(firstMailEntryId))
|
if (firstMailReceivedTimestamp > secondMailReceivedTimestamp) {
|
||||||
|
return -1
|
||||||
|
} else if (secondMailReceivedTimestamp < firstMailReceivedTimestamp) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
if (firstBiggerThanSecond(getElementId(firstMail), getElementId(secondMail))) {
|
||||||
|
return -1
|
||||||
|
} else if (firstBiggerThanSecond(getElementId(secondMail), getElementId(firstMail))) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ViewModel for the overall mail view. */
|
/** ViewModel for the overall mail view. */
|
||||||
|
@ -63,7 +74,7 @@ export class MailViewModel {
|
||||||
private _folder: MailFolder | null = null
|
private _folder: MailFolder | null = null
|
||||||
/** id of the mail we are trying to load based on the URL */
|
/** id of the mail we are trying to load based on the URL */
|
||||||
private targetMailId: Id | null = null
|
private targetMailId: Id | null = null
|
||||||
/** needed to prevent parallel target loads*/
|
/** needed to prevent parallel target loads */
|
||||||
private loadingToTargetId: Id | null = null
|
private loadingToTargetId: Id | null = null
|
||||||
private conversationViewModel: ConversationViewModel | null = null
|
private conversationViewModel: ConversationViewModel | null = null
|
||||||
private _filterType: MailFilterType | null = null
|
private _filterType: MailFilterType | null = null
|
||||||
|
@ -72,7 +83,7 @@ export class MailViewModel {
|
||||||
* We remember the last URL used for each folder so if we switch between folders we can keep the selected mail.
|
* We remember the last URL used for each folder so if we switch between folders we can keep the selected mail.
|
||||||
* There's a similar (but different) hacky mechanism where we store last URL but per each top-level view: navButtonRoutes. This one is per folder.
|
* There's a similar (but different) hacky mechanism where we store last URL but per each top-level view: navButtonRoutes. This one is per folder.
|
||||||
*/
|
*/
|
||||||
private mailFolderToSelectedMail: ReadonlyMap<MailFolder, Id> = new Map()
|
private mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id> = new Map()
|
||||||
private listStreamSubscription: Stream<unknown> | null = null
|
private listStreamSubscription: Stream<unknown> | null = null
|
||||||
private conversationPref: boolean = false
|
private conversationPref: boolean = false
|
||||||
|
|
||||||
|
@ -117,7 +128,7 @@ export class MailViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// important to set it early enough because setting listId will trigger URL update.
|
// important to set it early enough because setting listId will trigger URL update.
|
||||||
// if we don't set this one before setListId, url update will cause this function to be called again but without target mail and we will lose the
|
// if we don't set this one before setListId, url update will cause this function to be called again but without target mail, and we will lose the
|
||||||
// target URL
|
// target URL
|
||||||
this.targetMailId = typeof mailId === "string" ? mailId : null
|
this.targetMailId = typeof mailId === "string" ? mailId : null
|
||||||
|
|
||||||
|
@ -135,9 +146,9 @@ export class MailViewModel {
|
||||||
|
|
||||||
await this.setListId(folderToUse)
|
await this.setListId(folderToUse)
|
||||||
|
|
||||||
// if there is a target id and we are not loading for this id already then start loading towards that id
|
// if there is a target id, and we are not loading for this id already then start loading towards that id
|
||||||
if (this.targetMailId && this.targetMailId != this.loadingToTargetId) {
|
if (this.targetMailId && this.targetMailId != this.loadingToTargetId) {
|
||||||
this.mailFolderToSelectedMail = mapWith(this.mailFolderToSelectedMail, folderToUse, this.targetMailId)
|
this.mailFolderElementIdToSelectedMailId = mapWith(this.mailFolderElementIdToSelectedMailId, getElementId(folderToUse), this.targetMailId)
|
||||||
try {
|
try {
|
||||||
this.loadingToTargetId = this.targetMailId
|
this.loadingToTargetId = this.targetMailId
|
||||||
await this.loadAndSelectMail(folderToUse, this.targetMailId)
|
await this.loadAndSelectMail(folderToUse, this.targetMailId)
|
||||||
|
@ -198,8 +209,8 @@ export class MailViewModel {
|
||||||
return this._folder ? this.listModelForFolder(getElementId(this._folder)) : null
|
return this._folder ? this.listModelForFolder(getElementId(this._folder)) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
getMailFolderToSelectedMail(): ReadonlyMap<MailFolder, Id> {
|
getMailFolderToSelectedMail(): ReadonlyMap<Id, Id> {
|
||||||
return this.mailFolderToSelectedMail
|
return this.mailFolderElementIdToSelectedMailId
|
||||||
}
|
}
|
||||||
|
|
||||||
getFolder(): MailFolder | null {
|
getFolder(): MailFolder | null {
|
||||||
|
@ -224,14 +235,19 @@ export class MailViewModel {
|
||||||
return this.conversationViewModel
|
return this.conversationViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
private listModelForFolder = memoized((folderId: Id) => {
|
private listModelForFolder = memoized((_folderId: Id) => {
|
||||||
const customGetLoadIdForElement: ListModelConfig<Mail>["getLoadIdForElement"] = (mail) =>
|
|
||||||
mail == null ? CUSTOM_MAX_ID : constructMailSetEntryId(mail.receivedDate, getElementId(mail))
|
|
||||||
|
|
||||||
return new ListModel<Mail>({
|
return new ListModel<Mail>({
|
||||||
getLoadIdForElement: this._folder?.isMailSet ? customGetLoadIdForElement : undefined,
|
fetch: async (lastFetchedMail, count) => {
|
||||||
fetch: async (startId, count) => {
|
|
||||||
const folder = assertNotNull(this._folder)
|
const folder = assertNotNull(this._folder)
|
||||||
|
|
||||||
|
// in case the folder is a new MailSet folder we need to load via the MailSetEntry index indirection
|
||||||
|
let startId: Id
|
||||||
|
if (folder.isMailSet) {
|
||||||
|
startId = lastFetchedMail == null ? CUSTOM_MAX_ID : constructMailSetEntryId(lastFetchedMail.receivedDate, getElementId(lastFetchedMail))
|
||||||
|
} else {
|
||||||
|
startId = lastFetchedMail == null ? GENERATED_MAX_ID : getElementId(lastFetchedMail)
|
||||||
|
}
|
||||||
|
|
||||||
const { complete, items } = await this.loadMailRange(folder, startId, count)
|
const { complete, items } = await this.loadMailRange(folder, startId, count)
|
||||||
if (complete) {
|
if (complete) {
|
||||||
this.fixCounterIfNeeded(folder, [])
|
this.fixCounterIfNeeded(folder, [])
|
||||||
|
@ -239,7 +255,6 @@ export class MailViewModel {
|
||||||
return { complete, items }
|
return { complete, items }
|
||||||
},
|
},
|
||||||
loadSingle: async (listId: Id, elementId: Id): Promise<Mail | null> => {
|
loadSingle: async (listId: Id, elementId: Id): Promise<Mail | null> => {
|
||||||
// await this.entityClient.load(MailSetEntryTypeRef, )
|
|
||||||
return this.entityClient.load(MailTypeRef, [listId, elementId])
|
return this.entityClient.load(MailTypeRef, [listId, elementId])
|
||||||
},
|
},
|
||||||
sortCompare: (firstMail, secondMail): number =>
|
sortCompare: (firstMail, secondMail): number =>
|
||||||
|
@ -284,7 +299,11 @@ export class MailViewModel {
|
||||||
if (!newState.inMultiselect && newState.selectedItems.size === 1) {
|
if (!newState.inMultiselect && newState.selectedItems.size === 1) {
|
||||||
const mail = this.listModel!.getSelectedAsArray()[0]
|
const mail = this.listModel!.getSelectedAsArray()[0]
|
||||||
if (!this.conversationViewModel || !isSameId(this.conversationViewModel?.primaryMail._id, mail._id)) {
|
if (!this.conversationViewModel || !isSameId(this.conversationViewModel?.primaryMail._id, mail._id)) {
|
||||||
this.mailFolderToSelectedMail = mapWith(this.mailFolderToSelectedMail, assertNotNull(this.getFolder()), getElementId(mail))
|
this.mailFolderElementIdToSelectedMailId = mapWith(
|
||||||
|
this.mailFolderElementIdToSelectedMailId,
|
||||||
|
getElementId(assertNotNull(this.getFolder())),
|
||||||
|
getElementId(mail),
|
||||||
|
)
|
||||||
|
|
||||||
this.createConversationViewModel({
|
this.createConversationViewModel({
|
||||||
mail,
|
mail,
|
||||||
|
@ -295,7 +314,7 @@ export class MailViewModel {
|
||||||
} else {
|
} else {
|
||||||
this.conversationViewModel?.dispose()
|
this.conversationViewModel?.dispose()
|
||||||
this.conversationViewModel = null
|
this.conversationViewModel = null
|
||||||
this.mailFolderToSelectedMail = mapWithout(this.mailFolderToSelectedMail, assertNotNull(this.getFolder()))
|
this.mailFolderElementIdToSelectedMailId = mapWithout(this.mailFolderElementIdToSelectedMailId, getElementId(assertNotNull(this.getFolder())))
|
||||||
}
|
}
|
||||||
this.updateUrl()
|
this.updateUrl()
|
||||||
this.updateUi()
|
this.updateUi()
|
||||||
|
@ -304,7 +323,7 @@ export class MailViewModel {
|
||||||
private updateUrl() {
|
private updateUrl() {
|
||||||
const folder = this._folder
|
const folder = this._folder
|
||||||
const folderId = folder ? getElementId(folder) : null
|
const folderId = folder ? getElementId(folder) : null
|
||||||
const mailId = this.targetMailId ?? (folder ? this.getMailFolderToSelectedMail().get(folder) : null)
|
const mailId = this.targetMailId ?? (folderId ? this.getMailFolderToSelectedMail().get(folderId) : null)
|
||||||
if (mailId != null) {
|
if (mailId != null) {
|
||||||
this.router.routeTo("/mail/:folderId/:mailId", { folderId, mailId })
|
this.router.routeTo("/mail/:folderId/:mailId", { folderId, mailId })
|
||||||
} else {
|
} else {
|
||||||
|
@ -370,7 +389,6 @@ export class MailViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMailSetMailRange(folder: MailFolder, startId: string, count: number): Promise<ListFetchResult<Mail>> {
|
private async loadMailSetMailRange(folder: MailFolder, startId: string, count: number): Promise<ListFetchResult<Mail>> {
|
||||||
console.log("range request for ", folder.entries, startId, count)
|
|
||||||
try {
|
try {
|
||||||
const loadMailSetEntries = () => this.entityClient.loadRange(MailSetEntryTypeRef, folder.entries, startId, count, true)
|
const loadMailSetEntries = () => this.entityClient.loadRange(MailSetEntryTypeRef, folder.entries, startId, count, true)
|
||||||
const loadMails = (listId: Id, mailIds: Array<Id>) => this.entityClient.loadMultiple(MailTypeRef, listId, mailIds)
|
const loadMails = (listId: Id, mailIds: Array<Id>) => this.entityClient.loadMultiple(MailTypeRef, listId, mailIds)
|
||||||
|
@ -379,7 +397,7 @@ export class MailViewModel {
|
||||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||||
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
||||||
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
||||||
// applies, the email is moved out of the inbox and we don't return it here.
|
// applies, the email is moved out of the inbox, and we don't return it here.
|
||||||
if (mailboxDetail) {
|
if (mailboxDetail) {
|
||||||
const mailsToKeepInInbox = await promiseFilter(mails, async (mail) => {
|
const mailsToKeepInInbox = await promiseFilter(mails, async (mail) => {
|
||||||
const wasMatched = await this.inboxRuleHandler.findAndApplyMatchingRule(mailboxDetail, mail, true)
|
const wasMatched = await this.inboxRuleHandler.findAndApplyMatchingRule(mailboxDetail, mail, true)
|
||||||
|
@ -391,7 +409,7 @@ export class MailViewModel {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The way the cache works is that it tries to fulfill the API contract of returning as many items as requested as long as it can.
|
// The way the cache works is that it tries to fulfill the API contract of returning as many items as requested as long as it can.
|
||||||
// This is problematic for offline where we might not have the full page of emails loaded (e.g. we delete part as it's too old or we move emails
|
// This is problematic for offline where we might not have the full page of emails loaded (e.g. we delete part as it's too old, or we move emails
|
||||||
// around). Because of that cache will try to load additional items from the server in order to return `count` items. If it fails to load them,
|
// around). Because of that cache will try to load additional items from the server in order to return `count` items. If it fails to load them,
|
||||||
// it will not return anything and instead will throw an error.
|
// it will not return anything and instead will throw an error.
|
||||||
// This is generally fine but in case of offline we want to display everything that we have cached. For that we fetch directly from the cache,
|
// This is generally fine but in case of offline we want to display everything that we have cached. For that we fetch directly from the cache,
|
||||||
|
@ -436,7 +454,7 @@ export class MailViewModel {
|
||||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||||
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
// For inbox rules there are two points where we might want to apply them. The first one is MailModel which applied inbox rules as they are received
|
||||||
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
// in real time. The second one is here, when we load emails in inbox. If they are unread we want to apply inbox rules to them. If inbox rule
|
||||||
// applies, the email is moved out of the inbox and we don't return it here.
|
// applies, the email is moved out of the inbox, and we don't return it here.
|
||||||
if (mailboxDetail) {
|
if (mailboxDetail) {
|
||||||
const mailsToKeepInInbox = await promiseFilter(items, async (mail) => {
|
const mailsToKeepInInbox = await promiseFilter(items, async (mail) => {
|
||||||
const wasMatched = await this.inboxRuleHandler.findAndApplyMatchingRule(mailboxDetail, mail, true)
|
const wasMatched = await this.inboxRuleHandler.findAndApplyMatchingRule(mailboxDetail, mail, true)
|
||||||
|
@ -448,7 +466,7 @@ export class MailViewModel {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The way the cache works is that it tries to fulfill the API contract of returning as many items as requested as long as it can.
|
// The way the cache works is that it tries to fulfill the API contract of returning as many items as requested as long as it can.
|
||||||
// This is problematic for offline where we might not have the full page of emails loaded (e.g. we delete part as it's too old or we move emails
|
// This is problematic for offline where we might not have the full page of emails loaded (e.g. we delete part as it's too old, or we move emails
|
||||||
// around). Because of that cache will try to load additional items from the server in order to return `count` items. If it fails to load them,
|
// around). Because of that cache will try to load additional items from the server in order to return `count` items. If it fails to load them,
|
||||||
// it will not return anything and instead will throw an error.
|
// it will not return anything and instead will throw an error.
|
||||||
// This is generally fine but in case of offline we want to display everything that we have cached. For that we fetch directly from the cache,
|
// This is generally fine but in case of offline we want to display everything that we have cached. For that we fetch directly from the cache,
|
||||||
|
@ -467,7 +485,7 @@ export class MailViewModel {
|
||||||
async switchToFolder(folderType: Omit<MailSetKind, MailSetKind.CUSTOM>): Promise<void> {
|
async switchToFolder(folderType: Omit<MailSetKind, MailSetKind.CUSTOM>): Promise<void> {
|
||||||
const mailboxDetail = assertNotNull(await this.getMailboxDetails())
|
const mailboxDetail = assertNotNull(await this.getMailboxDetails())
|
||||||
const folder = assertSystemFolderOfType(mailboxDetail.folders, folderType)
|
const folder = assertSystemFolderOfType(mailboxDetail.folders, folderType)
|
||||||
await this.showMail(folder, this.mailFolderToSelectedMail.get(folder))
|
await this.showMail(folder, this.mailFolderElementIdToSelectedMailId.get(getElementId(folder)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMailboxDetails(): Promise<MailboxDetail> {
|
async getMailboxDetails(): Promise<MailboxDetail> {
|
||||||
|
|
|
@ -685,7 +685,9 @@ export class SearchViewModel {
|
||||||
// note in case of refactor: the fact that the list updates the URL every time it changes
|
// note in case of refactor: the fact that the list updates the URL every time it changes
|
||||||
// its state is a major source of complexity and makes everything very order-dependent
|
// its state is a major source of complexity and makes everything very order-dependent
|
||||||
return new ListModel<SearchResultListEntry>({
|
return new ListModel<SearchResultListEntry>({
|
||||||
fetch: async (startId: Id, count: number) => {
|
fetch: async (lastFetchedEntity: SearchResultListEntry, count: number) => {
|
||||||
|
const startId = lastFetchedEntity == null ? GENERATED_MAX_ID : getElementId(lastFetchedEntity)
|
||||||
|
|
||||||
const lastResult = this._searchResult
|
const lastResult = this._searchResult
|
||||||
if (lastResult !== this._searchResult) {
|
if (lastResult !== this._searchResult) {
|
||||||
console.warn("got a fetch request for outdated results object, ignoring")
|
console.warn("got a fetch request for outdated results object, ignoring")
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { lang } from "../../common/misc/LanguageViewModel"
|
||||||
|
|
||||||
import { size } from "../../common/gui/size"
|
import { size } from "../../common/gui/size"
|
||||||
import { EntityClient } from "../../common/api/common/EntityClient"
|
import { EntityClient } from "../../common/api/common/EntityClient"
|
||||||
import { GENERATED_MAX_ID, isSameId, listIdPart } from "../../common/api/common/utils/EntityUtils"
|
import { isSameId, listIdPart } from "../../common/api/common/utils/EntityUtils"
|
||||||
import { hasCapabilityOnGroup } from "../../common/sharing/GroupUtils"
|
import { hasCapabilityOnGroup } from "../../common/sharing/GroupUtils"
|
||||||
import { ShareCapability } from "../../common/api/common/TutanotaConstants"
|
import { ShareCapability } from "../../common/api/common/TutanotaConstants"
|
||||||
import type { LoginController } from "../../common/api/main/LoginController"
|
import type { LoginController } from "../../common/api/main/LoginController"
|
||||||
|
@ -83,19 +83,14 @@ export class KnowledgeBaseListView implements UpdatableSettingsViewer {
|
||||||
private makeListModel() {
|
private makeListModel() {
|
||||||
const listModel = new ListModel<KnowledgeBaseEntry>({
|
const listModel = new ListModel<KnowledgeBaseEntry>({
|
||||||
sortCompare: (a: KnowledgeBaseEntry, b: KnowledgeBaseEntry) => {
|
sortCompare: (a: KnowledgeBaseEntry, b: KnowledgeBaseEntry) => {
|
||||||
var titleA = a.title.toUpperCase()
|
const titleA = a.title.toUpperCase()
|
||||||
var titleB = b.title.toUpperCase()
|
const titleB = b.title.toUpperCase()
|
||||||
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0
|
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0
|
||||||
},
|
},
|
||||||
fetch: async (startId, count) => {
|
fetch: async (_lastFetchedEntity, _count) => {
|
||||||
// fetch works like in ContactListView, because we have a custom sort order there too
|
// load all entries at once to apply custom sort order
|
||||||
if (startId === GENERATED_MAX_ID) {
|
const allEntries = await this.entityClient.loadAll(KnowledgeBaseEntryTypeRef, this.getListId())
|
||||||
// load all entries at once to apply custom sort order
|
return { items: allEntries, complete: true }
|
||||||
const allEntries = await this.entityClient.loadAll(KnowledgeBaseEntryTypeRef, this.getListId())
|
|
||||||
return { items: allEntries, complete: true }
|
|
||||||
} else {
|
|
||||||
throw new Error("fetch knowledgeBase entry called for specific start id")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
loadSingle: (_listId: Id, elementId: Id) => {
|
loadSingle: (_listId: Id, elementId: Id) => {
|
||||||
return this.entityClient.load<KnowledgeBaseEntry>(KnowledgeBaseEntryTypeRef, [this.getListId(), elementId])
|
return this.entityClient.load<KnowledgeBaseEntry>(KnowledgeBaseEntryTypeRef, [this.getListId(), elementId])
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { showTemplateEditor } from "./TemplateEditor"
|
||||||
import type { EmailTemplate } from "../../common/api/entities/tutanota/TypeRefs.js"
|
import type { EmailTemplate } from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||||
import { EmailTemplateTypeRef } from "../../common/api/entities/tutanota/TypeRefs.js"
|
import { EmailTemplateTypeRef } from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||||
import { EntityClient } from "../../common/api/common/EntityClient"
|
import { EntityClient } from "../../common/api/common/EntityClient"
|
||||||
import { GENERATED_MAX_ID, isSameId } from "../../common/api/common/utils/EntityUtils"
|
import { isSameId } from "../../common/api/common/utils/EntityUtils"
|
||||||
import { searchInTemplates, TEMPLATE_SHORTCUT_PREFIX } from "../templates/model/TemplatePopupModel"
|
import { searchInTemplates, TEMPLATE_SHORTCUT_PREFIX } from "../templates/model/TemplatePopupModel"
|
||||||
import { hasCapabilityOnGroup } from "../../common/sharing/GroupUtils"
|
import { hasCapabilityOnGroup } from "../../common/sharing/GroupUtils"
|
||||||
import { ShareCapability } from "../../common/api/common/TutanotaConstants"
|
import { ShareCapability } from "../../common/api/common/TutanotaConstants"
|
||||||
|
@ -86,15 +86,10 @@ export class TemplateListView implements UpdatableSettingsViewer {
|
||||||
const titleB = b.title.toUpperCase()
|
const titleB = b.title.toUpperCase()
|
||||||
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0
|
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0
|
||||||
},
|
},
|
||||||
fetch: async (startId, count) => {
|
fetch: async (_lastFetchedEntity, _count) => {
|
||||||
// fetch works like in ContactListView and KnowledgeBaseListView, because we have a custom sort order there too
|
// load all entries at once to apply custom sort order
|
||||||
if (startId === GENERATED_MAX_ID) {
|
const allEntries = await this.entityClient.loadAll(EmailTemplateTypeRef, this.templateListId())
|
||||||
// load all entries at once to apply custom sort order
|
return { items: allEntries, complete: true }
|
||||||
const allEntries = await this.entityClient.loadAll(EmailTemplateTypeRef, this.templateListId())
|
|
||||||
return { items: allEntries, complete: true }
|
|
||||||
} else {
|
|
||||||
throw new Error("fetch template entry called for specific start id")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
loadSingle: (_listId: Id, elementId: Id) => {
|
loadSingle: (_listId: Id, elementId: Id) => {
|
||||||
return this.entityClient.load<EmailTemplate>(EmailTemplateTypeRef, [this.templateListId(), elementId])
|
return this.entityClient.load<EmailTemplate>(EmailTemplateTypeRef, [this.templateListId(), elementId])
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { SelectableRowContainer, SelectableRowSelectedSetter, setVisibility } fr
|
||||||
import Stream from "mithril/stream"
|
import Stream from "mithril/stream"
|
||||||
import { List, ListAttrs, MultiselectMode, RenderConfig } from "../../../common/gui/base/List.js"
|
import { List, ListAttrs, MultiselectMode, RenderConfig } from "../../../common/gui/base/List.js"
|
||||||
import { size } from "../../../common/gui/size.js"
|
import { size } from "../../../common/gui/size.js"
|
||||||
import { GENERATED_MAX_ID } from "../../../common/api/common/utils/EntityUtils.js"
|
|
||||||
import { ListModel } from "../../../common/misc/ListModel.js"
|
import { ListModel } from "../../../common/misc/ListModel.js"
|
||||||
import { compareGroupInfos } from "../../../common/api/common/utils/GroupUtils.js"
|
import { compareGroupInfos } from "../../../common/api/common/utils/GroupUtils.js"
|
||||||
import { NotFoundError } from "../../../common/api/common/error/RestError.js"
|
import { NotFoundError } from "../../../common/api/common/error/RestError.js"
|
||||||
|
@ -160,15 +159,12 @@ export class GroupListView implements UpdatableSettingsViewer {
|
||||||
private makeListModel(): ListModel<GroupInfo> {
|
private makeListModel(): ListModel<GroupInfo> {
|
||||||
const listModel = new ListModel<GroupInfo>({
|
const listModel = new ListModel<GroupInfo>({
|
||||||
sortCompare: compareGroupInfos,
|
sortCompare: compareGroupInfos,
|
||||||
fetch: async (startId) => {
|
fetch: async (_lastFetchedEntity, _count) => {
|
||||||
if (startId === GENERATED_MAX_ID) {
|
// load all entries at once to apply custom sort order
|
||||||
const listId = await this.listId.getAsync()
|
const listId = await this.listId.getAsync()
|
||||||
const allGroupInfos = await locator.entityClient.loadAll(GroupInfoTypeRef, listId)
|
const allGroupInfos = await locator.entityClient.loadAll(GroupInfoTypeRef, listId)
|
||||||
|
|
||||||
return { items: allGroupInfos, complete: true }
|
return { items: allGroupInfos, complete: true }
|
||||||
} else {
|
|
||||||
throw new Error("fetch user group infos called for specific start id")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
loadSingle: async (_listId: Id, elementId: Id) => {
|
loadSingle: async (_listId: Id, elementId: Id) => {
|
||||||
const listId = await this.listId.getAsync()
|
const listId = await this.listId.getAsync()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue