510 lines
18 KiB
QML
510 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
|
|
|
|
signal launchTransactionDetail(var transaction, int entryIndex)
|
|
|
|
function resetView() {
|
|
if (!!filterPanelLoader.item) {
|
|
filterPanelLoader.item.resetView()
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (!visible)
|
|
return
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
enabled: root.visible
|
|
}
|
|
|
|
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: false
|
|
asynchronous: true
|
|
Layout.fillWidth: true
|
|
sourceComponent: ActivityFilterPanel {
|
|
visible: d.isInitialLoading || transactionListRoot.count > 0 || WalletStores.RootStore.currentActivityFiltersStore.filtersSet
|
|
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
|
|
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 {
|
|
launchTransactionDetail(modelData, 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()
|
|
}
|
|
}
|
|
}
|
|
}
|