feat(wallet): extend API to continue loading activity

Bump status-go with the refactoring of hasMore and add offset
Add support for continuously loading activity in the wallet API.
Extend the debugging demo with continuously loading

Closes #10994
This commit is contained in:
Stefan 2023-06-11 16:22:53 +02:00 committed by Stefan Dunca
parent be6fe0633e
commit 4bcbe51154
5 changed files with 120 additions and 71 deletions

View File

@ -34,7 +34,7 @@ QtObject:
loadingData: bool
errorCode: backend_activity.ErrorCode
# TODO remove chains and addresses to use the app one
# TODO remove chains and addresses after using ground truth
addresses: seq[string]
chainIds: seq[int]
@ -138,15 +138,9 @@ QtObject:
self.errorCode = backend_activity.ErrorCode(errorCode)
self.errorCodeChanged()
proc refreshData(self: Controller) =
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error
return
self.setLoadingData(true)
proc processResponse(self: Controller, response: JsonNode) =
defer: self.setLoadingData(false)
let res = fromJson(response, backend_activity.FilterResponse)
defer: self.setErrorCode(res.errorCode.int)
@ -156,11 +150,23 @@ QtObject:
return
let entries = self.backendToPresentation(res.activities)
self.model.setEntries(entries, res.thereMightBeMore)
self.setLoadingData(false)
self.model.setEntries(entries, res.offset, res.hasMore)
proc updateFilter*(self: Controller) {.slot.} =
self.refreshData()
self.setLoadingData(true)
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error
self.setLoadingData(false)
return
proc loadMoreItems(self: Controller) {.slot.} =
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error
return
self.setLoadingData(true)
proc setFilterTime*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} =
self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp)

View File

@ -78,7 +78,7 @@ QtObject:
return self.transaction
proc getSender*(self: ActivityEntry): string {.slot.} =
# TODO: lookup sender's name from addressbook and cache it or in advance
# TODO: lookup sender's name
if self.isMultiTransaction():
return self.multi_transaction.fromAddress
@ -88,7 +88,7 @@ QtObject:
read = getSender
proc getRecipient*(self: ActivityEntry): string {.slot.} =
# TODO: lookup recipient name from addressbook and cache it or in advance
# TODO: lookup recipient name
if self.isMultiTransaction():
return self.multi_transaction.toAddress

View File

@ -1,8 +1,8 @@
import NimQml, Tables, strutils, strformat, sequtils
import NimQml, Tables, strutils, strformat, sequtils, logging
import ./entry
# TODO - DEV: remove this
# TODO - DEV: remove these imports and associated code after all the metadata is returned by the filter API
import app_service/service/transaction/dto
import app/modules/shared_models/currency_amount
import ../transactions/item as transaction
@ -71,15 +71,24 @@ QtObject:
self.hasMore = hasMore
self.hasMoreChanged()
proc setEntries*(self: Model, entries: seq[ActivityEntry], hasMore: bool) =
self.beginResetModel()
self.entries = entries
self.endResetModel()
proc setEntries*(self: Model, newEntries: seq[ActivityEntry], offset: int, hasMore: bool) =
if offset == 0:
self.beginResetModel()
self.entries = newEntries
self.endResetModel()
else:
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
if offset != self.entries.len:
error "offset != self.entries.len"
return
self.beginInsertRows(parentModelIndex, self.entries.len, self.entries.len + newEntries.len - 1)
self.entries.add(newEntries)
self.endInsertRows()
self.countChanged()
self.setHasMore(hasMore)
# TODO: fetch more
proc getHasMore*(self: Model): bool {.slot.} =
return self.hasMore

View File

@ -157,7 +157,8 @@ type
FilterResponse* = object
activities*: seq[ActivityEntry]
thereMightBeMore*: bool
offset*: int
hasMore*: bool
errorCode*: ErrorCode
# Define toJson proc for PayloadType
@ -198,7 +199,8 @@ proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inlin
result = T(
activities: backendEntities,
thereMightBeMore: if e.hasKey("thereMightBeMore"): e["thereMightBeMore"].getBool()
offset: e["offset"].getInt(),
hasMore: if e.hasKey("hasMore"): e["hasMore"].getBool()
else: false,
errorCode: ErrorCode(e["errorCode"].getInt())
)

View File

@ -321,24 +321,19 @@ Control {
}
}
Text {
id: loadingText
Layout.fillWidth: true
Layout.fillHeight: true
text: qsTr("Loading...")
visible: controller.loadingData
horizontalAlignment: Text.AlignHCenter
}
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
Component.onCompleted: {
if(controller.model.hasMore) {
controller.loadMoreItems();
}
}
model: controller.model
visible: !controller.loadingData
delegate: Item {
width: parent ? parent.width : 0
@ -346,55 +341,92 @@ Control {
readonly property var entry: model.activityEntry
RowLayout {
ColumnLayout {
id: itemLayout
anchors.fill: parent
spacing: 5
Label { text: entry.isMultiTransaction ? qsTr("MT") : entry.isPendingTransaction ? qsTr("PT") : qsTr(" T") }
Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` }
Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount }
Label { text: qsTr("from"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: qsTr("to"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: qsTr("got"); Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label {
text: `{${
function() {
switch (entry.status) {
case Constants.TransactionStatus.Failed: return qsTr("F");
case Constants.TransactionStatus.Pending: return qsTr("P");
case Constants.TransactionStatus.Complete: return qsTr("C");
case Constants.TransactionStatus.Finalized: return qsTr("FZ");
}
return qsTr("-")
}()}}`
Layout.leftMargin: 5;
RowLayout {
Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount }
Label { text: qsTr("from"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: qsTr("to"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: qsTr("got"); Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
RowLayout {} // Spacer
}
RowLayout {
Label { text: entry.isMultiTransaction ? qsTr("MT") : entry.isPendingTransaction ? qsTr("PT") : qsTr(" T") }
Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` }
Label {
text: `{${
function() {
switch (entry.status) {
case Constants.TransactionStatus.Failed: return qsTr("Failed");
case Constants.TransactionStatus.Pending: return qsTr("Pending");
case Constants.TransactionStatus.Complete: return qsTr("Complete");
case Constants.TransactionStatus.Finalized: return qsTr("Finalized");
}
return qsTr("-")
}()}}`
Layout.leftMargin: 5;
}
RowLayout {} // Spacer
}
Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
RowLayout {} // Spacer
}
}
footer: Component {
Item {
width: listView.width
height: footerText.implicitHeight
onContentYChanged: checkIfFooterVisible()
onHeightChanged: checkIfFooterVisible()
onContentHeightChanged: checkIfFooterVisible()
Connections {
target: listView.footerItem
function onHeightChanged() {
listView.checkIfFooterVisible()
}
}
function checkIfFooterVisible() {
if((contentY + height) > (contentHeight - footerItem.height) && controller.model.hasMore && !controller.loadingData) {
controller.loadMoreItems();
}
}
footer: Column {
id: loadingItems
width: listView.width
visible: controller.model.hasMore
Repeater {
model: controller.model.hasMore ? 10 : 0
Text {
id: footerText
text: qsTr("Loading more items...")
anchors.centerIn: parent
text: loadingItems.loadingPattern
}
}
visible: controller.model.hasMore
property string loadingPattern: ""
property int glanceOffset: 0
Timer {
interval: 25; repeat: true; running: true
// Load more items when this footer comes into view.
onVisibleChanged: {
if (visible) {
controller.loadMoreItems();
onTriggered: {
let offset = loadingItems.glanceOffset
let length = 100
let slashCount = 3;
let pattern = new Array(length).fill(' ');
for (let i = 0; i < slashCount; i++) {
let position = (offset + i) % length;
pattern[position] = '/';
}
pattern = '[' + pattern.join('') + ']';
loadingItems.loadingPattern = pattern;
loadingItems.glanceOffset = (offset + 1) % length;
}
}
}