diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 86136233d8..bc17bbe23f 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -44,7 +44,6 @@ interface IProps { } interface IState { - searchFilter: string; showBreadcrumbs: boolean; showTagPanel: boolean; } @@ -69,7 +68,6 @@ export default class LeftPanel extends React.Component { super(props); this.state = { - searchFilter: "", showBreadcrumbs: BreadcrumbsStore.instance.visible, showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -97,10 +95,6 @@ export default class LeftPanel extends React.Component { this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } - private onSearch = (term: string): void => { - this.setState({searchFilter: term}); - }; - private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -366,7 +360,6 @@ export default class LeftPanel extends React.Component { onKeyDown={this.onKeyDown} > { onKeyDown={this.onKeyDown} resizeNotifier={null} collapsed={false} - searchFilter={this.state.searchFilter} onFocus={this.onFocus} onBlur={this.onBlur} isMinimized={this.props.isMinimized} diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 1451630c97..69504e9ab8 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -24,9 +24,10 @@ import { throttle } from 'lodash'; import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; import { Action } from "../../dispatcher/actions"; +import RoomListStore from "../../stores/room-list/RoomListStore"; +import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; interface IProps { - onQueryUpdate: (newQuery: string) => void; isMinimized: boolean; onVerticalArrow(ev: React.KeyboardEvent): void; onEnter(ev: React.KeyboardEvent): boolean; @@ -40,6 +41,7 @@ interface IState { export default class RoomSearch extends React.PureComponent { private dispatcherRef: string; private inputRef: React.RefObject = createRef(); + private searchFilter: NameFilterCondition = new NameFilterCondition(); constructor(props: IProps) { super(props); @@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent { this.dispatcherRef = defaultDispatcher.register(this.onAction); } + public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if (prevState.query !== this.state.query) { + const hadSearch = !!this.searchFilter.search.trim(); + const haveSearch = !!this.state.query.trim(); + this.searchFilter.search = this.state.query; + if (!hadSearch && haveSearch) { + // started a new filter - add the condition + RoomListStore.instance.addFilter(this.searchFilter); + } else if (hadSearch && !haveSearch) { + // cleared a filter - remove the condition + RoomListStore.instance.removeFilter(this.searchFilter); + } // else the filter hasn't changed enough for us to care here + } + } + public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); } @@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent { private onChange = () => { if (!this.inputRef.current) return; this.setState({query: this.inputRef.current.value}); - this.onSearchUpdated(); }; - // it wants this at the top of the file, but we know better - // tslint:disable-next-line - private onSearchUpdated = throttle( - () => { - // We can't use the state variable because it can lag behind the input. - // The lag is most obvious when deleting/clearing text with the keyboard. - this.props.onQueryUpdate(this.inputRef.current.value); - }, 200, {trailing: true, leading: true}, - ); - private onFocus = (ev: React.FocusEvent) => { this.setState({focused: true}); ev.target.select(); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index dd8b567c26..f4b9de93b1 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -31,7 +31,6 @@ import dis from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import RoomSublist from "./RoomSublist"; import { ActionPayload } from "../../../dispatcher/payloads"; -import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import GroupAvatar from "../avatars/GroupAvatar"; import TemporaryTile from "./TemporaryTile"; @@ -52,7 +51,6 @@ interface IProps { onResize: () => void; resizeNotifier: ResizeNotifier; collapsed: boolean; - searchFilter: string; isMinimized: boolean; } @@ -150,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics { }; } -export default class RoomList extends React.Component { - private searchFilter: NameFilterCondition = new NameFilterCondition(); +export default class RoomList extends React.PureComponent { private dispatcherRef; private customTagStoreRef; @@ -165,21 +162,6 @@ export default class RoomList extends React.Component { this.dispatcherRef = defaultDispatcher.register(this.onAction); } - public componentDidUpdate(prevProps: Readonly): void { - if (prevProps.searchFilter !== this.props.searchFilter) { - const hadSearch = !!this.searchFilter.search.trim(); - const haveSearch = !!this.props.searchFilter.trim(); - this.searchFilter.search = this.props.searchFilter; - if (!hadSearch && haveSearch) { - // started a new filter - add the condition - RoomListStore.instance.addFilter(this.searchFilter); - } else if (hadSearch && !haveSearch) { - // cleared a filter - remove the condition - RoomListStore.instance.removeFilter(this.searchFilter); - } // else the filter hasn't changed enough for us to care here - } - } - public componentDidMount(): void { RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); @@ -266,12 +248,11 @@ export default class RoomList extends React.Component { } }; - private renderCommunityInvites(): React.ReactElement[] { + private renderCommunityInvites(): TemporaryTile[] { // TODO: Put community invites in a more sensible place (not in the room list) // See https://github.com/vector-im/riot-web/issues/14456 return MatrixClientPeg.get().getGroups().filter(g => { - if (g.myMembership !== 'invite') return false; - return !this.searchFilter || this.searchFilter.matches(g.name || ""); + return g.myMembership === 'invite'; }).map(g => { const avatar = ( { isMinimized={this.props.isMinimized} onResize={this.props.onResize} extraBadTilesThatShouldntExist={extraTiles} - isFiltered={!!this.searchFilter.search} /> ); } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index edbdfc0a2c..54766c58df 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -38,7 +38,6 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import dis from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import NotificationBadge from "./NotificationBadge"; -import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { Key } from "../../../Keyboard"; import { ActionPayload } from "../../../dispatcher/payloads"; @@ -49,6 +48,8 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import { arrayHasOrderChange } from "../../../utils/arrays"; import { objectExcluding, objectHasValueChange } from "../../../utils/objects"; +import TemporaryTile from "./TemporaryTile"; +import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS @@ -68,11 +69,10 @@ interface IProps { isMinimized: boolean; tagId: TagID; onResize: () => void; - isFiltered: boolean; // TODO: Don't use this. It's for community invites, and community invites shouldn't be here. // You should feel bad if you use this. - extraBadTilesThatShouldntExist?: React.ReactElement[]; + extraBadTilesThatShouldntExist?: TemporaryTile[]; // TODO: Account for https://github.com/vector-im/riot-web/issues/14179 } @@ -86,12 +86,12 @@ interface ResizeDelta { type PartialDOMRect = Pick; interface IState { - notificationState: ListNotificationState; contextMenuPosition: PartialDOMRect; isResizing: boolean; isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered height: number; rooms: Room[]; + filteredExtraTiles?: TemporaryTile[]; } export default class RoomSublist extends React.Component { @@ -100,23 +100,25 @@ export default class RoomSublist extends React.Component { private dispatcherRef: string; private layout: ListLayout; private heightAtStart: number; + private isBeingFiltered: boolean; + private notificationState: ListNotificationState; constructor(props: IProps) { super(props); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); this.heightAtStart = 0; + this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition(); + this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId); this.state = { - notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId), contextMenuPosition: null, isResizing: false, - isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed, + isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed, height: 0, // to be fixed in a moment, we need `rooms` to calculate this. rooms: RoomListStore.instance.orderedLists[this.props.tagId] || [], }; // Why Object.assign() and not this.state.height? Because TypeScript says no. this.state = Object.assign(this.state, {height: this.calculateInitialHeight()}); - this.state.notificationState.setRooms(this.state.rooms); this.dispatcherRef = defaultDispatcher.register(this.onAction); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); } @@ -146,8 +148,18 @@ export default class RoomSublist extends React.Component { return padding; } + private get extraTiles(): TemporaryTile[] | null { + if (this.state.filteredExtraTiles) { + return this.state.filteredExtraTiles; + } + if (this.props.extraBadTilesThatShouldntExist) { + return this.props.extraBadTilesThatShouldntExist; + } + return null; + } + private get numTiles(): number { - return RoomSublist.calcNumTiles(this.state.rooms, this.props.extraBadTilesThatShouldntExist); + return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles); } private static calcNumTiles(rooms: Room[], extraTiles: any[]) { @@ -160,17 +172,10 @@ export default class RoomSublist extends React.Component { } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { - this.state.notificationState.setRooms(this.state.rooms); - if (prevProps.isFiltered !== this.props.isFiltered) { - if (this.props.isFiltered) { - this.setState({isExpanded: true}); - } else { - this.setState({isExpanded: !this.layout.isCollapsed}); - } - } + const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist; // as the rooms can come in one by one we need to reevaluate // the amount of available rooms to cap the amount of requested visible rooms by the layout - if (RoomSublist.calcNumTiles(prevState.rooms, prevProps.extraBadTilesThatShouldntExist) !== this.numTiles) { + if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) { this.setState({height: this.calculateInitialHeight()}); } } @@ -191,14 +196,14 @@ export default class RoomSublist extends React.Component { // If we're supposed to handle extra tiles, take the performance hit and re-render all the // time so we don't have to consider them as part of the visible room optimization. const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || []; - const nextExtraTiles = nextProps.extraBadTilesThatShouldntExist || []; + const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || []; if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) { return true; } // If we're about to update the height of the list, we don't really care about which rooms // are visible or not for no-op purposes, so ensure that the height calculation runs through. - if (RoomSublist.calcNumTiles(nextState.rooms, nextProps.extraBadTilesThatShouldntExist) !== this.numTiles) { + if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) { return true; } @@ -232,16 +237,41 @@ export default class RoomSublist extends React.Component { } public componentWillUnmount() { - this.state.notificationState.destroy(); defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); } private onListsUpdated = () => { + const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer + + if (this.props.extraBadTilesThatShouldntExist) { + const nameCondition = RoomListStore.instance.getFirstNameFilterCondition(); + if (nameCondition) { + stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist + .filter(t => nameCondition.matches(t.props.displayName || "")); + } else if (this.state.filteredExtraTiles) { + stateUpdates.filteredExtraTiles = null; + } + } + const currentRooms = this.state.rooms; const newRooms = RoomListStore.instance.orderedLists[this.props.tagId] || []; if (arrayHasOrderChange(currentRooms, newRooms)) { - this.setState({rooms: newRooms}); + stateUpdates.rooms = newRooms; + } + + const isStillBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition(); + if (isStillBeingFiltered !== this.isBeingFiltered) { + this.isBeingFiltered = isStillBeingFiltered; + if (isStillBeingFiltered) { + stateUpdates.isExpanded = true; + } else { + stateUpdates.isExpanded = !this.layout.isCollapsed; + } + } + + if (Object.keys(stateUpdates).length > 0) { + this.setState(stateUpdates); } }; @@ -376,8 +406,8 @@ export default class RoomSublist extends React.Component { } else { // find the first room with a count of the same colour as the badge count room = this.state.rooms.find((r: Room) => { - const notifState = this.state.notificationState.getForRoom(r); - return notifState.count > 0 && notifState.color === this.state.notificationState.color; + const notifState = this.notificationState.getForRoom(r); + return notifState.count > 0 && notifState.color === this.notificationState.color; }); } @@ -484,8 +514,9 @@ export default class RoomSublist extends React.Component { } } - if (this.props.extraBadTilesThatShouldntExist) { - tiles.push(...this.props.extraBadTilesThatShouldntExist); + if (this.extraTiles) { + // HACK: We break typing here, but this 'extra tiles' property shouldn't exist. + (tiles as any[]).push(...this.extraTiles); } // We only have to do this because of the extra tiles. We do it conditionally @@ -592,7 +623,7 @@ export default class RoomSublist extends React.Component { const badge = ( '} as ID ${watcherId}`); SettingsStore._watchers[watcherId] = localizedCallback; defaultWatchManager.watchSetting(settingName, roomId, localizedCallback); @@ -167,7 +166,6 @@ export default class SettingsStore { return; } - console.log(`Ending watcher ID ${watcherReference}`); defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]); delete SettingsStore._watchers[watcherReference]; } diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 9967708c29..1f24dc589a 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -22,6 +22,7 @@ import SettingsStore from "../settings/SettingsStore"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; import {isCustomTag} from "./room-list/models"; +import {objectHasDiff} from "../utils/objects"; function commonPrefix(a, b) { const len = Math.min(a.length, b.length); @@ -107,7 +108,10 @@ class CustomRoomTagStore extends EventEmitter { } _onListsUpdated = () => { - this._setState({tags: this._getUpdatedTags()}); + const newTags = this._getUpdatedTags(); + if (!this._state.tags || objectHasDiff(this._state.tags, newTags)) { + this._setState({tags: newTags}); + } }; _onDispatch(payload) { @@ -134,7 +138,7 @@ class CustomRoomTagStore extends EventEmitter { _getUpdatedTags() { if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) { - return; + return {}; // none } const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort(); diff --git a/src/stores/notifications/RoomNotificationStateStore.ts b/src/stores/notifications/RoomNotificationStateStore.ts index 72fdd87ace..8b5da674f5 100644 --- a/src/stores/notifications/RoomNotificationStateStore.ts +++ b/src/stores/notifications/RoomNotificationStateStore.ts @@ -29,6 +29,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { private static internalInstance = new RoomNotificationStateStore(); private roomMap = new Map(); + private listMap = new Map(); private constructor() { super(defaultDispatcher, {}); @@ -52,21 +53,23 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { } /** - * Creates a new list notification state. The consumer is expected to set the rooms - * on the notification state, and destroy the state when it no longer needs it. - * @param tagId The tag to create the notification state for. + * Gets an instance of the list state class for the given tag. + * @param tagId The tag to get the notification state for. * @returns The notification state for the tag. */ public getListState(tagId: TagID): ListNotificationState { - // Note: we don't cache these notification states as the consumer is expected to call - // .setRooms() on the returned object, which could confuse other consumers. + if (this.listMap.has(tagId)) { + return this.listMap.get(tagId); + } // TODO: Update if/when invites move out of the room list. const useTileCount = tagId === DefaultTagID.Invite; const getRoomFn: FetchRoomFn = (room: Room) => { return this.getRoomState(room); }; - return new ListNotificationState(useTileCount, tagId, getRoomFn); + const state = new ListNotificationState(useTileCount, tagId, getRoomFn); + this.listMap.set(tagId, state); + return state; } /** diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 1f6c14ba2f..c3e7fd5c51 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -32,6 +32,8 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import RoomListLayoutStore from "./RoomListLayoutStore"; import { MarkedExecution } from "../../utils/MarkedExecution"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import { NameFilterCondition } from "./filters/NameFilterCondition"; +import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; interface IState { tagsEnabled?: boolean; @@ -54,7 +56,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private algorithm = new Algorithm(); private filterConditions: IFilterCondition[] = []; private tagWatcher = new TagWatcher(this); - private updateFn = new MarkedExecution(() => this.emit(LISTS_UPDATE_EVENT)); + private updateFn = new MarkedExecution(() => { + for (const tagId of Object.keys(this.unfilteredLists)) { + RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.unfilteredLists[tagId]); + } + this.emit(LISTS_UPDATE_EVENT); + }); private readonly watchedSettings = [ 'feature_custom_tags', @@ -71,6 +78,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated); } + public get unfilteredLists(): ITagMap { + if (!this.algorithm) return {}; // No tags yet. + return this.algorithm.getUnfilteredRooms(); + } + public get orderedLists(): ITagMap { if (!this.algorithm) return {}; // No tags yet. return this.algorithm.getOrderedRooms(); @@ -588,6 +600,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.updateFn.trigger(); } + /** + * Gets the first (and ideally only) name filter condition. If one isn't present, + * this returns null. + * @returns The first name filter condition, or null if none. + */ + public getFirstNameFilterCondition(): NameFilterCondition | null { + for (const filter of this.filterConditions) { + if (filter instanceof NameFilterCondition) { + return filter; + } + } + return null; + } + /** * Gets the tags for a room identified by the store. The returned set * should never be empty, and will contain DefaultTagID.Untagged if diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 667084d653..9b2779d900 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -465,6 +465,10 @@ export class Algorithm extends EventEmitter { return this.filteredRooms; } + public getUnfilteredRooms(): ITagMap { + return this._cachedStickyRooms || this.cachedRooms; + } + /** * This returns the same as getOrderedRooms(), but without the sticky room * map as it causes issues for sticky room handling (see sticky room handling diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 6014a122f8..88edaecfb6 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; +import { throttle } from "lodash"; /** * A filter condition for the room list which reveals rooms of a particular @@ -41,9 +42,13 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio public set search(val: string) { this._search = val; - this.emit(FILTER_CHANGED); + this.callUpdate(); } + private callUpdate = throttle(() => { + this.emit(FILTER_CHANGED); + }, 200, {trailing: true, leading: true}); + public isVisible(room: Room): boolean { const lcFilter = this.search.toLowerCase(); if (this.search[0] === '#') { diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 2a5b1b5f16..fa5515878f 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -53,6 +53,9 @@ export function arrayHasDiff(a: any[], b: any[]): boolean { // an element from the other. if (b.some(i => !a.includes(i))) return true; if (a.some(i => !b.includes(i))) return true; + + // if all the keys are common, say so + return false; } else { return true; // different lengths means they are naturally diverged } diff --git a/src/utils/objects.ts b/src/utils/objects.ts index db8248759d..9dcc41ecd2 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -72,6 +72,23 @@ export function objectHasValueChange(a: any, b: any): boolean { return arrayHasDiff(aValues, bValues); } +/** + * Determines if any keys were added, removed, or changed between two objects. + * For changes, simple triple equal comparisons are done, not in-depth + * tree checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns True if there's a difference between the objects, false otherwise + */ +export function objectHasDiff(a: any, b: any): boolean { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (arrayHasDiff(aKeys, bKeys)) return true; + + const possibleChanges = arrayUnion(aKeys, bKeys); + return possibleChanges.some(k => a[k] !== b[k]); +} + /** * Determines the keys added, changed, and removed between two objects. * For changes, simple triple equal comparisons are done, not in-depth