Skip to content

Commit

Permalink
feat(activity): add incremental updates to current activity filter
Browse files Browse the repository at this point in the history
Switch the activity filter to use the new session-based API that
deliver incremental updates to the current filter.

Drop the old quick win listening for individual change events and
use the unified API instead.

The new transactions (on-top) trigger the old "new transactions" buttons
that trigger reset of the current filter and the top new transacitons
highlighted.

Highlight mixed changes (not new on top) as they come in

Highlight new changes on filter reset

Closes #12120
  • Loading branch information
stefandunca committed Feb 29, 2024
1 parent f934615 commit 3daed67
Show file tree
Hide file tree
Showing 17 changed files with 429 additions and 222 deletions.
118 changes: 71 additions & 47 deletions src/app/modules/main/wallet_section/activity/controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ QtObject:
# call updateAssetsIdentities after updating chainIds
chainIds: seq[int]

requestId: int32

proc setup(self: Controller) =
self.QObject.setup

Expand All @@ -87,31 +85,9 @@ QtObject:
QtProperty[QVariant] collectiblesModel:
read = getCollectiblesModel

proc buildMultiTransactionExtraData(self: Controller, metadata: backend_activity.ActivityEntry): ExtraData =
if metadata.symbolIn.isSome():
result.inAmount = self.currencyService.parseCurrencyValue(metadata.symbolIn.get(), metadata.amountIn)
if metadata.symbolOut.isSome():
result.outAmount = self.currencyService.parseCurrencyValue(metadata.symbolOut.get(), metadata.amountOut)

proc buildTransactionExtraData(self: Controller, metadata: backend_activity.ActivityEntry): ExtraData =
if metadata.symbolIn.isSome() or metadata.amountIn > 0:
result.inAmount = self.currencyService.parseCurrencyValue(metadata.symbolIn.get(""), metadata.amountIn)
if metadata.symbolOut.isSome() or metadata.amountOut > 0:
result.outAmount = self.currencyService.parseCurrencyValue(metadata.symbolOut.get(""), metadata.amountOut)

proc backendToPresentation(self: Controller, backendEntities: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] =
let amountToCurrencyConvertor = proc(amount: UInt256, symbol: string): CurrencyAmount =
return currencyAmountToItem(self.currencyService.parseCurrencyValue(symbol, amount),
self.currencyService.getCurrencyFormat(symbol))
for backendEntry in backendEntities:
var ae: entry.ActivityEntry
case backendEntry.getPayloadType():
of MultiTransaction:
let extraData = self.buildMultiTransactionExtraData(backendEntry)
ae = entry.newMultiTransactionActivityEntry(backendEntry, extraData, amountToCurrencyConvertor)
of SimpleTransaction, PendingTransaction:
let extraData = self.buildTransactionExtraData(backendEntry)
ae = entry.newTransactionActivityEntry(backendEntry, self.addresses, extraData, amountToCurrencyConvertor)
let ae = entry.newActivityEntry(backendEntry, self.addresses, self.currencyService)
result.add(ae)

proc fetchTxDetails*(self: Controller, entryIndex: int) {.slot.} =
Expand Down Expand Up @@ -154,45 +130,77 @@ QtObject:

self.model.setEntries(entries, res.offset, res.hasMore)

if len(entries) > 0:
self.eventsHandler.updateRelevantTimestamp(entries[len(entries) - 1].getTimestamp())
if res.offset == 0:
self.status.setNewDataAvailable(false)

proc updateFilter*(self: Controller) {.slot.} =
proc sessionId(self: Controller): int32 =
return self.eventsHandler.getSessionId()

proc invalidateData(self: Controller) =
self.status.setLoadingData(true)
self.status.setIsFilterDirty(false)

self.model.resetModel(@[])

self.eventsHandler.updateSubscribedAddresses(self.addresses)
self.eventsHandler.updateSubscribedChainIDs(self.chainIds)
self.status.setNewDataAvailable(false)

let response = backend_activity.filterActivityAsync(self.requestId, self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)

# Stops the old session and starts a new one. All the incremental changes are lost
proc newFilterSession*(self: Controller) {.slot.} =
self.invalidateData()

# stop the previous filter session
if self.eventsHandler.hasSessionId():
let res = backend_activity.stopActivityFilterSession(self.sessionId())
if res.error != nil:
error "error stopping the previous session of activity fitlering: ", res.error
self.eventsHandler.clearSessionId()

# start a new filter session
let (sessionId, ok) = backend_activity.newActivityFilterSession(self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, FETCH_BATCH_COUNT_DEFAULT)
if not ok:
self.status.setLoadingData(false)
return

self.eventsHandler.setSessionId(sessionId)

proc updateFilter*(self: Controller) {.slot.} =
self.invalidateData()

if not backend_activity.updateFilterForSession(self.sessionId(), self.currentActivityFilter, FETCH_BATCH_COUNT_DEFAULT):
self.status.setLoadingData(false)
error "error updating activity filter"
return

proc resetActivityData*(self: Controller) {.slot.} =
self.invalidateData()

let response = backend_activity.resetActivityFilterSession(self.sessionId(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error
self.status.setLoadingData(false)
error "error fetching activity entries from start: ", response.error
return

proc loadMoreItems(self: Controller) {.slot.} =
self.status.setLoadingData(true)

let response = backend_activity.filterActivityAsync(self.requestId, self.addresses, self.allAddressesSelected, seq[backend_activity.ChainId](self.chainIds), self.currentActivityFilter, self.model.getCount(), FETCH_BATCH_COUNT_DEFAULT)
let response = backend_activity.getMoreForActivityFilterSession(self.sessionId(), FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
self.status.setLoadingData(false)
error "error fetching activity entries: ", response.error
error "error fetching more activity entries: ", response.error
return

proc updateCollectiblesModel*(self: Controller) {.slot.} =
self.status.setLoadingCollectibles(true)
let res = backend_activity.getActivityCollectiblesAsync(self.requestId, self.chainIds, self.addresses, 0, FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
let res = backend_activity.getActivityCollectiblesAsync(self.sessionId(), self.chainIds, self.addresses, 0, FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingCollectibles(false)
error "error fetching collectibles: ", res.error
return

proc loadMoreCollectibles*(self: Controller) {.slot.} =
self.status.setLoadingCollectibles(true)
let res = backend_activity.getActivityCollectiblesAsync(self.requestId, self.chainIds, self.addresses, self.collectiblesModel.getCount(), FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
let res = backend_activity.getActivityCollectiblesAsync(self.sessionId(), self.chainIds, self.addresses, self.collectiblesModel.getCount(), FETCH_COLLECTIBLES_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingCollectibles(false)
error "error fetching collectibles: ", res.error
Expand Down Expand Up @@ -221,7 +229,7 @@ QtObject:
proc updateStartTimestamp*(self: Controller) {.slot.} =
self.status.setLoadingStartTimestamp(true)

let resJson = backend_activity.getOldestActivityTimestampAsync(self.requestId, self.addresses)
let resJson = backend_activity.getOldestActivityTimestampAsync(self.sessionId(), self.addresses)
if resJson.error != nil:
self.status.setLoadingStartTimestamp(false)
error "error requesting oldest activity timestamp: ", resJson.error
Expand Down Expand Up @@ -265,6 +273,24 @@ QtObject:
self.model.updateEntries(entries)
)

self.eventsHandler.onFilteringSessionUpdated(proc (jn: JsonNode) =
if jn.kind != JObject:
error "expected an object"

let res = fromJson(jn, backend_activity.SessionUpdate)

var updated = newSeq[backend_activity.ActivityEntry](len(res.`new`))
var indices = newSeq[int](len(res.`new`))
for i in 0 ..< len(res.`new`):
updated[i] = res.`new`[i].entry
indices[i] = res.`new`[i].pos

self.status.setNewDataAvailable(res.hasNewOnTop)
if len(res.`new`) > 0:
let entries = self.backendToPresentation(updated)
self.model.addNewEntries(entries, indices)
)

self.eventsHandler.onGetRecipientsDone(proc (jsonObj: JsonNode) =
defer: self.status.setLoadingRecipients(false)
let res = fromJson(jsonObj, backend_activity.GetRecipientsResponse)
Expand Down Expand Up @@ -302,26 +328,20 @@ QtObject:
error "Error converting activity entries: ", e.msg
)

self.eventsHandler.onNewDataAvailable(proc () =
self.status.setNewDataAvailable(true)
)

proc newController*(requestId: int32,
currencyService: currency_service.Service,
proc newController*(currencyService: currency_service.Service,
tokenService: token_service.Service,
savedAddressService: saved_address_service.Service,
events: EventEmitter): Controller =
new(result, delete)

result.requestId = requestId
result.model = newModel()
result.recipientsModel = newRecipientsModel()
result.collectiblesModel = newCollectiblesModel()
result.tokenService = tokenService
result.savedAddressService = savedAddressService
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()

result.eventsHandler = newEventsHandler(result.requestId, events)
result.eventsHandler = newEventsHandler(events)
result.status = newStatus()

result.currencyService = currencyService
Expand Down Expand Up @@ -461,7 +481,7 @@ QtObject:

proc updateRecipientsModel*(self: Controller) {.slot.} =
self.status.setLoadingRecipients(true)
let res = backend_activity.getRecipientsAsync(self.requestId, self.chainIds, self.addresses, 0, FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
let res = backend_activity.getRecipientsAsync(self.sessionId(), self.chainIds, self.addresses, 0, FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
if res.error != nil or res.result.kind != JBool:
self.status.setLoadingRecipients(false)
error "error fetching recipients: ", res.error, "; kind ", res.result.kind
Expand All @@ -473,7 +493,7 @@ QtObject:

proc loadMoreRecipients(self: Controller) {.slot.} =
self.status.setLoadingRecipients(true)
let res = backend_activity.getRecipientsAsync(self.requestId, self.chainIds, self.addresses, self.recipientsModel.getCount(), FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
let res = backend_activity.getRecipientsAsync(self.sessionId(), self.chainIds, self.addresses, self.recipientsModel.getCount(), FETCH_RECIPIENTS_BATCH_COUNT_DEFAULT)
if res.error != nil:
self.status.setLoadingRecipients(false)
error "error fetching more recipient entries: ", res.error
Expand All @@ -492,8 +512,12 @@ QtObject:
proc globalFilterChanged*(self: Controller, addresses: seq[string], allAddressesSelected: bool, chainIds: seq[int], allChainsEnabled: bool) =
if (self.addresses == addresses and self.allAddressesSelected == allAddressesSelected and self.chainIds == chainIds):
return

self.setFilterAddresses(addresses, allAddressesSelected)
self.setFilterChains(chainIds, allChainsEnabled)

# Every change of chains and addresses have to start a new session to get incremental updates when filter is cleared
self.newFilterSession()

proc noLimitTimestamp*(self: Controller): int {.slot.} =
return backend_activity.noLimitTimestampForPeriod
81 changes: 63 additions & 18 deletions src/app/modules/main/wallet_section/activity/entry.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import app/global/global_singleton

import app_service/service/currency/service

import app/modules/shared/wallet_utils

import web3/ethtypes as eth

# Additional data needed to build an Entry, which is
Expand All @@ -17,13 +19,10 @@ type
inAmount*: float64
outAmount*: float64

AmountToCurrencyConvertor* = proc (amount: UInt256, symbol: string): CurrencyAmount

# Used to display an activity history header entry in the QML UI
QtObject:
type
ActivityEntry* = ref object of QObject
valueConvertor: AmountToCurrencyConvertor
metadata: backend.ActivityEntry
extradata: ExtraData

Expand All @@ -33,6 +32,9 @@ QtObject:
nftName: string
nftImageURL: string

# true for entries that were changed/added in the current session
highlight: bool

proc setup(self: ActivityEntry) =
self.QObject.setup

Expand All @@ -42,32 +44,61 @@ QtObject:
proc isInTransactionType(self: ActivityEntry): bool =
return self.metadata.activityType == backend.ActivityType.Receive or self.metadata.activityType == backend.ActivityType.Mint

proc newMultiTransactionActivityEntry*(metadata: backend.ActivityEntry, extradata: ExtraData, valueConvertor: AmountToCurrencyConvertor): ActivityEntry =
proc extractCurrencyAmount(self: ActivityEntry, currencyService: Service): CurrencyAmount =
let amount = if self.isInTransactionType(): self.metadata.amountIn else: self.metadata.amountOut
let symbol = if self.isInTransactionType(): self.metadata.symbolIn.get("") else: self.metadata.symbolOut.get("")
return currencyAmountToItem(
currencyService.parseCurrencyValue(symbol, amount),
currencyService.getCurrencyFormat(symbol),
)

proc newMultiTransactionActivityEntry*(metadata: backend.ActivityEntry, extradata: ExtraData, currencyService: Service): ActivityEntry =
new(result, delete)
result.valueConvertor = valueConvertor
result.metadata = metadata
result.extradata = extradata
result.noAmount = newCurrencyAmount()
result.amountCurrency = valueConvertor(
if result.isInTransactionType(): metadata.amountIn else: metadata.amountOut,
if result.isInTransactionType(): metadata.symbolIn.get("") else: metadata.symbolOut.get(""),
)

result.amountCurrency = result.extractCurrencyAmount(currencyService)

result.highlight = metadata.isNew

result.setup()

proc newTransactionActivityEntry*(metadata: backend.ActivityEntry, fromAddresses: seq[string], extradata: ExtraData, valueConvertor: AmountToCurrencyConvertor): ActivityEntry =
proc newTransactionActivityEntry*(metadata: backend.ActivityEntry, fromAddresses: seq[string], extradata: ExtraData, currencyService: Service): ActivityEntry =
new(result, delete)
result.valueConvertor = valueConvertor
result.metadata = metadata
result.extradata = extradata

result.amountCurrency = valueConvertor(
if result.isInTransactionType(): metadata.amountIn else: metadata.amountOut,
if result.isInTransactionType(): metadata.symbolIn.get("") else: metadata.symbolOut.get(""),
)
result.noAmount = newCurrencyAmount()

result.amountCurrency = result.extractCurrencyAmount(currencyService)

result.highlight = metadata.isNew

result.setup()

proc buildMultiTransactionExtraData(metadata: backend.ActivityEntry, currencyService: Service): ExtraData =
if metadata.symbolIn.isSome():
result.inAmount = currencyService.parseCurrencyValue(metadata.symbolIn.get(), metadata.amountIn)
if metadata.symbolOut.isSome():
result.outAmount = currencyService.parseCurrencyValue(metadata.symbolOut.get(), metadata.amountOut)

proc buildTransactionExtraData(metadata: backend.ActivityEntry, currencyService: Service): ExtraData =
if metadata.symbolIn.isSome() or metadata.amountIn > 0:
result.inAmount = currencyService.parseCurrencyValue(metadata.symbolIn.get(""), metadata.amountIn)
if metadata.symbolOut.isSome() or metadata.amountOut > 0:
result.outAmount = currencyService.parseCurrencyValue(metadata.symbolOut.get(""), metadata.amountOut)

proc newActivityEntry*(backendEntry: backend.ActivityEntry, addresses: seq[string], currencyService: Service): ActivityEntry =
var ae: entry.ActivityEntry
case backendEntry.getPayloadType():
of MultiTransaction:
let extraData = buildMultiTransactionExtraData(backendEntry, currencyService)
ae = newMultiTransactionActivityEntry(backendEntry, extraData, currencyService)
of SimpleTransaction, PendingTransaction:
let extraData = buildTransactionExtraData(backendEntry, currencyService)
ae = newTransactionActivityEntry(backendEntry, addresses, extraData, currencyService)
return ae

proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} =
return self.metadata.getPayloadType() == backend.PayloadType.MultiTransaction

Expand Down Expand Up @@ -278,6 +309,20 @@ QtObject:
if self.metadata.communityId.isSome():
return self.metadata.communityId.unsafeGet()
return ""

QtProperty[string] communityId:
read = getCommunityId
read = getCommunityId

proc highlightChanged*(self: ActivityEntry) {.signal.}

proc getHighlight*(self: ActivityEntry): bool {.slot.} =
return self.highlight

proc doneHighlighting*(self: ActivityEntry) {.slot.} =
if self.highlight:
self.highlight = false
self.highlightChanged()

QtProperty[bool] highlight:
read = getHighlight
notify = highlightChanged
Loading

0 comments on commit 3daed67

Please sign in to comment.