diff --git a/src/app/modules/main/profile_section/devices/model.nim b/src/app/modules/main/profile_section/devices/model.nim index 0f8c9e5f0d..e99177c602 100644 --- a/src/app/modules/main/profile_section/devices/model.nim +++ b/src/app/modules/main/profile_section/devices/model.nim @@ -80,18 +80,10 @@ QtObject: of ModelRole.IsCurrentDevice: result = newQVariant(item.isCurrentDevice) - proc addItems*(self: Model, items: seq[Item]) = - if(items.len == 0): - return - - let parentModelIndex = newQModelIndex() - defer: parentModelIndex.delete - - let first = self.items.len - let last = first + items.len - 1 - self.beginInsertRows(parentModelIndex, first, last) - self.items.add(items) - self.endInsertRows() + proc setItems*(self: Model, items: seq[Item]) = + self.beginResetModel() + self.items = items + self.endResetModel() self.countChanged() proc addItem*(self: Model, item: Item) = diff --git a/src/app/modules/main/profile_section/devices/module.nim b/src/app/modules/main/profile_section/devices/module.nim index 1ba10e76be..03e0fc29e2 100644 --- a/src/app/modules/main/profile_section/devices/module.nim +++ b/src/app/modules/main/profile_section/devices/module.nim @@ -64,7 +64,7 @@ method onDevicesLoaded*(self: Module, allDevices: seq[InstallationDto]) = for d in allDevices: let item = initItem(d, self.isMyDevice(d.id)) items.add(item) - self.view.model().addItems(items) + self.view.model().setItems(items) self.view.setDevicesLoading(false) self.view.deviceSetupChanged() diff --git a/ui/StatusQ/sandbox/controls/ListItems.qml b/ui/StatusQ/sandbox/controls/ListItems.qml index 2f0d81ea24..d38f735b69 100644 --- a/ui/StatusQ/sandbox/controls/ListItems.qml +++ b/ui/StatusQ/sandbox/controls/ListItems.qml @@ -545,34 +545,34 @@ ColumnLayout { StatusBaseText { Layout.fillWidth: true Layout.topMargin: 16 - text: "Loading features:" + text: "Device delegate with online badge" font.pixelSize: 17 } - StatusListItem { + component DeviceListItem: StatusListItem { title: "Nokia 3310" - subTitle: "Incoming device" - label: "loading: true" - asset.width: 40 - asset.height: 40 - asset.emoji: "😁" - asset.color: "hotpink" - asset.letterSize: 14 - asset.isLetterIdenticon: true - loading: true + asset.name: "mobile" + asset.bgColor: Theme.palette.primaryColor3 + asset.color: Theme.palette.primaryColor1 } - StatusListItem { - title: "Nokia 3310" - subTitle: "Device" - label: "loadingFailed: true" - asset.width: 40 - asset.height: 40 - asset.emoji: "😁" - asset.color: "hotpink" - asset.letterSize: 14 - asset.isLetterIdenticon: true - loadingFailed: true + DeviceListItem { + subTitle: "Online now" + subTitleBadgeComponent: StatusOnlineBadge { + online: true + } + } + + DeviceListItem { + subTitle: "Online 47 minutes ago" + subTitleBadgeComponent: StatusOnlineBadge { + online: false + } + } + + DeviceListItem { + subTitle: "This device" + subTitleBadgeComponent: null } StatusListItem { diff --git a/ui/StatusQ/sandbox/controls/LoadingStates.qml b/ui/StatusQ/sandbox/controls/LoadingStates.qml index c1a90aca4f..a72c5e0918 100644 --- a/ui/StatusQ/sandbox/controls/LoadingStates.qml +++ b/ui/StatusQ/sandbox/controls/LoadingStates.qml @@ -1,4 +1,4 @@ -import QtQuick 2.14 +import QtQuick 2.15 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 @@ -26,7 +26,7 @@ Column { width: 200 } - StatusListItem { + component DeviceListItem: StatusListItem { title: "Nokia 3310" subTitle: "Incoming device" asset.width: 40 @@ -35,34 +35,20 @@ Column { asset.color: "hotpink" asset.letterSize: 14 asset.isLetterIdenticon: true + } + + DeviceListItem { statusListItemSubTitle.loading: loadingButton.checked } - StatusListItem { - title: "Nokia 3310" - subTitle: "Incoming device" - asset.width: 40 - asset.height: 40 - asset.emoji: "😁" - asset.color: "hotpink" - asset.letterSize: 14 - asset.isLetterIdenticon: true + DeviceListItem { statusListItemSubTitle.loading: loadingButton.checked statusListItemIcon.loading: loadingButton.checked } - StatusListItem { - title: "Nokia 3310" - subTitle: "Incoming device" - asset.width: 40 - asset.height: 40 - asset.emoji: "😁" - asset.color: "hotpink" - asset.letterSize: 14 - asset.isLetterIdenticon: true + DeviceListItem { statusListItemTitle.loading: loadingButton.checked statusListItemSubTitle.loading: loadingButton.checked statusListItemIcon.loading: loadingButton.checked } - } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index 5f5d90453e..55af5a6683 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -33,7 +33,6 @@ Rectangle { property var inlineTagModel: [] property Component inlineTagDelegate property bool loading: false - property bool loadingFailed: false property StatusAssetSettings asset: StatusAssetSettings { height: isImage ? 40 : 20 @@ -78,6 +77,7 @@ Rectangle { property alias statusListItemTagsSlot: statusListItemTagsSlot property alias statusListItemInlineTagsSlot: statusListItemTagsSlotInline property alias statusListItemLabel: statusListItemLabel + property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent signal clicked(string itemId, var mouse) signal titleClicked(string titleId) @@ -257,6 +257,12 @@ Rectangle { width: parent.width spacing: 4 + Loader { + id: subTitleBadgeLoader + Layout.alignment: Qt.AlignVCenter + visible: sourceComponent + } + StatusTextWithLoadingState { id: statusListItemSubTitle objectName: "statusListItemSubTitle" diff --git a/ui/StatusQ/src/StatusQ/Components/StatusOnlineBadge.qml b/ui/StatusQ/src/StatusQ/Components/StatusOnlineBadge.qml new file mode 100644 index 0000000000..b749dc2aa5 --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Components/StatusOnlineBadge.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import StatusQ.Core.Theme 0.1 + +StatusBadge { + id: root + + property bool online: false + + implicitHeight: 11 + implicitWidth: 11 + color: online ? Theme.palette.successColor1 : Theme.palette.baseColor1 + border.width: 2 + border.color: Theme.palette.baseColor4 + radius: Math.ceil(width / 2) +} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusSyncDeviceDelegate.qml b/ui/StatusQ/src/StatusQ/Components/StatusSyncDeviceDelegate.qml index de4e709111..ff2c3b3291 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusSyncDeviceDelegate.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusSyncDeviceDelegate.qml @@ -15,8 +15,9 @@ StatusListItem { property string deviceName: "" property string deviceType: "" + property real timestamp: 0 property bool isCurrentDevice: false - property int timestamp: 0 + property bool showOnlineBadge: !isCurrentDevice signal itemClicked signal setupSyncingButtonClicked @@ -30,35 +31,29 @@ StatusListItem { subTitle: { if (root.isCurrentDevice) - return qsTr("This device"); + return qsTr("This device") - if (d.secondsFromSync <= 120) - return qsTr("Online now"); + if (d.onlineNow) + return qsTr("Online now") if (d.minutesFromSync <= 60) - return qsTr("Online %n minutes(s) ago", "", d.minutesFromSync); + return qsTr("Online %n minute(s) ago", "", d.minutesFromSync) if (d.daysFromSync == 0) - return qsTr("Last seen earlier today"); + return qsTr("Last seen earlier today") if (d.daysFromSync == 1) - return qsTr("Last online yesterday"); + return qsTr("Last online yesterday") + + const date = new Date(d.deviceLastTimestamp) if (d.daysFromSync <= 6) - return qsTr("Last online [%1]").arg(Qt.locale().dayName[d.lastSyncDate.getDay()]); + return qsTr("Last online [%1]").arg(LocaleUtils.getDayName(date)) - return qsTr("Last online %1").arg(LocaleUtils.formatDate(lastSyncDate)) + return qsTr("Last online %1").arg(LocaleUtils.formatDate(date)) } - QtObject { - id: d - - readonly property var lastSyncDate: new Date(root.timestamp) - readonly property int millisecondsFromSync: lastSyncDate - Date.now() - readonly property int secondsFromSync: millisecondsFromSync / 1000 - readonly property int minutesFromSync: secondsFromSync / 60 - readonly property int daysFromSync: new Date().getDay() - lastSyncDate.getDay() - } + subTitleBadgeComponent: root.showOnlineBadge ? onlineBadgeComponent : null components: [ StatusButton { @@ -78,4 +73,34 @@ StatusListItem { color: Theme.palette.baseColor1 } ] + + QtObject { + id: d + + property real now: 0 + readonly property int deviceLastTimestamp: root.timestamp / 1000000 + readonly property int secondsFromSync: (now - Math.max(0, d.deviceLastTimestamp)) / 1000 + readonly property int minutesFromSync: secondsFromSync / 60 + readonly property int hoursFromSync: minutesFromSync / 60 + readonly property int daysFromSync: hoursFromSync / 24 + readonly property bool onlineNow: secondsFromSync <= 120 + } + + Timer { + interval: 1000 + repeat: true + triggeredOnStart: true + running: root.showOnlineBadge && root.visible + onTriggered: { + d.now = Date.now() + } + } + + Component { + id: onlineBadgeComponent + + StatusOnlineBadge { + online: d.onlineNow + } + } } diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index 9579a5f278..dd7bde4475 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -52,3 +52,4 @@ StatusStepper 0.1 StatusStepper.qml LoadingComponent 0.1 LoadingComponent.qml StatusQrCodeScanner 0.1 StatusQrCodeScanner.qml StatusSyncDeviceDelegate 0.1 StatusSyncDeviceDelegate.qml +StatusOnlineBadge 0.1 StatusOnlineBadge.qml diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 6cd3bedda0..e0891869ce 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -312,4 +312,8 @@ QtObject { function getMonthYear(value) { return formatDate(value, "MMM yyyy") } + + function getDayName(value) { + return Qt.locale().standaloneDayName(value.getDay()) + } } diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index e32805d048..ca2e9bb8b5 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -197,5 +197,6 @@ StatusQ/Core/Utils/ModelsComparator.qml StatusQ/Core/Utils/ModelChangeTracker.qml StatusQ/Components/StatusQrCodeScanner.qml + StatusQ/Components/StatusOnlineBadge.qml diff --git a/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml b/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml index 24d06fcd85..84b054b93a 100644 --- a/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml +++ b/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml @@ -14,7 +14,7 @@ Item { property string sectionTitle property int contentWidth - readonly property int contentHeight: (root.height - d.topHeaderHeight - d.titleRowHeight) + readonly property int contentHeight: root.height - titleRow.height - Style.current.padding property alias titleRowComponentLoader: loader property list headerComponents @@ -35,7 +35,6 @@ Item { QtObject { id: d - readonly property int topHeaderHeight: 56 readonly property int titleRowHeight: 56 } @@ -58,6 +57,7 @@ Item { id: titleRow width: root.contentWidth spacing: 0 + RowLayout { Layout.preferredWidth: (parent.width - Style.current.padding) Layout.preferredHeight: visible ? d.titleRowHeight : 0 diff --git a/ui/app/AppLayouts/Profile/views/SyncingView.qml b/ui/app/AppLayouts/Profile/views/SyncingView.qml index 37f755e019..91bcdf7593 100644 --- a/ui/app/AppLayouts/Profile/views/SyncingView.qml +++ b/ui/app/AppLayouts/Profile/views/SyncingView.qml @@ -17,6 +17,8 @@ import shared.panels 1.0 import shared.controls 1.0 import shared.controls.chat 1.0 +import SortFilterProxyModel 0.2 + import "../stores" import "../popups" import "../controls" @@ -29,37 +31,23 @@ SettingsContentBase { property ProfileStore profileStore property PrivacyStore privacyStore - property bool isSyncing: false - Component.onCompleted: { root.devicesStore.loadDevices() } ColumnLayout { + id: layout width: root.contentWidth spacing: Style.current.padding QtObject { id: d - /* - Device INFO: - id: "abcdabcd-1234-5678-9012-12a34b5cd678", - identity: "" - version: 1 - enabled: true - timestamp: 0 - metadata: - name: "MacBook-1" - deviceType: "macosx" - fcmToken: "" - */ - readonly property var instructionsModel: [ - qsTr("Verify your login with password or KeyCard"), - qsTr("Reveal a temporary QR and Sync Code") + "*", - qsTr("Share that information with your new device"), - ] + qsTr("Verify your login with password or KeyCard"), + qsTr("Reveal a temporary QR and Sync Code") + "*", + qsTr("Share that information with your new device"), + ] function personalizeDevice(model) { @@ -94,36 +82,45 @@ SettingsContentBase { StatusBaseText { Layout.fillWidth: true + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding text: qsTr("Devices") - font.pixelSize: 15 + font.pixelSize: Constants.settingsSection.subHeaderFontSize + color: Theme.palette.baseColor1 } StatusBaseText { Layout.fillWidth: true visible: root.devicesStore.devicesModule.devicesLoading + horizontalAlignment: Text.AlignHCenter text: qsTr("Loading devices...") } StatusBaseText { Layout.fillWidth: true visible: root.devicesStore.devicesModule.devicesLoadingError + horizontalAlignment: Text.AlignHCenter text: qsTr("Error loading devices. Please try again later.") + color: Theme.palette.dangerColor1 } - ListView { - id: listView + StatusListView { Layout.fillWidth: true - Layout.topMargin: 17 - Layout.bottomMargin: 17 - implicitHeight: contentHeight - spacing: Style.current.padding - model: root.devicesStore.devicesModel - + interactive: false + spacing: 0 visible: !root.devicesStore.devicesModule.devicesLoading && - !root.devicesStore.devicesModule.devicesLoadingError && - root.devicesStore.isDeviceSetup + !root.devicesStore.devicesModule.devicesLoadingError && + root.devicesStore.isDeviceSetup + + model: SortFilterProxyModel { + sourceModel: root.devicesStore.devicesModel + sorters: StringSorter { + roleName: "isCurrentDevice" + sortOrder: Qt.DescendingOrder + } + } delegate: StatusSyncDeviceDelegate { width: ListView.view.width @@ -218,7 +215,6 @@ SettingsContentBase { } StatusButton { -// type: StatusRoundButton.Type.Secondary Layout.alignment: Qt.AlignHCenter normalColor: Theme.palette.primaryColor1 hoverColor: Theme.palette.miscColor1; @@ -243,10 +239,9 @@ SettingsContentBase { StatusButton { id: backupBtn Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 17 text: qsTr("Backup Data") onClicked : { - let lastUpdate = root.privacyStore.backupData() * 1000 + const lastUpdate = root.privacyStore.backupData() * 1000 console.log("Backup done at: ", LocaleUtils.formatDateTime(lastUpdate)) } } @@ -269,5 +264,10 @@ SettingsContentBase { profileStore: root.profileStore } } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } } } diff --git a/ui/imports/shared/views/DeviceSyncingView.qml b/ui/imports/shared/views/DeviceSyncingView.qml index d1cd2f5828..aeb6984e07 100644 --- a/ui/imports/shared/views/DeviceSyncingView.qml +++ b/ui/imports/shared/views/DeviceSyncingView.qml @@ -130,6 +130,7 @@ Item { loading: d.pairingInProgress deviceName: qsTr("No device name") isCurrentDevice: false + showOnlineBadge: false } ErrorDetails { @@ -166,14 +167,14 @@ Item { implicitWidth: contentWidth + leftPadding + rightPadding implicitHeight: contentHeight + topPadding + bottomPadding - ListView { + StatusListView { id: listView width: scrollView.availableWidth height: scrollView.availableHeight - spacing: 4 clip: true + delegate: StatusSyncDeviceDelegate { width: ListView.view.width enabled: false