Skip to content

Commit

Permalink
[Calendar] Make Calendar Search separation clearer
Browse files Browse the repository at this point in the history
  • Loading branch information
wrdhub committed Jul 16, 2024
1 parent 1e8d8c6 commit f94328f
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 162 deletions.
16 changes: 9 additions & 7 deletions src/calendar-app/calendar-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import { LoginController } from "../common/api/main/LoginController.js"
import { SettingsViewAttrs } from "../common/settings/Interfaces.js"
import { CalendarSearchView, CalendarSearchViewAttrs } from "./calendar/search/view/CalendarSearchView.js"
import { initCommonLocator } from "../common/api/main/CommonLocator.js"
import { SettingsView } from "./calendar/view/SettingsView.js"
import { SearchViewModel } from "../mail-app/search/view/SearchViewModel.js"
import { CalendarSettingsView } from "./calendar/view/CalendarSettingsView.js"
import { CalendarSearchViewModel } from "./calendar/search/view/CalendarSearchViewModel.js"
import { CalendarBottomNav } from "./gui/CalendarBottomNav.js"

assertMainOrNodeBoot()
bootFinished()
Expand Down Expand Up @@ -167,13 +167,13 @@ import("../mail-app/translations/en.js")
},
calendarLocator.logins,
),
settings: makeViewResolver<SettingsViewAttrs, SettingsView, { drawerAttrsFactory: () => DrawerMenuAttrs; header: AppHeaderAttrs }>(
settings: makeViewResolver<SettingsViewAttrs, CalendarSettingsView, { drawerAttrsFactory: () => DrawerMenuAttrs; header: AppHeaderAttrs }>(
{
prepareRoute: async () => {
const { SettingsView } = await import("../calendar-app/calendar/view/SettingsView.js")
const { CalendarSettingsView } = await import("./calendar/view/CalendarSettingsView.js")
const drawerAttrsFactory = await calendarLocator.drawerAttrsFactory()
return {
component: SettingsView,
component: CalendarSettingsView,
cache: { drawerAttrsFactory, header: await calendarLocator.appHeaderAttrs() },
}
},
Expand Down Expand Up @@ -210,7 +210,7 @@ import("../mail-app/translations/en.js")
calendar: makeViewResolver<
CalendarViewAttrs,
CalendarView,
{ drawerAttrsFactory: () => DrawerMenuAttrs; header: AppHeaderAttrs; calendarViewModel: CalendarViewModel }
{ drawerAttrsFactory: () => DrawerMenuAttrs; header: AppHeaderAttrs; calendarViewModel: CalendarViewModel; bottomNav: Children }
>(
{
prepareRoute: async (cache) => {
Expand All @@ -222,13 +222,15 @@ import("../mail-app/translations/en.js")
drawerAttrsFactory,
header: await calendarLocator.appHeaderAttrs(),
calendarViewModel: await calendarLocator.calendarViewModel(),
bottomNav: m(CalendarBottomNav),
},
}
},
prepareAttrs: ({ header, calendarViewModel, drawerAttrsFactory }) => ({
prepareAttrs: ({ header, calendarViewModel, drawerAttrsFactory, bottomNav }) => ({
drawerAttrs: drawerAttrsFactory(),
header,
calendarViewModel,
bottomNav,
}),
},
calendarLocator.logins,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
import type { Shortcut } from "../../../common/misc/KeyManager"
import { isKeyPressed, keyManager } from "../../../common/misc/KeyManager"
import { encodeCalendarSearchKey, getRestriction } from "./model/SearchUtils"
import { locator } from "../../../common/api/main/MainLocator"
import type { WhitelabelChild } from "../../../common/api/entities/sys/TypeRefs.js"
import { FULL_INDEXED_TIMESTAMP, Keys } from "../../../common/api/common/TutanotaConstants"
import { assertMainOrNode } from "../../../common/api/common/Env"
Expand All @@ -27,6 +26,8 @@ import { generateCalendarInstancesInRange } from "../../../common/calendar/date/
import { loadMultipleFromLists } from "../../../common/api/common/EntityClient.js"
import { SearchRouter } from "../../../common/search/view/SearchRouter.js"
import { SearchBarOverlay } from "../../../mail-app/search/SearchBarOverlay.js"
import { calendarLocator } from "../../calendarLocator.js"
import { CalendarSearchBarOverlay } from "./CalendarSearchBarOverlay.js"

assertMainOrNode()
export type ShowMoreAction = {
Expand All @@ -35,7 +36,7 @@ export type ShowMoreAction = {
indexTimestamp: number
allowShowMore: boolean
}
export type SearchBarAttrs = {
export type CalendarSearchBarAttrs = {
placeholder?: string | null
returnListener?: (() => unknown) | null
disabled?: boolean
Expand All @@ -44,22 +45,21 @@ export type SearchBarAttrs = {
const MAX_SEARCH_PREVIEW_RESULTS = 10
export type Entry = Mail | Contact | CalendarEvent | WhitelabelChild | ShowMoreAction
type Entries = Array<Entry>
export type SearchBarState = {
export type CalendarSearchBarState = {
query: string
searchResult: SearchResult | null
indexState: SearchIndexStateInfo
entities: Entries
selected: Entry | null
}

// create our own copy which is not perfect because we don't benefit from the shared cache but currently there's no way to get async dependencies into
// singletons like this (without top-level await at least)
// once SearchBar is rewritten this should be removed
const searchRouter = new SearchRouter(locator.throttledRouter())
const searchRouter = new SearchRouter(calendarLocator.throttledRouter())

export class SearchBar implements Component<SearchBarAttrs> {
export class CalendarSearchBar implements Component<CalendarSearchBarAttrs> {
focused: boolean = false
private readonly state: Stream<SearchBarState>
private readonly state: Stream<CalendarSearchBarState>
busy: boolean = false
private closeOverlayFunction: (() => void) | null = null
private readonly overlayContentComponent: Component
Expand All @@ -70,16 +70,15 @@ export class SearchBar implements Component<SearchBarAttrs> {
private lastQueryStream: Stream<unknown> | null = null

constructor() {
this.state = stream<SearchBarState>({
this.state = stream<CalendarSearchBarState>({
query: "",
searchResult: null,
indexState: locator.search.indexState(),
entities: [] as Entries,
selected: null,
})
this.overlayContentComponent = {
view: () => {
return m(SearchBarOverlay, {
return m(CalendarSearchBarOverlay, {
state: this.state(),
isQuickSearch: this.isQuickSearch(),
isFocused: this.focused,
Expand All @@ -98,7 +97,7 @@ export class SearchBar implements Component<SearchBarAttrs> {
* that shouldn't clear our current state, but if the URL changed in a way that makes the previous state outdated, we clear it.
*/
private readonly onPathChange = memoized((newPath: string) => {
if (locator.search.isNewSearch(this.state().query, getRestriction(newPath))) {
if (calendarLocator.search.isNewSearch(this.state().query, getRestriction(newPath))) {
this.updateState({
searchResult: null,
selected: null,
Expand All @@ -107,7 +106,7 @@ export class SearchBar implements Component<SearchBarAttrs> {
}
})

view(vnode: Vnode<SearchBarAttrs>) {
view(vnode: Vnode<CalendarSearchBarAttrs>) {
this.onPathChange(m.route.get())

return m(BaseSearchBar, {
Expand Down Expand Up @@ -195,7 +194,7 @@ export class SearchBar implements Component<SearchBarAttrs> {
oncreate() {
keyManager.registerShortcuts(this.shortcuts)
this.stateStream = this.state.map((state) => m.redraw())
this.lastQueryStream = locator.search.lastQueryString.map((value) => {
this.lastQueryStream = calendarLocator.search.lastQueryString.map((value) => {
// Set value from the model when it's set from the URL e.g. reloading the page on the search screen
if (value) {
this.updateState({
Expand Down Expand Up @@ -339,8 +338,8 @@ export class SearchBar implements Component<SearchBarAttrs> {

let restriction = this.getRestriction()

if (!locator.search.isNewSearch(query, restriction) && oldQuery === query) {
const result = locator.search.result()
if (!calendarLocator.search.isNewSearch(query, restriction) && oldQuery === query) {
const result = calendarLocator.search.result()

if (this.isQuickSearch() && result) {
this.showResultsInOverlay(result)
Expand Down Expand Up @@ -371,15 +370,15 @@ export class SearchBar implements Component<SearchBarAttrs> {
// We don't limit contacts because we need to download all of them to sort them. They should be cached anyway.
const limit = isSameTypeRef(MailTypeRef, restriction.type) ? (this.isQuickSearch() ? MAX_SEARCH_PREVIEW_RESULTS : PageSize) : null

locator.search
calendarLocator.search
.search(
{
query: query ?? "",
restriction,
minSuggestionCount: useSuggestions ? 10 : 0,
maxResults: limit,
},
locator.progressTracker,
calendarLocator.progressTracker,
)
.then((result) => this.loadAndDisplayResult(query, result ? result : null, limit))
.finally(() => cb())
Expand All @@ -394,14 +393,14 @@ export class SearchBar implements Component<SearchBarAttrs> {
searchResult: safeResult,
})

if (!safeResult || locator.search.isNewSearch(query, safeResult.restriction)) {
if (!safeResult || calendarLocator.search.isNewSearch(query, safeResult.restriction)) {
return
}

if (this.isQuickSearch()) {
if (safeLimit && hasMoreResults(safeResult) && safeResult.results.length < safeLimit) {
locator.searchFacade.getMoreSearchResults(safeResult, safeLimit - safeResult.results.length).then((moreResults) => {
if (locator.search.isNewSearch(query, moreResults.restriction)) {
calendarLocator.searchFacade.getMoreSearchResults(safeResult, safeLimit - safeResult.results.length).then((moreResults) => {
if (calendarLocator.search.isNewSearch(query, moreResults.restriction)) {
return
} else {
this.loadAndDisplayResult(query, moreResults, limit)
Expand All @@ -421,7 +420,7 @@ export class SearchBar implements Component<SearchBarAttrs> {
// this needs to happen in this order, otherwise the list's result subscription will override our
// routing.
this.updateSearchUrl("")
locator.search.result(null)
calendarLocator.search.result(null)
}

this.updateState({
Expand All @@ -433,9 +432,9 @@ export class SearchBar implements Component<SearchBarAttrs> {
}

private async showResultsInOverlay(result: SearchResult): Promise<void> {
const entries = await loadMultipleFromLists(result.restriction.type, locator.entityClient, result.results)
const entries = await loadMultipleFromLists(result.restriction.type, calendarLocator.entityClient, result.results)
// If there was no new search while we've been downloading the result
if (!locator.search.isNewSearch(result.query, result.restriction)) {
if (!calendarLocator.search.isNewSearch(result.query, result.restriction)) {
const { filteredEntries, couldShowMore } = this.filterResults(entries, result.restriction)

if (
Expand Down Expand Up @@ -504,7 +503,7 @@ export class SearchBar implements Component<SearchBarAttrs> {
m.redraw()
}

private updateState(update: Partial<SearchBarState>): SearchBarState {
private updateState(update: Partial<CalendarSearchBarState>): CalendarSearchBarState {
const newState = Object.assign({}, this.state(), update)

this.state(newState)
Expand All @@ -515,4 +514,4 @@ export class SearchBar implements Component<SearchBarAttrs> {

// Should be changed to not be a singleton and be proper component (instantiated by mithril).
// We need to extract some state of it into some kind of viewModel, pluggable depending on the current view but this requires complete rewrite of SearchBar.
export const searchBar = new SearchBar()
export const searchBar = new CalendarSearchBar()
94 changes: 94 additions & 0 deletions src/calendar-app/calendar/search/CalendarSearchBarOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { CalendarSearchBarAttrs, CalendarSearchBarState, Entry, ShowMoreAction } from "./CalendarSearchBar.js"
import m, { Children, Component, Vnode } from "mithril"
import { downcast, isEmpty, isSameTypeRef, TypeRef } from "@tutao/tutanota-utils"
import { px, size } from "../../../common/gui/size.js"
import { CalendarEvent, CalendarEventTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
import { FULL_INDEXED_TIMESTAMP } from "../../../common/api/common/TutanotaConstants.js"
import { formatDate } from "../../../common/misc/Formatter.js"
import { formatEventDuration } from "../gui/CalendarGuiUtils.js"
import { getTimeZone } from "../../../common/calendar/date/CalendarUtils.js"

type CalendarSearchBarOverlayAttrs = {
state: CalendarSearchBarState
isQuickSearch: boolean
isFocused: boolean
selectResult: (result: Entry | null) => void
}

export class CalendarSearchBarOverlay implements Component<CalendarSearchBarOverlayAttrs> {
view({ attrs }: Vnode<CalendarSearchBarOverlayAttrs>): Children {
const { state } = attrs
return [state.entities && !isEmpty(state.entities) && attrs.isQuickSearch && attrs.isFocused ? this.renderResults(state, attrs) : null]
}

renderResults(state: CalendarSearchBarState, attrs: CalendarSearchBarOverlayAttrs): Children {
return m("ul.list.click.mail-list", [
state.entities.map((result) => {
return m(
"li.plr-l.flex-v-center.",
{
style: {
height: px(52),
"border-left": px(size.border_selection) + " solid transparent",
},
// avoid closing overlay before the click event can be received
onmousedown: (e: MouseEvent) => e.preventDefault(),
onclick: () => attrs.selectResult(result),
class: state.selected === result ? "row-selected" : "",
},
this.renderResult(state, result),
)
}),
])
}

renderResult(state: CalendarSearchBarState, result: Entry): Children {
let type: TypeRef<any> | null = "_type" in result ? result._type : null

if (!type) {
return this.renderShowMoreAction(downcast(result))
} else if (isSameTypeRef(CalendarEventTypeRef, type)) {
return this.renderCalendarEventResult(downcast(result))
} else {
return []
}
}

private renderShowMoreAction(result: ShowMoreAction): Children {
// show more action
let showMoreAction = result as any as ShowMoreAction
let infoText
let indexInfo

if (showMoreAction.resultCount === 0) {
infoText = lang.get("searchNoResults_msg")

if (locator.logins.getUserController().isFreeAccount()) {
indexInfo = lang.get("changeTimeFrame_msg")
}
} else if (showMoreAction.allowShowMore) {
infoText = lang.get("showMore_action")
} else {
infoText = lang.get("moreResultsFound_msg", {
"{1}": showMoreAction.resultCount - showMoreAction.shownCount,
})
}

if (showMoreAction.indexTimestamp > FULL_INDEXED_TIMESTAMP && !indexInfo) {
indexInfo = lang.get("searchedUntil_msg") + " " + formatDate(new Date(showMoreAction.indexTimestamp))
}

return indexInfo
? [m(".top.flex-center", infoText), m(".bottom.flex-center.small", indexInfo)]
: m("li.plr-l.pt-s.pb-s.items-center.flex-center", m(".flex-center", infoText))
}

private renderCalendarEventResult(event: CalendarEvent): Children {
return [
m(".top.flex-space-between", m(".name.text-ellipsis", { title: event.summary }, event.summary)),
m(".bottom.flex-space-between", m("small.mail-address", formatEventDuration(event, getTimeZone(), false))),
]
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import stream from "mithril/stream"
import Stream from "mithril/stream"
import { NOTHING_INDEXED_TIMESTAMP } from "../../../../common/api/common/TutanotaConstants"
import type { SearchIndexStateInfo, SearchRestriction, SearchResult } from "../../../../common/api/worker/search/SearchTypes"
import type { SearchRestriction, SearchResult } from "../../../../common/api/worker/search/SearchTypes"
import { arrayEquals, assertNonNull, assertNotNull, incrementMonth, isSameTypeRef, lazyAsync, tokenize } from "@tutao/tutanota-utils"
import type { SearchFacade } from "../../../../common/api/worker/search/SearchFacade"
import { assertMainOrNode } from "../../../../common/api/common/Env"
Expand Down
Loading

0 comments on commit f94328f

Please sign in to comment.