chore(StatusMemberListItem): refactor to use ItemDelegate

- simpler, standard property based API
- much lighter than deriving from the heavy StatusListItem
- should reduce RAM usage significantly, esp. with large communities

Iterates #11059
This commit is contained in:
Lukáš Tinkl 2024-09-06 15:11:47 +02:00 committed by Lukáš Tinkl
parent 9050459ee8
commit c5598d9ff9
23 changed files with 222 additions and 125 deletions

View File

@ -9,7 +9,7 @@ import utils 1.0
import shared.status 1.0
import shared.stores 1.0 as SharedStores
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Chat.stores 1.0 as ChatStores
@ -59,10 +59,9 @@ SplitView {
active: globalUtilsMock.ready
sourceComponent: StatusChatInput {
id: chatInput
property var globalUtils: globalUtilsMock.globalUtils
property string unformattedText: chatInput.textInput.getText(0, chatInput.textInput.length)
readonly property ModelChangeTracker urlsModelChangeTracker: ModelChangeTracker {
readonly property SQUtils.ModelChangeTracker urlsModelChangeTracker: SQUtils.ModelChangeTracker {
model: fakeLinksModel
}
@ -85,7 +84,7 @@ SplitView {
linkPreviewModel: fakeLinksModel
urlsList: {
urlsModelChangeTracker.revision
ModelUtils.modelToFlatArray(fakeLinksModel, "url")
return SQUtils.ModelUtils.modelToFlatArray(fakeLinksModel, "url")
}
askToEnableLinkPreview: askToEnableLinkPreviewSwitch.checked
onAskToEnableLinkPreviewChanged: {
@ -94,9 +93,7 @@ SplitView {
d.loadLinkPreviews(unformattedText)
}
}
usersStore: ChatStores.UsersStore {
readonly property var usersModel: fakeUsersModel
}
usersModel: fakeUsersModel
sharedStore: SharedStores.RootStore {
isWalletEnabled: true

View File

@ -140,7 +140,7 @@ Item {
let delegateUnderTest = providersList.itemAtIndex(i)
verify(!!delegateUnderTest)
compare(delegateUnderTest.title, modelToCompareAgainst.get(i).name)
tryCompare(delegateUnderTest, "title", modelToCompareAgainst.get(i).name)
compare(delegateUnderTest.subTitle, modelToCompareAgainst.get(i).description)
compare(delegateUnderTest.asset.name, modelToCompareAgainst.get(i).logoUrl)
compare(delegateUnderTest.isUrlLoading, false)

View File

@ -26,9 +26,7 @@ Item {
width: parent.width
height: implicitHeight
anchors.bottom: parent.bottom
usersStore: ChatStores.UsersStore {
property var usersModel: ListModel {}
}
usersModel: ListModel {}
sharedStore: SharedStores.RootStore {
isWalletEnabled: true

View File

@ -1,4 +1,4 @@
import QtQuick 2.14
import QtQuick 2.15
ListModel {
id: root
@ -10,7 +10,7 @@ ListModel {
isVerified: false
isAdmin: false
isUntrustworthy: true
displayName: "Mike"
displayName: "Mike has a very long name that should elide eventually and result in a tooltip displayed instead"
alias: ""
localNickname: ""
ensName: ""
@ -22,6 +22,8 @@ ListModel {
ListElement {colorId: 0; segmentLength: 2},
ListElement {colorId: 17; segmentLength: 2}
]
isAwaitingAddress: false
memberRole: 0 // Constants.memberRole.none
}
ListElement {
pubKey: "0x04df12f12f12f12f1234"
@ -41,6 +43,8 @@ ListModel {
ListElement {colorId: 0; segmentLength: 1},
ListElement {colorId: 19; segmentLength: 2}
]
isAwaitingAddress: false
memberRole: 1 // Constants.memberRole.owner
}
ListElement {
pubKey: "0x04d1b7cc0ef3f470f1238"
@ -53,9 +57,11 @@ ListModel {
alias: ""
localNickname: "Johnny Johny"
ensName: ""
icon: ""
icon: "https://cryptologos.cc/logos/status-snt-logo.svg?v=033"
colorId: 4
isEnsVerified: false
isAwaitingAddress: false
memberRole: 0
}
ListElement {
pubKey: "0x04d1bed192343f470f1257"
@ -71,6 +77,8 @@ ListModel {
icon: ""
colorId: 5
isEnsVerified: true
isAwaitingAddress: false
memberRole: 0
}
ListElement {
pubKey: "0x04d1bed192343f470f1255"
@ -86,6 +94,8 @@ ListModel {
icon: ""
colorId: 3
isEnsVerified: true
isAwaitingAddress: false
memberRole: 0
}
ListElement {
pubKey: "0x04d1bed192343f470fabc"
@ -101,5 +111,7 @@ ListModel {
icon: ""
colorId: 7
isEnsVerified: true
isAwaitingAddress: true
memberRole: 0
}
}

View File

@ -94,7 +94,6 @@ ColumnLayout {
nickName: model.nick
isVerified: model.isVerified
isContact: model.isContact
asset.isLetterIdenticon: true
}
}

View File

@ -1,15 +1,18 @@
import QtQuick 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1
/*!
\qmltype StatusMemberListItem
\inherits StatusListItem
\inherits ItemDelegate
\inqmlmodule StatusQ.Components
\since StatusQ.Components 0.1
\brief It is a list item with a specific format to display members of a community or chat. Inherits from \c StatusListItem.
\brief It is a list item with a specific format to display members of a community or chat. Inherits from \c ItemDelegate.
The \c StatusMemberListItem is a clickable / hovering list item with a specific format to display members of a community or chat.
@ -36,7 +39,7 @@ import StatusQ.Core.Utils 0.1
For a list of components available see StatusQ.
*/
StatusListItem {
ItemDelegate {
id: root
/*!
@ -91,6 +94,42 @@ StatusListItem {
*/
property bool isAwaitingAddress: false
/*!
\qmlproperty color StatusMemberListItem::color
Defines the background color of the delegate
*/
property color color: hovered || highlighted ? Theme.palette.baseColor2 : Theme.palette.baseColor4
/*!
\qmlproperty list<Item> StatusMemberListItem::components
This property holds the optional list of actions, displayed on the right side.
The actions are reparented into a Row.
*/
property list<Item> components
onComponentsChanged: {
for (let idx in components) {
components[idx].parent = componentsRow
}
}
/*!
\qmlproperty StatusIdenticonRingSettings StatusMemberListItem::ringSettings
This property holds the StatusSmartIdenticon ring settings
*/
property alias ringSettings: identicon.ringSettings
/*!
\qmlproperty StatusBadge StatusMemberListItem::badge
This property holds the StatusBadge used for displaying user's online status
*/
property alias badge: identicon.badge
/*!
\qmlsignal
This signal is emitted when the StatusMemberListItem is clicked.
*/
signal clicked(var mouse)
QtObject {
id: d
@ -118,33 +157,108 @@ StatusListItem {
}
}
// root object settings:
title: root.nickName || Emoji.parse(root.userName)
statusListItemIcon.name: root.nickName || root.userName
statusListItemTitleIcons.sourceComponent: root.isAwaitingAddress ?
awaitingAddressComponent : statusContactVerificationIcons
horizontalPadding: 8
verticalPadding: 12
spacing: 8
subTitle: d.composeSubtitle()
statusListItemSubTitle.font.pixelSize: 10
statusListItemIcon.badge.visible: true
statusListItemIcon.badge.color: root.status === 1 ? Theme.palette.successColor1 : Theme.palette.baseColor1 // FIXME
color: sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4
icon.width: 32
icon.height: 32
// Default sizes / positions by design
implicitWidth: 256
implicitHeight: Math.max(56, statusListItemTitleArea.height + leftPadding)
leftPadding: 8
asset.width: 32
asset.height: 32
font.family: Theme.palette.baseFont.name
font.pixelSize: Theme.primaryTextFontSize
background: Rectangle {
color: root.color
radius: 8
}
contentItem: RowLayout {
spacing: root.spacing
StatusSmartIdenticon {
id: identicon
name: root.nickName || root.userName
asset.name: root.icon.name
asset.color: root.icon.color
asset.isImage: asset.name !== ""
asset.isLetterIdenticon: asset.name === ""
asset.width: root.icon.width
asset.height: root.icon.height
asset.charactersLen: 2
asset.letterSize: asset._twoLettersSize
statusListItemIcon.anchors.verticalCenter: sensor.verticalCenter
statusListItemIcon.anchors.top: undefined
statusListItemIcon.badge.border.width: 2
statusListItemIcon.badge.implicitHeight: 12 // 8 px + 2 px * 2 borders
statusListItemIcon.badge.implicitWidth: 12 // 8 px + 2 px * 2 borders
components: [
// badge
badge.visible: true
badge.color: root.status === 1 ? Theme.palette.successColor1 : Theme.palette.baseColor1 // FIXME, see root.status
badge.anchors.top: undefined
badge.border.width: 2
badge.border.color: Theme.palette.statusListItem.backgroundColor
badge.implicitHeight: 12 // 8 px + 2 px * 2 borders
badge.implicitWidth: 12 // 8 px + 2 px * 2 borders
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Row {
spacing: 4
Layout.fillWidth: true
StatusBaseText {
width: Math.min(implicitWidth, parent.width - (iconsLoader.item ? iconsLoader.item.width + parent.spacing : 0))
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: root.nickName || Emoji.parse(root.userName)
font.pixelSize: root.font.pixelSize
font.weight: Font.Medium
color: Theme.palette.directColor4
HoverHandler {
id: primaryTextHandler
}
StatusToolTip {
text: parent.text
delay: 0
visible: parent.truncated && primaryTextHandler.hovered
}
}
Loader {
id: iconsLoader
anchors.verticalCenter: parent.verticalCenter
sourceComponent: root.isAwaitingAddress ? awaitingAddressComponent : statusContactVerificationIcons
}
}
StatusBaseText {
Layout.fillWidth: true
elide: Text.ElideRight
text: d.composeSubtitle()
font.pixelSize: 10
color: Theme.palette.baseColor1
visible: !!text
HoverHandler {
id: secondaryTextHandler
}
StatusToolTip {
text: parent.text
delay: 0
visible: parent.truncated && secondaryTextHandler.hovered
}
}
}
Row {
id: componentsRow
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
spacing: 12
}
Loader {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
active: root.isAdmin
sourceComponent: StatusIcon {
anchors.verticalCenter: parent.verticalCenter
@ -152,7 +266,14 @@ StatusListItem {
color: Theme.palette.directColor1
}
}
]
}
MouseArea {
anchors.fill: parent
cursorShape: root.enabled && root.hoverEnabled && root.hovered ? Qt.PointingHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: root.clicked(mouse)
}
Component {
id: statusContactVerificationIcons

View File

@ -415,14 +415,11 @@ Item {
isVerified: model.isVerified
isUntrustworthy: model.isUntrustworthy
isContact: model.isContact
asset.name: model.icon
asset.color: Theme.palette.userCustomizationColors[root.colorIdForPubkeyGetter(model.pubKey)]
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
icon.name: model.icon
icon.color: Theme.palette.userCustomizationColors[root.colorIdForPubkeyGetter(model.pubKey)]
status: model.onlineStatus
statusListItemIcon.badge.border.color: sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4
ringSettings.ringSpecModel: root.ringSpecModelGetter(model.pubKey)
color: (sensor.containsMouse || highlighted) ? Theme.palette.baseColor2 : "transparent"
color: (hovered || highlighted) ? Theme.palette.baseColor2 : "transparent"
onClicked: {
root.insertTag(model.displayName, model.pubKey, model.isAdmin, model.isAdmin ? "crown" : "");
}

View File

@ -1,5 +1,4 @@
import QtQuick 2.13
import StatusQ.Core 0.1
import QtQuick 2.15
QtObject {
id: root

View File

@ -95,7 +95,7 @@ Item {
return
}
return globalUtils.plainText(this.filter)
return Utils.plainText(this.filter)
}
function shouldShowAll(filter) {

View File

@ -121,22 +121,20 @@ Item {
isVerified: model.isVerified
isUntrustworthy: model.isUntrustworthy
isAdmin: model.memberRole === Constants.memberRole.owner
asset.name: model.icon
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
asset.color: Utils.colorForColorId(model.colorId)
icon.name: model.icon
icon.color: Utils.colorForColorId(model.colorId)
status: model.onlineStatus
ringSettings.ringSpecModel: model.colorHash
onClicked: {
if (mouse.button === Qt.RightButton) {
Global.openMenu(profileContextMenuComponent, this, {
myPublicKey: userProfile.pubKey,
myPublicKey: root.store.myPublicKey(),
selectedUserPublicKey: model.pubKey,
selectedUserDisplayName: nickName || userName,
selectedUserIcon: model.icon,
})
} else if (mouse.button === Qt.LeftButton) {
Global.openProfilePopup(model.pubKey);
Global.openProfilePopup(model.pubKey)
}
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.13
import QtQuick 2.15
QtObject {
id: root

View File

@ -286,8 +286,7 @@ Item {
}
store: root.rootStore
usersStore: d.activeUsersStore
usersModel: d.activeUsersStore.usersModel
sharedStore: SharedStores.RootStore
linkPreviewModel: !!d.activeChatContentModule ? d.activeChatContentModule.inputAreaModule.linkPreviewModel : null

View File

@ -161,9 +161,7 @@ Page {
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup
closeGifPopupAfterSelection: true
usersStore: ({
usersModel: membersSelector.model
})
sharedStore: SharedStores.RootStore
onStickerSelected: {
root.createChatPropertiesStore.createChatStickerHashId = hashId;

View File

@ -71,8 +71,8 @@ Control {
delegate: ContactListItemDelegate {
width: ListView.view.width
height: d.delegateHeight
asset.width: 29
asset.height: 29
icon.width: 29
icon.height: 29
color: "transparent"

View File

@ -103,7 +103,7 @@ ItemDelegate {
color: "transparent"
leftPadding: 0
rightPadding: 0
sensor.enabled: false
hoverEnabled: false
nickName: root.contactDetails.localNickname
userName: ProfileUtils.displayName("", root.contactDetails.ensName, root.contactDetails.displayName, root.contactDetails.alias)
pubKey: root.contactDetails.isEnsVerified ? "" : Utils.getCompressedPk(root.contactId)
@ -112,10 +112,8 @@ ItemDelegate {
isUntrustworthy: root.contactDetails.trustStatus === Constants.trustStatus.untrustworthy
isAdmin: root.contactDetails.memberRole === Constants.memberRole.owner
status: root.contactDetails.onlineStatus
asset.name: root.contactDetails.thumbnailImage
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
asset.color: Utils.colorForPubkey(root.contactId)
icon.name: root.contactDetails.thumbnailImage
icon.color: Utils.colorForPubkey(root.contactId)
ringSettings.ringSpecModel: Utils.getColorHashAsJson(root.contactId)
}
}

View File

@ -143,7 +143,7 @@ Item {
readonly property bool ctaAllowed: !isRejectedPending && !isAcceptedPending && !isBanPending && !isUnbanPending && !isKickPending
readonly property bool itsMe: model.pubKey.toLowerCase() === Global.userProfile.pubKey.toLowerCase()
readonly property bool isHovered: memberItem.sensor.containsMouse
readonly property bool isHovered: memberItem.hovered
readonly property bool canBeBanned: {
if (memberItem.itsMe) {
return false
@ -186,9 +186,6 @@ Item {
isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress
statusListItemComponentsSlot.spacing: 16
statusListItemTitleArea.anchors.rightMargin: 0
statusListItemSubTitle.elide: Text.ElideRight
rightPadding: 75
leftPadding: 12
@ -297,21 +294,19 @@ Item {
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
status: model.onlineStatus
asset.color: Utils.colorForColorId(model.colorId)
asset.name: model.icon
asset.isImage: !!model.icon
asset.isLetterIdenticon: !model.icon
asset.width: 40
asset.height: 40
icon.color: Utils.colorForColorId(model.colorId)
icon.name: model.icon
icon.width: 40
icon.height: 40
ringSettings.ringSpecModel: model.colorHash
statusListItemIcon.badge.visible: (root.panelType === MembersTabPanel.TabType.AllMembers)
badge.visible: (root.panelType === MembersTabPanel.TabType.AllMembers)
onClicked: {
if(mouse.button === Qt.RightButton) {
Global.openMenu(memberContextMenuComponent, this, {
selectedUserPublicKey: model.pubKey,
selectedUserDisplayName: memberItem.title,
selectedUserIcon: asset.name,
selectedUserIcon: icon.name,
})
} else {
Global.openProfilePopup(model.pubKey)

View File

@ -183,8 +183,8 @@ StatusDropdown {
width: ListView.view.width
height: d.delegateHeight
asset.width: 29
asset.height: 29
icon.width: 29
icon.height: 29
rightPadding: 0
leftPadding: 6

View File

@ -304,7 +304,7 @@ StatusScrollView {
color: "transparent"
leftPadding: 0
rightPadding: 0
sensor.enabled: false
hoverEnabled: false
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
@ -313,10 +313,8 @@ StatusScrollView {
isVerified: model.verificationStatus === Constants.verificationStatus.verified
isUntrustworthy: model.trustStatus === Constants.trustStatus.untrustworthy
status: model.onlineStatus
asset.name: model.icon
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
asset.color: Utils.colorForPubkey(model.pubKey)
icon.name: model.icon
icon.color: Utils.colorForPubkey(model.pubKey)
ringSettings.ringSpecModel: Utils.getColorHashAsJson(model.pubKey)
}
}

View File

@ -1,5 +1,5 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
@ -21,12 +21,9 @@ StatusMemberListItem {
isVerified: model.isVerified
isUntrustworthy: model.isUntrustworthy
isContact: model.isContact
asset.name: model.icon
asset.color: Utils.colorForColorId(model.colorId)
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
icon.name: model.icon
icon.color: Utils.colorForColorId(model.colorId)
status: model.onlineStatus
statusListItemIcon.badge.border.color: sensor.containsMouse ? Theme.palette.baseColor2 : Theme.palette.baseColor4
ringSettings.ringSpecModel: model.colorHash
color: (sensor.containsMouse || highlighted) ? Theme.palette.baseColor2 : "transparent"
color: (hovered || highlighted) ? Theme.palette.baseColor2 : "transparent"
}

View File

@ -36,7 +36,7 @@ Rectangle {
signal dismissLinkPreviewSettings()
signal dismissLinkPreview(int index)
property ChatStores.UsersStore usersStore
property var usersModel
property ChatStores.RootStore store
property SharedStores.RootStore sharedStore
@ -67,7 +67,7 @@ Rectangle {
property var linkPreviewModel: null
property var urlsList: null
property var urlsList: []
property bool askToEnableLinkPreview: false
@ -1010,7 +1010,7 @@ Rectangle {
SuggestionBoxPanel {
id: suggestionsBox
objectName: "suggestionsBox"
model: control.usersStore ? control.usersStore.usersModel : []
model: control.usersModel ?? []
x : messageInput.x
y: -height - Style.current.smallPadding
width: messageInput.width

View File

@ -86,19 +86,14 @@ Item {
isContact: model.isContact
status: model.onlineStatus
height: visible ? implicitHeight : 0
color: sensor.containsMouse ? Theme.palette.baseColor2 : "transparent"
color: hovered ? Theme.palette.baseColor2 : "transparent"
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
asset.name: model.icon
asset.isImage: (asset.name !== "")
asset.isLetterIdenticon: (asset.name === "")
asset.width: 40
asset.height: 40
asset.color: Utils.colorForColorId(model.colorId)
icon.name: model.icon
icon.width: 40
icon.height: 40
icon.color: Utils.colorForColorId(model.colorId)
ringSettings.ringSpecModel: model.colorHash
statusListItemIcon.badge.border.color: Theme.palette.baseColor4
statusListItemIcon.badge.implicitHeight: 14 // 10 px + 2 px * 2 borders
statusListItemIcon.badge.implicitWidth: 14 // 10 px + 2 px * 2 borders
onClicked: {
root.contactClicked(model);

View File

@ -45,21 +45,17 @@ Item {
status: model.onlineStatus
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
asset.name: model.icon
asset.isImage: asset.name !== ""
asset.isLetterIdenticon: asset.name === ""
asset.imgIsIdenticon: false
asset.width: 40
asset.height: 40
icon.name: model.icon
icon.width: 40
icon.height: 40
color: "transparent"
asset.color: Utils.colorForColorId(model.colorId)
icon.color: Utils.colorForColorId(model.colorId)
ringSettings.ringSpecModel: model.colorHash
statusListItemIcon.badge.border.color: Theme.palette.baseColor4
statusListItemIcon.badge.implicitHeight: 14 // 10 px + 2 px * 2 borders
statusListItemIcon.badge.implicitWidth: 14 // 10 px + 2 px * 2 borders
badge.border.color: Theme.palette.baseColor4
badge.implicitHeight: 14 // 10 px + 2 px * 2 borders
badge.implicitWidth: 14 // 10 px + 2 px * 2 borders
sensor.enabled: false
hoverEnabled: false
}
}
}

View File

@ -915,7 +915,7 @@ Loader {
}
store: root.rootStore
usersStore: root.usersStore
usersModel: root.usersStore.usersModel
sharedStore: SharedStores.RootStore
emojiPopup: root.emojiPopup
stickersPopup: root.stickersPopup