Begin yet another model refactor

Use native ListModel which require a lot of changes, but should be
much faster than the old way which exponentially slowed down to a crawl.
Also fix some popup bugs (leave/forget).

Not working yet: side pane keyboard controls, proper highlight,
room & member filtering, local echo replacement
This commit is contained in:
miruka
2019-12-02 16:29:29 -04:00
parent 2ce5e20efa
commit 9990fecc74
49 changed files with 826 additions and 781 deletions

View File

@@ -6,54 +6,61 @@ import Clipboard 0.1
import "../Base"
HTileDelegate {
id: accountDelegate
id: account
spacing: 0
topPadding: model.index > 0 ? theme.spacing / 2 : 0
bottomPadding: topPadding
backgroundColor: theme.mainPane.account.background
opacity: collapsed && ! forceExpand ?
opacity: collapsed && ! anyFilter ?
theme.mainPane.account.collapsedOpacity : 1
shouldBeCurrent:
window.uiState.page === "Pages/AccountSettings/AccountSettings.qml" &&
window.uiState.pageProperties.userId === model.data.user_id
title.color: theme.mainPane.account.name
title.text: model.display_name || model.id
title.font.pixelSize: theme.fontSize.big
title.leftPadding: theme.spacing
setCurrentTimer.running:
! mainPaneList.activateLimiter.running && ! mainPane.hasFocus
image: HUserAvatar {
userId: model.id
displayName: model.display_name
mxc: model.avatar_url
}
contextMenu: HMenu {
HMenuItem {
icon.name: "copy-user-id"
text: qsTr("Copy user ID")
onTriggered: Clipboard.text = model.id
}
Behavior on opacity { HNumberAnimation {} }
readonly property bool forceExpand: Boolean(mainPaneList.filter)
// Hide harmless error when a filter matches nothing
readonly property bool collapsed: try {
return mainPaneList.collapseAccounts[model.data.user_id] || false
} catch (err) {}
HMenuItemPopupSpawner {
icon.name: "sign-out"
icon.color: theme.colors.negativeBackground
text: qsTr("Sign out")
popup: "Popups/SignOutPopup.qml"
properties: { "userId": model.id }
}
}
onActivated: pageLoader.showPage(
"AccountSettings/AccountSettings", { "userId": model.data.user_id }
"AccountSettings/AccountSettings", { "userId": model.id }
)
readonly property bool collapsed:
window.uiState.collapseAccounts[model.id] || false
readonly property bool anyFilter: Boolean(mainPaneList.filter)
function toggleCollapse() {
window.uiState.collapseAccounts[model.data.user_id] = ! collapsed
window.uiState.collapseAccounts[model.id] = ! collapsed
window.uiStateChanged()
}
image: HUserAvatar {
userId: model.data.user_id
displayName: model.data.display_name
mxc: model.data.avatar_url
}
title.color: theme.mainPane.account.name
title.text: model.data.display_name || model.data.user_id
title.font.pixelSize: theme.fontSize.big
title.leftPadding: theme.spacing
Behavior on opacity { HNumberAnimation {} }
HButton {
id: addChat
@@ -62,7 +69,7 @@ HTileDelegate {
backgroundColor: "transparent"
toolTip.text: qsTr("Add new chat")
onClicked: pageLoader.showPage(
"AddChat/AddChat", {userId: model.data.user_id},
"AddChat/AddChat", {userId: model.id},
)
leftPadding: theme.spacing / 2
@@ -73,7 +80,7 @@ HTileDelegate {
Layout.fillHeight: true
Layout.maximumWidth:
accountDelegate.width >= 100 * theme.uiScale ? implicitWidth : 0
account.width >= 100 * theme.uiScale ? implicitWidth : 0
Behavior on Layout.maximumWidth { HNumberAnimation {} }
Behavior on opacity { HNumberAnimation {} }
@@ -81,22 +88,23 @@ HTileDelegate {
HButton {
id: expand
loading: ! model.data.first_sync_done || ! model.data.profile_updated
loading:
! model.first_sync_done || model.profile_updated < new Date(1)
iconItem.small: true
icon.name: "expand"
backgroundColor: "transparent"
toolTip.text: collapsed ? qsTr("Expand") : qsTr("Collapse")
onClicked: accountDelegate.toggleCollapse()
onClicked: account.toggleCollapse()
leftPadding: theme.spacing / 2
rightPadding: leftPadding
opacity: ! loading && accountDelegate.forceExpand ? 0 : 1
opacity: ! loading && account.anyFilter ? 0 : 1
visible: opacity > 0 && Layout.maximumWidth > 0
Layout.fillHeight: true
Layout.maximumWidth:
accountDelegate.width >= 120 * theme.uiScale ? implicitWidth : 0
account.width >= 120 * theme.uiScale ? implicitWidth : 0
iconItem.transform: Rotation {
@@ -110,21 +118,4 @@ HTileDelegate {
Behavior on Layout.maximumWidth { HNumberAnimation {} }
Behavior on opacity { HNumberAnimation {} }
}
contextMenu: HMenu {
HMenuItem {
icon.name: "copy-user-id"
text: qsTr("Copy user ID")
onTriggered: Clipboard.text = model.data.user_id
}
HMenuItemPopupSpawner {
icon.name: "sign-out"
icon.color: theme.colors.negativeBackground
text: qsTr("Sign out")
popup: "Popups/SignOutPopup.qml"
properties: { "userId": model.data.user_id }
}
}
}

View File

@@ -1,143 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
HListView {
id: mainPaneList
readonly property var originSource: window.mainPaneModelSource
readonly property var collapseAccounts: window.uiState.collapseAccounts
readonly property string filter: toolBar.roomFilter
readonly property alias activateLimiter: activateLimiter
onOriginSourceChanged: filterLimiter.restart()
onFilterChanged: filterLimiter.restart()
onCollapseAccountsChanged: filterLimiter.restart()
function filterSource() {
let show = []
// Hide a harmless error when activating a RoomDelegate
try { window.mainPaneModelSource } catch (err) { return }
for (let i = 0; i < window.mainPaneModelSource.length; i++) {
let item = window.mainPaneModelSource[i]
if (item.type === "Account" ||
(filter ?
utils.filterMatches(filter, item.data.filter_string) :
! window.uiState.collapseAccounts[item.user_id]))
{
if (filter && show.length && item.type === "Account" &&
show[show.length - 1].type === "Account" &&
! utils.filterMatches(
filter, show[show.length - 1].data.filter_string)
) {
// If filter active, current and previous items are
// both accounts and previous account doesn't match filter,
// that means the previous account had no matching rooms.
show.pop()
}
show.push(item)
}
}
let last = show[show.length - 1]
if (show.length && filter && last.type === "Account" &&
! utils.filterMatches(filter, last.data.filter_string))
{
// If filter active, last item is an account and last item
// doesn't match filter, that account had no matching rooms.
show.pop()
}
model.source = show
}
function previous(activate=true) {
decrementCurrentIndex()
if (activate) activateLimiter.restart()
}
function next(activate=true) {
incrementCurrentIndex()
if (activate) activateLimiter.restart()
}
function activate() {
currentItem.item.activated()
}
function accountSettings() {
if (! currentItem) incrementCurrentIndex()
pageLoader.showPage(
"AccountSettings/AccountSettings",
{userId: currentItem.item.delegateModel.user_id},
)
}
function addNewChat() {
if (! currentItem) incrementCurrentIndex()
pageLoader.showPage(
"AddChat/AddChat",
{userId: currentItem.item.delegateModel.user_id},
)
}
function toggleCollapseAccount() {
if (filter) return
if (! currentItem) incrementCurrentIndex()
if (currentItem.item.delegateModel.type === "Account") {
currentItem.item.toggleCollapse()
return
}
for (let i = 0; i < model.source.length; i++) {
let item = model.source[i]
if (item.type === "Account" && item.user_id ==
currentItem.item.delegateModel.user_id)
{
currentIndex = i
currentItem.item.toggleCollapse()
}
}
}
model: HListModel {
keyField: "id"
source: originSource
}
delegate: Loader {
width: mainPaneList.width
Component.onCompleted: setSource(
model.type === "Account" ?
"AccountDelegate.qml" : "RoomDelegate.qml",
{view: mainPaneList}
)
}
Timer {
id: filterLimiter
interval: 16
onTriggered: filterSource()
}
Timer {
id: activateLimiter
interval: 300
onTriggered: activate()
}
}

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import ".."
import "../Base"
Column {
id: delegate
property string userId: model.id
readonly property HListView view: ListView.view
Account {
id: account
width: parent.width
view: delegate.view
}
HListView {
id: roomList
width: parent.width
height: contentHeight * opacity
opacity: account.collapsed ? 0 : 1
visible: opacity > 0
interactive: false
model: ModelStore.get(delegate.userId, "rooms")
// model: HSortFilterProxy {
// model: ModelStore.get(delegate.userId, "rooms")
// comparator: (a, b) =>
// // Sort by membership, then last event date (most recent first)
// // then room display name or ID.
// // Invited rooms are first, then joined rooms, then left rooms.
// // Left rooms may still have an inviter_id, so check left first
// [
// a.left,
// b.inviter_id,
// b.last_event && b.last_event.date ?
// b.last_event.date.getTime() : 0,
// (a.display_name || a.id).toLocaleLowerCase(),
// ] < [
// b.left,
// a.inviter_id,
// a.last_event && a.last_event.date ?
// a.last_event.date.getTime() : 0,
// (b.display_name || b.id).toLocaleLowerCase(),
// ]
// }
delegate: Room {
width: roomList.width
userId: delegate.userId
}
Behavior on opacity {
HNumberAnimation { easing.type: Easing.InOutCirc }
}
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import ".."
import "../Base"
HListView {
id: mainPaneList
model: ModelStore.get("accounts")
// model: HSortFilterProxy {
// model: ModelStore.get("accounts")
// comparator: (a, b) =>
// // Sort by display name or user ID
// (a.display_name || a.id).toLocaleLowerCase() <
// (b.display_name || b.id).toLocaleLowerCase()
// }
delegate: AccountRoomsDelegate {
width: mainPaneList.width
height: childrenRect.height
}
readonly property string filter: toolBar.roomFilter
function previous(activate=true) {
decrementCurrentIndex()
if (activate) activateLimiter.restart()
}
function next(activate=true) {
incrementCurrentIndex()
if (activate) activateLimiter.restart()
}
function activate() {
currentItem.item.activated()
}
function accountSettings() {
if (! currentItem) incrementCurrentIndex()
pageLoader.showPage(
"AccountSettings/AccountSettings",
{userId: currentItem.item.delegateModel.user_id},
)
}
function addNewChat() {
if (! currentItem) incrementCurrentIndex()
pageLoader.showPage(
"AddChat/AddChat",
{userId: currentItem.item.delegateModel.user_id},
)
}
function toggleCollapseAccount() {
if (filter) return
if (! currentItem) incrementCurrentIndex()
if (currentItem.item.delegateModel.type === "Account") {
currentItem.item.toggleCollapse()
return
}
for (let i = 0; i < model.source.length; i++) {
let item = model.source[i]
if (item.type === "Account" && item.user_id ==
currentItem.item.delegateModel.user_id)
{
currentIndex = i
currentItem.item.toggleCollapse()
}
}
}
Timer {
id: activateLimiter
interval: 300
onTriggered: activate()
}
}

View File

@@ -37,7 +37,7 @@ HDrawer {
HColumnLayout {
anchors.fill: parent
AccountRoomList {
AccountRoomsList {
id: mainPaneList
clip: true

View File

@@ -9,7 +9,7 @@ HRowLayout {
// Hide filter field overflowing for a sec on size changes
clip: true
property AccountRoomList mainPaneList
property AccountRoomsList mainPaneList
readonly property alias addAccountButton: addAccountButton
readonly property alias filterField: filterField
property alias roomFilter: filterField.text

View File

@@ -3,85 +3,48 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import Clipboard 0.1
import ".."
import "../Base"
HTileDelegate {
id: roomDelegate
spacing: theme.spacing
backgroundColor: theme.mainPane.room.background
opacity: model.data.left ? theme.mainPane.room.leftRoomOpacity : 1
shouldBeCurrent:
window.uiState.page === "Pages/Chat/Chat.qml" &&
window.uiState.pageProperties.userId === model.user_id &&
window.uiState.pageProperties.roomId === model.data.room_id
setCurrentTimer.running:
! mainPaneList.activateLimiter.running && ! mainPane.hasFocus
Behavior on opacity { HNumberAnimation {} }
readonly property bool joined: ! invited && ! parted
readonly property bool invited: model.data.inviter_id && ! parted
readonly property bool parted: model.data.left
readonly property var lastEvent: model.data.last_event
onActivated: pageLoader.showRoom(model.user_id, model.data.room_id)
opacity: model.left ? theme.mainPane.room.leftRoomOpacity : 1
image: HRoomAvatar {
displayName: model.data.display_name
mxc: model.data.avatar_url
displayName: model.display_name
mxc: model.avatar_url
}
title.color: theme.mainPane.room.name
title.text: model.data.display_name || qsTr("Empty room")
title.text: model.display_name || qsTr("Empty room")
additionalInfo.children: HIcon {
svgName: "invite-received"
colorize: theme.colors.alertBackground
visible: invited
Layout.maximumWidth: invited ? implicitWidth : 0
Behavior on Layout.maximumWidth { HNumberAnimation {} }
}
rightInfo.color: theme.mainPane.room.lastEventDate
rightInfo.text: {
! lastEvent || ! lastEvent.date ?
"" :
utils.dateIsToday(lastEvent.date) ?
utils.formatTime(lastEvent.date, false) : // no seconds
lastEvent.date.getFullYear() === new Date().getFullYear() ?
Qt.formatDate(lastEvent.date, "d MMM") : // e.g. "5 Dec"
lastEvent.date.getFullYear()
}
subtitle.color: theme.mainPane.room.subtitle
subtitle.font.italic:
Boolean(lastEvent && lastEvent.event_type === "RoomMessageEmote")
subtitle.textFormat: Text.StyledText
subtitle.font.italic:
lastEvent && lastEvent.event_type === "RoomMessageEmote"
subtitle.text: {
if (! lastEvent) return ""
let isEmote = lastEvent.event_type === "RoomMessageEmote"
let isMsg = lastEvent.event_type.startsWith("RoomMessage")
let isUnknownMsg = lastEvent.event_type === "RoomMessageUnknown"
let isCryptMedia = lastEvent.event_type.startsWith("RoomEncrypted")
const isEmote = lastEvent.event_type === "RoomMessageEmote"
const isMsg = lastEvent.event_type.startsWith("RoomMessage")
const isUnknownMsg = lastEvent.event_type === "RoomMessageUnknown"
const isCryptMedia = lastEvent.event_type.startsWith("RoomEncrypted")
// If it's a general event
if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia)) {
if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia))
return utils.processedEventText(lastEvent)
}
let text = utils.coloredNameHtml(
const text = utils.coloredNameHtml(
lastEvent.sender_name, lastEvent.sender_id
) + ": " + lastEvent.inline_content
@@ -91,26 +54,40 @@ HTileDelegate {
)
}
rightInfo.color: theme.mainPane.room.lastEventDate
rightInfo.text: {
model.last_event_date < new Date(1) ?
"" :
utils.dateIsToday(model.last_event_date) ?
utils.formatTime(model.last_event_date, false) : // no seconds
model.last_event_date.getFullYear() === new Date().getFullYear() ?
Qt.formatDate(model.last_event_date, "d MMM") : // e.g. "5 Dec"
model.last_event_date.getFullYear()
}
contextMenu: HMenu {
HMenuItemPopupSpawner {
visible: joined
enabled: model.data.can_invite
enabled: model.can_invite
icon.name: "room-send-invite"
text: qsTr("Invite members")
popup: "Popups/InviteToRoomPopup.qml"
properties: ({
userId: model.user_id,
roomId: model.data.room_id,
roomName: model.data.display_name,
invitingAllowed: Qt.binding(() => model.data.can_invite)
userId: userId,
roomId: model.id,
roomName: model.display_name,
invitingAllowed: Qt.binding(() => model.can_invite)
})
}
HMenuItem {
icon.name: "copy-room-id"
text: qsTr("Copy room ID")
onTriggered: Clipboard.text = model.data.room_id
onTriggered: Clipboard.text = model.id
}
HMenuItem {
@@ -118,12 +95,12 @@ HTileDelegate {
icon.name: "invite-accept"
icon.color: theme.colors.positiveBackground
text: qsTr("Accept %1's invite").arg(utils.coloredNameHtml(
model.data.inviter_name, model.data.inviter_id
model.inviter_name, model.inviter_id
))
label.textFormat: Text.StyledText
onTriggered: py.callClientCoro(
model.user_id, "join", [model.data.room_id]
userId, "join", [model.id]
)
}
@@ -135,9 +112,9 @@ HTileDelegate {
popup: "Popups/LeaveRoomPopup.qml"
properties: ({
userId: model.user_id,
roomId: model.data.room_id,
roomName: model.data.display_name,
userId: userId,
roomId: model.id,
roomName: model.display_name,
})
}
@@ -149,10 +126,27 @@ HTileDelegate {
popup: "Popups/ForgetRoomPopup.qml"
autoDestruct: false
properties: ({
userId: model.user_id,
roomId: model.data.room_id,
roomName: model.data.display_name,
userId: userId,
roomId: model.id,
roomName: model.display_name,
})
}
}
onActivated: pageLoader.showRoom(userId, model.id)
property string userId
readonly property bool joined: ! invited && ! parted
readonly property bool invited: model.inviter_id && ! parted
readonly property bool parted: model.left
readonly property ListModel eventModel:
ModelStore.get(userId, model.id, "events")
readonly property QtObject lastEvent:
eventModel.count > 0 ? eventModel.get(0) : null
Behavior on opacity { HNumberAnimation {} }
}