Skip to content

Commit

Permalink
Fix Url->list interaction
Browse files Browse the repository at this point in the history
Co-authored-by: paw <[email protected]>

close: #8223
  • Loading branch information
BijinDev committed Jan 27, 2025
1 parent 6c30156 commit a3c9c89
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 26 deletions.
82 changes: 60 additions & 22 deletions src/mail-app/mail/model/ConversationListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
isSameId,
listIdPart,
} from "../../../common/api/common/utils/EntityUtils"
import { assertNotNull, compare, isNotNull } from "@tutao/tutanota-utils"
import { assertNotNull, compare, first, isNotNull } from "@tutao/tutanota-utils"
import { ListFetchResult } from "../../../common/gui/base/ListUtils"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils"
import { OperationType } from "../../../common/api/common/TutanotaConstants"
Expand All @@ -46,8 +46,11 @@ export class ConversationListModel implements MailSetListModel {
// Map conversation IDs (to ensure unique conversations)
private readonly conversationMap: Map<Id, LoadedConversation> = new Map()

// keep a reverse map for going from Mail element id -> LoadedConversation
private readonly mailMap: Map<Id, LoadedConversation> = new Map()
// keep a reverse map for going from Mail element id -> LoadedMail
private readonly mailMap: Map<Id, LoadedMail> = new Map()

// override selected email
private olderMailSelection: Id | null = null

constructor(
private readonly mailSet: MailFolder,
Expand Down Expand Up @@ -90,7 +93,7 @@ export class ConversationListModel implements MailSetListModel {
}

getMail(mailId: Id): Mail | null {
return this.getConversationForMailById(mailId)?.latestMail?.mail ?? null
return this.mailMap.get(mailId)?.mail ?? null
}

getSelectedAsArray(): Mail[] {
Expand All @@ -106,12 +109,12 @@ export class ConversationListModel implements MailSetListModel {
if (update.operation === OperationType.UPDATE) {
const mailSetId: IdTuple = [update.instanceListId, update.instanceId]
for (const loadedMail of this.mailMap.values()) {
const hasMailSet = loadedMail.latestMail.labels.some((label) => isSameId(mailSetId, label._id))
const hasMailSet = loadedMail.labels.some((label) => isSameId(mailSetId, label._id))
if (!hasMailSet) {
continue
}
// MailModel's entity event listener should have been fired first
const labels = this.mailModel.getLabelsForMail(loadedMail.latestMail.mail)
const labels = this.mailModel.getLabelsForMail(loadedMail.mail)
const newMailEntry = {
...loadedMail,
labels,
Expand All @@ -122,7 +125,7 @@ export class ConversationListModel implements MailSetListModel {
} else if (isUpdateForTypeRef(MailSetEntryTypeRef, update) && isSameId(this.mailSet.entries, update.instanceListId)) {
if (update.operation === OperationType.DELETE) {
const { mailId } = deconstructMailSetEntryId(update.instanceId)
const conversation = this.mailMap.get(mailId)
const conversation = this.getConversationForMailById(mailId)
if (!conversation) {
return
}
Expand Down Expand Up @@ -221,22 +224,18 @@ export class ConversationListModel implements MailSetListModel {
const newMailData = await this.entityClient.load(MailTypeRef, [update.instanceListId, update.instanceId])
const labels = this.mailModel.getLabelsForMail(newMailData) // in case labels were added/removed
const loadedMail = {
...mailItem.latestMail,
...mailItem,
labels,
mail: newMailData,
}
this._updateSingleMail({
...mailItem,
latestMail: loadedMail,
})
this._updateSingleMail(loadedMail)
}
}
}

// @VisibleForTesting
_updateSingleMail(loadedConversation: LoadedConversation) {
this.updateMailMaps(loadedConversation)
this.listModel.updateLoadedItem(loadedConversation)
_updateSingleMail(loadedMail: LoadedMail) {
this.mailMap.set(getElementId(loadedMail.mail), loadedMail)
}

private async loadSingleMail(id: IdTuple): Promise<{
Expand Down Expand Up @@ -274,11 +273,23 @@ export class ConversationListModel implements MailSetListModel {
}

async loadAndSelect(mailId: string, shouldStop: () => boolean): Promise<Mail | null> {
// loadAndSelect doesn't actually use the mailId if we pass our own finder in, so we can pass a finder that
// looks for a specific mail ID
const mailFinder = (loadedConversation: LoadedConversation) => isSameId(getElementId(loadedConversation.latestMail.mail), mailId)
const mail = await this.listModel.loadAndSelect(mailFinder, shouldStop)
return mail?.latestMail?.mail ?? null

// conversation listing has a special case: we may want to select an item that isn't on the list but is part of
// a conversation that is actually in the list; as such, we should disregard what listModel says
const stop = () => this.getMail(mailId) != null || shouldStop()
await this.listModel.loadAndSelect(mailFinder, stop)

const selectedMail = this.getMail(mailId)
if (selectedMail != null) {
const selectedMailId = getElementId(selectedMail)
const conversation = assertNotNull(this.getConversationForMailById(selectedMailId))
if (!isSameId(getElementId(conversation.latestMail.mail), selectedMailId)) {
this.olderMailSelection = selectedMailId
this.listModel.onSingleSelection(conversation)
}
}
return selectedMail
}

async loadInitial() {
Expand Down Expand Up @@ -331,6 +342,18 @@ export class ConversationListModel implements MailSetListModel {

get stateStream(): Stream<ListState<Mail>> {
return this.listModel.stateStream.map((state) => {
if (this.olderMailSelection) {
const olderMail = this.getMail(this.olderMailSelection)
if (
olderMail == null ||
state.selectedItems.size !== 1 ||
state.inMultiselect ||
[...state.selectedItems][0].conversationId !== listIdPart(olderMail.conversationEntry)
) {
this.olderMailSelection = null
}
}

const items = state.items.map((item) => item.latestMail.mail)
const selectedItems: Set<Mail> = new Set()
for (const item of state.selectedItems) {
Expand All @@ -353,13 +376,27 @@ export class ConversationListModel implements MailSetListModel {
this.listModel.onSingleExclusiveSelection(assertNotNull(this.getConversationForMail(mail)))
}

getDisplayedMail(): Mail | null {
if (this.olderMailSelection != null) {
return this.getMail(this.olderMailSelection)
} else if (this.isInMultiselect()) {
return null
} else {
return first(this.getSelectedAsArray())
}
}

/**
* Gets the conversation if this mail is the latest of that conversation.
*
* Note: We do not care about older mails in the conversation - only the mail that is going to be shown in the list.
*/
private getConversationForMailById(mailId: Id): LoadedConversation | null {
return this.mailMap.get(mailId) ?? null
const mail = this.mailMap.get(mailId)
if (mail == null) {
return null
}
return this.conversationMap.get(listIdPart(mail.mail.conversationEntry)) ?? null
}

private getConversationForMail(mail: Mail): LoadedConversation | null {
Expand Down Expand Up @@ -433,11 +470,12 @@ export class ConversationListModel implements MailSetListModel {

const existingConversation = this.conversationMap.get(conversation.conversationId)
if (existingConversation != null && this.reverseSortConversation(conversation, existingConversation) >= 0) {
// store it so we can load it later
this._updateSingleMail(mail)
continue
}

if (existingConversation != null) {
this.mailMap.delete(getElementId(existingConversation.latestMail.mail))
deletedItems.push(existingConversation)
}

Expand All @@ -449,7 +487,7 @@ export class ConversationListModel implements MailSetListModel {
}

private updateMailMaps(conversation: LoadedConversation) {
this.mailMap.set(getElementId(conversation.latestMail.mail), conversation)
this.mailMap.set(getElementId(conversation.latestMail.mail), conversation.latestMail)
this.conversationMap.set(conversation.conversationId, conversation)
}

Expand Down
10 changes: 9 additions & 1 deletion src/mail-app/mail/model/MailListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { EntityClient } from "../../../common/api/common/EntityClient"
import { ConversationPrefProvider } from "../view/ConversationViewModel"
import { assertMainOrNode } from "../../../common/api/common/Env"
import { assertNotNull, compare } from "@tutao/tutanota-utils"
import { assertNotNull, compare, first } from "@tutao/tutanota-utils"
import { ListLoadingState, ListState } from "../../../common/gui/base/List"
import Stream from "mithril/stream"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils"
Expand Down Expand Up @@ -252,6 +252,14 @@ export class MailListModel implements MailSetListModel {
this.listModel.stopLoading()
}

getDisplayedMail(): Mail | null {
if (this.isInMultiselect()) {
return null
} else {
return first(this.getSelectedAsArray())
}
}

private getLoadedMailByMailId(mailId: Id): LoadedMail | null {
return this.mailMap.get(mailId) ?? null
}
Expand Down
2 changes: 2 additions & 0 deletions src/mail-app/mail/model/MailSetListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface MailSetListModel {
cancelLoadAll(): void

isLoadingAll(): boolean

getDisplayedMail(): Mail | null
}

/**
Expand Down
4 changes: 1 addition & 3 deletions src/mail-app/mail/view/MailViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,7 @@ export class MailViewModel {
if (!(displayedMailId && isSameId(displayedMailId, this.stickyMailId))) {
const targetItem = this.stickyMailId
? newState.items.find((item) => isSameId(this.stickyMailId, item._id))
: !newState.inMultiselect && newState.selectedItems.size === 1
? first(this.listModel!.getSelectedAsArray())
: null
: assertNotNull(this.listModel).getDisplayedMail()
if (targetItem != null) {
// Always write the targetItem in case it was not written before but already being displayed (sticky mail)
this.mailFolderElementIdToSelectedMailId = mapWith(
Expand Down

0 comments on commit a3c9c89

Please sign in to comment.