mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-26 14:32:01 +00:00
dbd7937d8b
This commit handles saved addresses changes and reflect them to the history view and tx details view. In this context it handles the same way changes coming from sync devices. Also fix the issue when switching network mode. Closes: #13095
511 lines
18 KiB
QML
511 lines
18 KiB
QML
import QtQuick 2.15
|
|
import QtQml 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Components 0.1
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Popups 0.1
|
|
|
|
import SortFilterProxyModel 0.2
|
|
|
|
import utils 1.0
|
|
|
|
import "../panels"
|
|
import "../popups"
|
|
import "../popups/send"
|
|
import "../stores"
|
|
import "../controls"
|
|
|
|
import AppLayouts.Wallet.stores 1.0 as WalletStores
|
|
import AppLayouts.Wallet.popups 1.0
|
|
import AppLayouts.Wallet.controls 1.0
|
|
import AppLayouts.Wallet.panels 1.0
|
|
|
|
ColumnLayout {
|
|
id: root
|
|
|
|
property var overview
|
|
property bool showAllAccounts: false
|
|
property var sendModal
|
|
property bool filterVisible
|
|
|
|
property var selectedTransaction
|
|
|
|
signal launchTransactionDetail(int entryIndex)
|
|
|
|
function resetView() {
|
|
if (!!filterPanelLoader.item) {
|
|
filterPanelLoader.item.resetView()
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
filterPanelLoader.active = true
|
|
if (RootStore.transactionActivityStatus.isFilterDirty) {
|
|
WalletStores.RootStore.currentActivityFiltersStore.applyAllFilters()
|
|
}
|
|
|
|
WalletStores.RootStore.currentActivityFiltersStore.updateCollectiblesModel()
|
|
WalletStores.RootStore.currentActivityFiltersStore.updateRecipientsModel()
|
|
}
|
|
|
|
Connections {
|
|
target: RootStore.transactionActivityStatus
|
|
enabled: root.visible
|
|
function onIsFilterDirtyChanged() {
|
|
RootStore.updateTransactionFilter()
|
|
}
|
|
function onFilterChainsChanged() {
|
|
WalletStores.RootStore.currentActivityFiltersStore.updateCollectiblesModel()
|
|
WalletStores.RootStore.currentActivityFiltersStore.updateRecipientsModel()
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: d
|
|
readonly property bool isInitialLoading: RootStore.loadingHistoryTransactions && transactionListRoot.count === 0
|
|
|
|
readonly property int loadingSectionWidth: 56
|
|
readonly property int topSectionMargin: 20
|
|
|
|
property double lastRefreshTime
|
|
readonly property int maxSecondsBetweenRefresh: 3
|
|
function refreshData() {
|
|
RootStore.resetFilter()
|
|
d.lastRefreshTime = Date.now()
|
|
newTransactions.visible = false
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: nonArchivalNodeError
|
|
Layout.alignment: Qt.AlignTop
|
|
|
|
visible: RootStore.isNonArchivalNode
|
|
text: qsTr("Status Desktop is connected to a non-archival node. Transaction history may be incomplete.")
|
|
font.pixelSize: Style.current.primaryTextFontSize
|
|
color: Style.current.danger
|
|
}
|
|
|
|
ShapeRectangle {
|
|
id: noTxs
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 42
|
|
visible: !d.isInitialLoading && !WalletStores.RootStore.currentActivityFiltersStore.filtersSet && transactionListRoot.count === 0
|
|
font.pixelSize: Style.current.primaryTextFontSize
|
|
text: qsTr("Activity for this account will appear here")
|
|
}
|
|
|
|
Loader {
|
|
id: filterPanelLoader
|
|
active: root.filterVisible && (d.isInitialLoading || transactionListRoot.count > 0 || WalletStores.RootStore.currentActivityFiltersStore.filtersSet)
|
|
visible: active
|
|
asynchronous: true
|
|
Layout.fillWidth: true
|
|
sourceComponent: ActivityFilterPanel {
|
|
activityFilterStore: WalletStores.RootStore.currentActivityFiltersStore
|
|
store: WalletStores.RootStore
|
|
hideNoResults: newTransactions.visible
|
|
isLoading: d.isInitialLoading
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.alignment: Qt.AlignTop
|
|
Layout.topMargin: nonArchivalNodeError.visible || noTxs.visible ? Style.current.padding : 0
|
|
Layout.bottomMargin: Style.current.padding
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
Rectangle { // Shadow behind delegates when scrolling
|
|
anchors.top: topListSeparator.bottom
|
|
width: parent.width
|
|
height: 4
|
|
color: Style.current.separator
|
|
visible: topListSeparator.visible
|
|
}
|
|
|
|
StatusListView {
|
|
id: transactionListRoot
|
|
objectName: "walletAccountTransactionList"
|
|
anchors.fill: parent
|
|
|
|
model: SortFilterProxyModel {
|
|
id: txModel
|
|
|
|
sourceModel: RootStore.historyTransactions
|
|
|
|
// LocaleUtils is not accessable from inside expression, but local function works
|
|
property var daysTo: (d1, d2) => LocaleUtils.daysTo(d1, d2)
|
|
property var daysBetween: (d1, d2) => LocaleUtils.daysBetween(d1, d2)
|
|
property var getFirstDayOfTheCurrentWeek: () => LocaleUtils.getFirstDayOfTheCurrentWeek()
|
|
proxyRoles: ExpressionRole {
|
|
name: "date"
|
|
expression: {
|
|
if (model.activityEntry.timestamp === 0)
|
|
return ""
|
|
const currDate = new Date()
|
|
const timestampDate = new Date(model.activityEntry.timestamp * 1000)
|
|
const daysDiff = txModel.daysBetween(currDate, timestampDate)
|
|
const daysToBeginingOfThisWeek = txModel.daysTo(timestampDate, txModel.getFirstDayOfTheCurrentWeek())
|
|
|
|
if (daysDiff < 1) {
|
|
return qsTr("Today")
|
|
} else if (daysDiff < 2) {
|
|
return qsTr("Yesterday")
|
|
} else if (daysToBeginingOfThisWeek >= 0) {
|
|
return qsTr("Earlier this week")
|
|
} else if (daysToBeginingOfThisWeek > -7) {
|
|
return qsTr("Last week")
|
|
} else if (currDate.getMonth() === timestampDate.getMonth() && currDate.getYear() === timestampDate.getYear()) {
|
|
return qsTr("Earlier this month")
|
|
}
|
|
|
|
const previousMonthDate = (new Date(new Date().setDate(0)))
|
|
// Special case for the end of the year
|
|
if ((timestampDate.getMonth() === previousMonthDate.getMonth() && timestampDate.getYear() === previousMonthDate.getYear())
|
|
|| (previousMonthDate.getMonth() === 11 && timestampDate.getMonth() === 0 && Math.abs(timestampDate.getYear() - previousMonthDate.getYear()) === 1))
|
|
{
|
|
return qsTr("Last month")
|
|
}
|
|
|
|
return timestampDate.toLocaleDateString(Qt.locale(), "MMM yyyy")
|
|
}
|
|
}
|
|
}
|
|
|
|
delegate: transactionDelegate
|
|
|
|
headerPositioning: ListView.OverlayHeader
|
|
footer: footerComp
|
|
|
|
ScrollBar.vertical: StatusScrollBar {}
|
|
|
|
section.property: "date"
|
|
topMargin: d.isInitialLoading ? 0 : -d.topSectionMargin // Top margin for first section. Section cannot have different sizes
|
|
section.delegate: ColumnLayout {
|
|
id: sectionDelegate
|
|
|
|
width: ListView.view.width
|
|
height: 58
|
|
spacing: 0
|
|
|
|
required property string section
|
|
|
|
Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: d.topSectionMargin
|
|
implicitHeight: 1
|
|
}
|
|
|
|
StatusBaseText {
|
|
id: sectionText
|
|
Layout.alignment: Qt.AlignBottom
|
|
leftPadding: Style.current.padding
|
|
bottomPadding: Style.current.halfPadding
|
|
text: parent.section
|
|
font.pixelSize: 13
|
|
verticalAlignment: Qt.AlignBottom
|
|
}
|
|
}
|
|
|
|
visibleArea.onYPositionChanged: tryFetchMoreTransactions()
|
|
|
|
Connections {
|
|
target: RootStore
|
|
function onLoadingHistoryTransactionsChanged() {
|
|
// Calling timer instead directly to not cause binding loop
|
|
if (!RootStore.loadingHistoryTransactions)
|
|
fetchMoreTimer.start()
|
|
}
|
|
}
|
|
|
|
function tryFetchMoreTransactions() {
|
|
if (d.isInitialLoading || !footerItem || !RootStore.historyTransactions.hasMore)
|
|
return
|
|
const footerYPosition = footerItem.height / contentHeight
|
|
if (footerYPosition >= 1.0) {
|
|
return
|
|
}
|
|
|
|
// On startup, first loaded ListView will have heightRatio equal 0
|
|
if (footerYPosition + visibleArea.yPosition + visibleArea.heightRatio > 1.0) {
|
|
RootStore.fetchMoreTransactions()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: fetchMoreTimer
|
|
interval: 1
|
|
onTriggered: transactionListRoot.tryFetchMoreTransactions()
|
|
}
|
|
}
|
|
|
|
StatusButton {
|
|
id: newTransactions
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: parent.top
|
|
anchors.topMargin: Style.current.halfPadding
|
|
|
|
text: qsTr("New transactions")
|
|
|
|
visible: false
|
|
onClicked: d.refreshData()
|
|
|
|
icon.name: "arrow-up"
|
|
|
|
radius: 36
|
|
type: StatusButton.Primary
|
|
size: StatusBaseButton.Size.Tiny
|
|
}
|
|
|
|
Separator {
|
|
id: topListSeparator
|
|
width: parent.width
|
|
visible: !transactionListRoot.atYBeginning
|
|
}
|
|
|
|
|
|
Connections {
|
|
target: RootStore
|
|
|
|
function onNewDataAvailableChanged() {
|
|
if (!d.lastRefreshTime || ((Date.now() - d.lastRefreshTime) > (1000 * d.maxSecondsBetweenRefresh))) {
|
|
// Show `New transactions` button only when filter is applied
|
|
if (!WalletStores.RootStore.currentActivityFiltersStore.filtersSet) {
|
|
d.refreshData()
|
|
return
|
|
}
|
|
|
|
newTransactions.visible = RootStore.newDataAvailable
|
|
return
|
|
}
|
|
|
|
if (showRefreshButtonTimer.running) {
|
|
if (!RootStore.newDataAvailable) {
|
|
showRefreshButtonTimer.stop()
|
|
newTransactions.visible = false
|
|
}
|
|
} else if(RootStore.newDataAvailable) {
|
|
showRefreshButtonTimer.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: showRefreshButtonTimer
|
|
|
|
interval: 2000
|
|
running: false
|
|
repeat: false
|
|
onTriggered: newTransactions.visible = RootStore.newDataAvailable
|
|
}
|
|
}
|
|
|
|
StatusMenu {
|
|
id: delegateMenu
|
|
|
|
hideDisabledItems: true
|
|
|
|
property var transaction
|
|
property var transactionDelegate
|
|
|
|
function openMenu(delegate, mouse, data) {
|
|
if (!delegate || !data)
|
|
return
|
|
|
|
delegateMenu.transactionDelegate = delegate
|
|
delegateMenu.transaction = data
|
|
popup(delegate, mouse.x, mouse.y)
|
|
}
|
|
|
|
onClosed: {
|
|
delegateMenu.transaction = null
|
|
delegateMenu.transactionDelegate = null
|
|
}
|
|
|
|
StatusAction {
|
|
id: repeatTransactionAction
|
|
|
|
text: qsTr("Repeat transaction")
|
|
icon.name: "rotate"
|
|
|
|
property alias tx: delegateMenu.transaction
|
|
|
|
enabled: {
|
|
if (!overview.isWatchOnlyAccount && !tx)
|
|
return false
|
|
return WalletStores.RootStore.isTxRepeatable(tx)
|
|
}
|
|
|
|
onTriggered: {
|
|
if (!tx)
|
|
return
|
|
let asset = WalletStores.RootStore.getAssetForSendTx(tx)
|
|
|
|
let req = Helpers.lookupAddressesForSendModal(tx.sender, tx.recipient, asset, tx.isNFT, tx.amount)
|
|
|
|
root.sendModal.preSelectedAccount = req.preSelectedAccount
|
|
root.sendModal.preSelectedRecipient = req.preSelectedRecipient
|
|
root.sendModal.preSelectedRecipientType = req.preSelectedRecipientType
|
|
root.sendModal.preSelectedHolding = req.preSelectedHolding
|
|
root.sendModal.preSelectedHoldingID = req.preSelectedHoldingID
|
|
root.sendModal.preSelectedHoldingType = req.preSelectedHoldingType
|
|
root.sendModal.preSelectedSendType = req.preSelectedSendType
|
|
root.sendModal.preDefinedAmountToSend = req.preDefinedAmountToSend
|
|
root.sendModal.onlyAssets = false
|
|
root.sendModal.open()
|
|
}
|
|
}
|
|
StatusSuccessAction {
|
|
text: qsTr("Copy details")
|
|
successText: qsTr("Details copied")
|
|
icon.name: "copy"
|
|
onTriggered: {
|
|
if (!delegateMenu.transactionDelegate)
|
|
return
|
|
WalletStores.RootStore.addressWasShown(delegateMenu.transaction.sender)
|
|
if (delegateMenu.transaction.sender !== delegateMenu.transaction.recipient) {
|
|
WalletStores.RootStore.addressWasShown(delegateMenu.transaction.recipient)
|
|
}
|
|
RootStore.copyToClipboard(delegateMenu.transactionDelegate.getDetailsString())
|
|
}
|
|
}
|
|
StatusMenuSeparator {
|
|
visible: filterAction.enabled
|
|
}
|
|
StatusAction {
|
|
id: filterAction
|
|
text: qsTr("Filter by similar")
|
|
icon.name: "filter"
|
|
onTriggered: {
|
|
const store = WalletStores.RootStore.currentActivityFiltersStore
|
|
const tx = delegateMenu.transaction
|
|
|
|
store.autoUpdateFilter = false
|
|
store.resetAllFilters()
|
|
|
|
const currentAddress = overview.mixedcaseAddress.toUpperCase()
|
|
|
|
store.toggleType(tx.txType)
|
|
// Contract deployment has always ETH symbol. Symbol doesn't affect this type
|
|
if (tx.txType !== Constants.TransactionType.ContractDeployment) {
|
|
const symbol = tx.symbol
|
|
if (!!symbol)
|
|
store.toggleToken(symbol)
|
|
const inSymbol = tx.inSymbol
|
|
if (!!inSymbol && inSymbol !== symbol)
|
|
store.toggleToken(inSymbol)
|
|
}
|
|
if (showAllAccounts || tx.txType !== Constants.TransactionType.Bridge) {
|
|
const recipient = tx.recipient.toUpperCase()
|
|
if (!!recipient && recipient !== currentAddress && !/0X0+$/.test(recipient))
|
|
store.toggleRecents(recipient)
|
|
}
|
|
if (tx.isNFT) {
|
|
const uid = store.collectiblesList.getUidForData(tx.tokenID, tx.tokenAddress, tx.chainId)
|
|
if (!!uid)
|
|
store.toggleCollectibles(uid)
|
|
}
|
|
|
|
store.autoUpdateFilter = true
|
|
store.applyAllFilters()
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: transactionDelegate
|
|
TransactionDelegate {
|
|
required property var model
|
|
required property int index
|
|
width: ListView.view.width
|
|
modelData: model.activityEntry
|
|
timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000, true) : ""
|
|
rootStore: RootStore
|
|
walletRootStore: WalletStores.RootStore
|
|
showAllAccounts: root.showAllAccounts
|
|
onClicked: {
|
|
if (mouse.button === Qt.RightButton) {
|
|
delegateMenu.openMenu(this, mouse, modelData)
|
|
} else {
|
|
root.selectedTransaction = Qt.binding(() => model.activityEntry)
|
|
launchTransactionDetail(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: footerComp
|
|
ColumnLayout {
|
|
id: footerColumn
|
|
readonly property bool allActivityLoaded: !RootStore.historyTransactions.hasMore && transactionListRoot.count !== 0
|
|
width: root.width
|
|
spacing: d.isInitialLoading ? 6 : 12
|
|
|
|
Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.current.halfPadding
|
|
visible: d.isInitialLoading
|
|
}
|
|
|
|
StatusTextWithLoadingState {
|
|
Layout.alignment: Qt.AlignLeft
|
|
Layout.leftMargin: Style.current.padding
|
|
text: "01.01.2000"
|
|
width: d.loadingSectionWidth
|
|
font.pixelSize: 15
|
|
loading: visible
|
|
visible: d.isInitialLoading
|
|
}
|
|
|
|
Repeater {
|
|
model: {
|
|
if (!root.visible)
|
|
return 0
|
|
if (!noTxs.visible) {
|
|
const delegateHeight = 64 + footerColumn.spacing
|
|
if (d.isInitialLoading) {
|
|
return Math.floor(transactionListRoot.height / delegateHeight)
|
|
} else if (RootStore.historyTransactions.hasMore) {
|
|
return Math.max(3, Math.floor(transactionListRoot.height / delegateHeight) - 3)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
TransactionDelegate {
|
|
Layout.fillWidth: true
|
|
rootStore: RootStore
|
|
walletRootStore: WalletStores.RootStore
|
|
loading: true
|
|
}
|
|
}
|
|
|
|
Separator {
|
|
Layout.topMargin: Style.current.bigPadding
|
|
Layout.fillWidth: true
|
|
visible: footerColumn.allActivityLoaded
|
|
}
|
|
StatusBaseText {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("You have reached the beginning of the activity for this account")
|
|
font.pixelSize: 13
|
|
color: Theme.palette.baseColor1
|
|
visible: footerColumn.allActivityLoaded
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
StatusButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: qsTr("Back to most recent transaction")
|
|
visible: footerColumn.allActivityLoaded && transactionListRoot.contentHeight > transactionListRoot.height
|
|
onClicked: transactionListRoot.positionViewAtBeginning()
|
|
}
|
|
}
|
|
}
|
|
}
|