diff --git a/TODO.md b/TODO.md index 2a9d08b8..98bf3bf9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,11 @@ # TODO - add account number binds -- rename goto*account → scrollto*account -- fix opacity +- remove `import QtQml.Models 2.12`s - fix left rooms opacity - fix escape keybinds (filter rooms, message selection) - fix python getting stuck when loading large room -- fix room out of bounds (above section) when filtering and only one match - account delegates refactor - lag when switching accounts diff --git a/src/backend/models/filters.py b/src/backend/models/filters.py index 3ba376e2..d10e7bcf 100644 --- a/src/backend/models/filters.py +++ b/src/backend/models/filters.py @@ -31,6 +31,8 @@ class ModelFilter(ModelProxy): _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: if self.accept_source(source): + value = self.convert_item(value) + if self.accept_item(value): self.__setitem__((source.sync_id, key), value, _changed_fields) self.filtered_out.pop((source.sync_id, key), None) @@ -66,16 +68,25 @@ class ModelFilter(ModelProxy): callback() - def refilter(self) -> None: + def refilter( + self, + only_if: Optional[Callable[["ModelItem"], bool]] = None, + ) -> None: with self._write_lock: take_out = [] bring_back = [] for key, item in sorted(self.items(), key=lambda kv: kv[1]): + if only_if and not only_if(item): + continue + if not self.accept_item(item): take_out.append(key) for key, item in self.filtered_out.items(): + if only_if and not only_if(item): + continue + if self.accept_item(item): bring_back.append(key) @@ -86,8 +97,9 @@ class ModelFilter(ModelProxy): for key in bring_back: self[key] = self.filtered_out.pop(key) - for callback in self.items_changed_callbacks: - callback() + if take_out or bring_back: + for callback in self.items_changed_callbacks: + callback() class FieldSubstringFilter(ModelFilter): diff --git a/src/backend/models/items.py b/src/backend/models/items.py index 7d127702..79cd5ef4 100644 --- a/src/backend/models/items.py +++ b/src/backend/models/items.py @@ -53,7 +53,7 @@ class Room(ModelItem): """A matrix room we are invited to, are or were member of.""" id: str = field() - for_account: str = field() + for_account: str = "" given_name: str = "" display_name: str = "" main_alias: str = "" @@ -118,6 +118,33 @@ class Room(ModelItem): ) +@dataclass +class AccountOrRoom(Account, Room): + type: Union[Type[Account], Type[Room]] = Account + + def __lt__(self, other: "AccountOrRoom") -> bool: # type: ignore + return ( + self.id if self.type is Account else self.for_account, + other.type is Account, + self.left, + other.inviter_id, + bool(other.mentions), + bool(other.unreads), + other.last_event_date, + (self.display_name or self.id).lower(), + + ) < ( + other.id if other.type is Account else other.for_account, + self.type is Account, + other.left, + self.inviter_id, + bool(self.mentions), + bool(self.unreads), + self.last_event_date, + (other.display_name or other.id).lower(), + ) + + @dataclass class Member(ModelItem): """A member in a matrix room.""" diff --git a/src/backend/models/proxy.py b/src/backend/models/proxy.py index e9814afe..f61e2a2c 100644 --- a/src/backend/models/proxy.py +++ b/src/backend/models/proxy.py @@ -25,6 +25,10 @@ class ModelProxy(Model): return True + def convert_item(self, item: "ModelItem") -> "ModelItem": + return item + + def source_item_set( self, source: Model, @@ -33,6 +37,7 @@ class ModelProxy(Model): _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: if self.accept_source(source): + value = self.convert_item(value) self.__setitem__((source.sync_id, key), value, _changed_fields) diff --git a/src/backend/models/special_models.py b/src/backend/models/special_models.py index 7fd01cd4..9e278e59 100644 --- a/src/backend/models/special_models.py +++ b/src/backend/models/special_models.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from dataclasses import asdict + from .filters import FieldSubstringFilter, ModelFilter +from .items import Account, AccountOrRoom from .model import Model from .model_item import ModelItem @@ -8,16 +11,38 @@ from .model_item import ModelItem class AllRooms(FieldSubstringFilter): def __init__(self) -> None: super().__init__(sync_id="all_rooms", fields=("display_name",)) + self.items_changed_callbacks.append(self.refilter_accounts) def accept_source(self, source: Model) -> bool: - return ( + return source.sync_id == "accounts" or ( isinstance(source.sync_id, tuple) and len(source.sync_id) == 2 and source.sync_id[1] == "rooms" # type: ignore ) + def convert_item(self, item: ModelItem) -> AccountOrRoom: + return AccountOrRoom(**asdict(item), type=type(item)) # type: ignore + + + def accept_item(self, item: ModelItem) -> bool: + matches_filter = super().accept_item(item) + + if item.type is not Account or not self.filter: # type: ignore + return matches_filter + + return next( + (i for i in self.values() if i.for_account == item.id), False, + ) + + + def refilter_accounts(self) -> None: + self.refilter( + lambda i: isinstance(i, AccountOrRoom) and i.type is Account, + ) + + class MatchingAccounts(ModelFilter): def __init__(self, all_rooms: AllRooms) -> None: self.all_rooms = all_rooms @@ -35,7 +60,7 @@ class MatchingAccounts(ModelFilter): return True return next( - (r for r in self.all_rooms.values() if r.for_account == item.id), + (i for i in self.all_rooms.values() if i.id == item.id), False, ) diff --git a/src/gui/MainPane/Account.qml b/src/gui/MainPane/Account.qml index 51298d14..70f8306f 100644 --- a/src/gui/MainPane/Account.qml +++ b/src/gui/MainPane/Account.qml @@ -16,19 +16,19 @@ HTile { HUserAvatar { id: avatar - userId: accountModel.id - displayName: accountModel.display_name - mxc: accountModel.avatar_url + userId: model.id + displayName: model.display_name + mxc: model.avatar_url radius: 0 compact: account.compact } TitleLabel { - text: accountModel.display_name || accountModel.id + text: model.display_name || model.id color: hovered ? utils.nameColor( - accountModel.display_name || accountModel.id.substring(1), + model.display_name || model.id.substring(1), ) : theme.accountView.account.name @@ -42,7 +42,7 @@ HTile { backgroundColor: "transparent" toolTip.text: qsTr("Add new chat") onClicked: pageLoader.showPage( - "AddChat/AddChat", {userId: accountModel.id}, + "AddChat/AddChat", {userId: model.id}, ) Layout.fillHeight: true @@ -57,16 +57,15 @@ HTile { } } - contextMenu: AccountContextMenu { userId: accountModel.id } + contextMenu: AccountContextMenu { userId: model.id } onLeftClicked: { pageLoader.showPage( - "AccountSettings/AccountSettings", { "userId": accountModel.id } + "AccountSettings/AccountSettings", { "userId": model.id } ) } - property var accountModel property bool isCurrent: false diff --git a/src/gui/MainPane/AccountsBar.qml b/src/gui/MainPane/AccountsBar.qml index 8326968f..8e1b9068 100644 --- a/src/gui/MainPane/AccountsBar.qml +++ b/src/gui/MainPane/AccountsBar.qml @@ -33,7 +33,9 @@ HColumnLayout { roomList.count === 0 || roomList.currentIndex === -1 ? -1 : model.findIndex( - roomList.model.get(roomList.currentIndex).for_account, -1, + roomList.model.get(roomList.currentIndex).for_account || + roomList.model.get(roomList.currentIndex).id, + -1, ) model: ModelStore.get("matching_accounts") @@ -77,6 +79,11 @@ HColumnLayout { HLoader { anchors.fill: parent + anchors.leftMargin: + accountList.highlightItem ? + accountList.highlightItem.border.width : + 0 + opacity: model.first_sync_done ? 0 : 1 active: opacity > 0 @@ -96,14 +103,12 @@ HColumnLayout { contextMenu: AccountContextMenu { userId: model.id } - onLeftClicked: { - model.id in roomList.sectionIndice ? - roomList.goToAccount(model.id) : - pageLoader.showPage("AddChat/AddChat", {userId: model.id}) - } + onLeftClicked: roomList.goToAccount(model.id) } highlight: Item { + readonly property alias border: border + Rectangle { anchors.fill: parent color: theme.accountsBar.accountList.account.selectedBackground @@ -112,7 +117,7 @@ HColumnLayout { } Rectangle { - z: 100 + id: border width: theme.accountsBar.accountList.account.selectedBorderSize height: parent.height color: theme.accountsBar.accountList.account.selectedBorder diff --git a/src/gui/MainPane/RoomList.qml b/src/gui/MainPane/RoomList.qml index 3010c862..64fac796 100644 --- a/src/gui/MainPane/RoomList.qml +++ b/src/gui/MainPane/RoomList.qml @@ -3,6 +3,7 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQml.Models 2.12 +import Qt.labs.qmlmodels 1.0 import ".." import "../Base" @@ -10,82 +11,96 @@ HListView { id: roomList model: ModelStore.get("all_rooms") - delegate: Room { - id: room - width: roomList.width - onActivated: showRoomAtIndex(model.index) - } + delegate: DelegateChooser { + role: "type" - section.property: "for_account" - section.labelPositioning: - ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart + DelegateChoice { + roleValue: "Account" + Account { width: roomList.width } + } - section.delegate: Account { - width: roomList.width - accountModel: ModelStore.get("accounts").find(section) + DelegateChoice { + roleValue: "Room" + Room { + width: roomList.width + onActivated: showItemAtIndex(model.index) + } + } } onFilterChanged: py.callCoro("set_substring_filter", ["all_rooms", filter]) property string filter: "" - readonly property var sectionIndice: { - const sections = {} - let currentUserId = null + readonly property var accountIndice: { + const accounts = {} for (let i = 0; i < model.count; i++) { - const userId = model.get(i).for_account - - if (userId !== currentUserId) { - sections[userId] = i - currentUserId = userId - } + if (model.get(i).type === "Account") + accounts[model.get(i).id] = i } - return sections + return accounts } function goToAccount(userId) { - currentIndex = sectionIndice[userId] + model.get(accountIndice[userId] + 1).type === "Room" ? + currentIndex = accountIndice[userId] + 1 : + currentIndex = accountIndice[userId] + + showItemLimiter.restart() } function goToAccountNumber(num) { - currentIndex = Object.values(sectionIndice).sort()[num] + const index = Object.values(accountIndice).sort()[num] + + model.get(index + 1).type === "Room" ? + currentIndex = index + 1 : + currentIndex = index + + showItemLimiter.restart() } - function showRoomAtIndex(index=currentIndex) { + function showItemAtIndex(index=currentIndex) { if (index === -1) index = 0 index = Math.min(index, model.count - 1) - const room = model.get(index) - pageLoader.showRoom(room.for_account, room.id) + const item = model.get(index) + + item.type === "Account" ? + pageLoader.showPage( + "AccountSettings/AccountSettings", { "userId": item.id } + ) : + pageLoader.showRoom(item.for_account, item.id) + currentIndex = index } function showAccountRoomAtIndex(index) { - const currentUserId = model.get( - currentIndex === -1 ? 0 : currentIndex - ).for_account + const item = model.get(currentIndex === -1 ? 0 : currentIndex) - showRoomAtIndex(sectionIndice[currentUserId] + index) + const currentUserId = + item.type === "Account" ? item.id : item.for_account + + showItemAtIndex(accountIndice[currentUserId] + 1 + index) } Timer { - id: showRoomLimiter + id: showItemLimiter interval: 200 - onTriggered: showRoomAtIndex() + onTriggered: showItemAtIndex() } HShortcut { sequences: window.settings.keys.goToPreviousRoom - onActivated: { decrementCurrentIndex(); showRoomLimiter.restart() } + onActivated: { decrementCurrentIndex(); showItemLimiter.restart() } } HShortcut { sequences: window.settings.keys.goToNextRoom - onActivated: { incrementCurrentIndex(); showRoomLimiter.restart() } + onActivated: { incrementCurrentIndex(); showItemLimiter.restart() } } Repeater {