diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 0a77d2a0..f7d28fed 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; +import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {ViewModel} from "../ViewModel.js"; @@ -29,22 +30,22 @@ export class SessionViewModel extends ViewModel { reconnector: sessionContainer.reconnector, session: sessionContainer.session, }))); + this._leftPanelViewModel = new LeftPanelViewModel(this.childOptions({ + rooms: this._session.rooms, + openRoom: this._openRoom.bind(this) + })); this._currentRoomTileViewModel = null; this._currentRoomViewModel = null; - const roomTileVMs = this._session.rooms.mapValues((room, emitChange) => { - return new RoomTileViewModel({ - room, - emitChange, - emitOpen: this._openRoom.bind(this) - }); - }); - this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b)); } start() { this._sessionStatusViewModel.start(); } + get leftPanelViewModel() { + return this._leftPanelViewModel; + } + get sessionStatusViewModel() { return this._sessionStatusViewModel; } diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js new file mode 100644 index 00000000..50864b4c --- /dev/null +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -0,0 +1,58 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../../ViewModel.js"; +import {RoomTileViewModel} from "./RoomTileViewModel.js"; +import {RoomFilter} from "./RoomFilter.js"; +import {ApplyMap} from "../../../observable/map/ApplyMap.js"; + +export class LeftPanelViewModel extends ViewModel { + constructor(options) { + super(options); + const {rooms, openRoom} = options; + const roomTileVMs = rooms.mapValues((room, emitChange) => { + return new RoomTileViewModel({ + room, + emitChange, + emitOpen: openRoom + }); + }); + this._roomListFilterMap = new ApplyMap(roomTileVMs); + this._roomList = this._roomListFilterMap.sortValues((a, b) => a.compare(b)); + } + + get roomList() { + return this._roomList; + } + + clearFilter() { + this._roomListFilterMap.setApply(null); + this._roomListFilterMap.applyOnce((roomId, vm) => vm.hidden = false); + } + + setFilter(query) { + query = query.trim(); + if (query.length === 0) { + this.clearFilter(); + } else { + const filter = new RoomFilter(query); + this._roomListFilterMap.setApply((roomId, vm) => { + vm.hidden = !filter.matches(vm); + }); + } + } +} diff --git a/src/domain/session/leftpanel/RoomFilter.js b/src/domain/session/leftpanel/RoomFilter.js new file mode 100644 index 00000000..cdbe2cb2 --- /dev/null +++ b/src/domain/session/leftpanel/RoomFilter.js @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export class RoomFilter { + constructor(query) { + this._parts = query.split(" ").map(s => s.toLowerCase().trim()); + } + + matches(roomTileVM) { + const name = roomTileVM.name.toLowerCase(); + return this._parts.every(p => name.includes(p)); + } +} diff --git a/src/ui/web/css/left-panel.css b/src/ui/web/css/left-panel.css index cde12111..fa935e10 100644 --- a/src/ui/web/css/left-panel.css +++ b/src/ui/web/css/left-panel.css @@ -14,10 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ - .LeftPanel { - overflow-y: auto; - overscroll-behavior: contain; + display: flex; + flex-direction: column; +} + +.LeftPanel .filter { + flex: 0; + display: flex; +} + +.LeftPanel .filter input { + display: block; + flex: 1; + box-sizing: border-box; } .LeftPanel ul { @@ -26,19 +36,24 @@ limitations under the License. margin: 0; } -.LeftPanel li { +.RoomList { + overflow-y: auto; + overscroll-behavior: contain; +} + +.RoomList li { display: flex; align-items: center; } -.LeftPanel div.description { +.RoomList .description { margin: 0; flex: 1 1 0; min-width: 0; display: flex; } -.LeftPanel .description > .name { +.RoomList .description > .name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; diff --git a/src/ui/web/session/leftpanel/LeftPanelView.js b/src/ui/web/session/leftpanel/LeftPanelView.js new file mode 100644 index 00000000..6688b537 --- /dev/null +++ b/src/ui/web/session/leftpanel/LeftPanelView.js @@ -0,0 +1,49 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ListView} from "../../general/ListView.js"; +import {TemplateView} from "../../general/TemplateView.js"; +import {RoomTileView} from "./RoomTileView.js"; + +export class LeftPanelView extends TemplateView { + render(t, vm) { + const filterInput = t.input({ + type: "text", + placeholder: vm.i18n`Filter rooms…`, + "aria-label": vm.i18n`Filter rooms by name`, + autocomplete: true, + name: "room-filter", + onInput: event => vm.setFilter(event.target.value), + }); + return t.div({className: "LeftPanel"}, [ + t.div({className: "filter"}, [ + filterInput, + t.button({onClick: () => { + filterInput.value = ""; + vm.clearFilter(); + }}, vm.i18n`Clear`) + ]), + t.view(new ListView( + { + className: "RoomList", + list: vm.roomList, + onItemClick: (roomTile, event) => roomTile.clicked(event) + }, + roomTileVM => new RoomTileView(roomTileVM) + )) + ]); + } +}