diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index 820c08f1..d2f29a07 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -351,20 +351,58 @@ const resolvers = {
facts: history
}
},
- walletLogs: async (parent, args, { me, models }) => {
+ walletLogs: async (parent, { type, from, to, cursor }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
- return await models.walletLog.findMany({
- where: {
- userId: me.id
- },
- orderBy: [
- { createdAt: 'desc' },
- { id: 'desc' }
- ]
- })
+ // we cursoring with the wallet logs on the client
+ // if we have from, don't use cursor
+ // regardless, store the state of the cursor for the next call
+
+ const decodedCursor = cursor ? decodeCursor(cursor) : { offset: 0, time: to ?? new Date() }
+
+ let logs = []
+ let nextCursor
+ if (from) {
+ logs = await models.walletLog.findMany({
+ where: {
+ userId: me.id,
+ wallet: type ?? undefined,
+ createdAt: {
+ gte: from ? new Date(Number(from)) : undefined,
+ lte: to ? new Date(Number(to)) : undefined
+ }
+ },
+ orderBy: [
+ { createdAt: 'desc' },
+ { id: 'desc' }
+ ]
+ })
+ nextCursor = nextCursorEncoded(decodedCursor, logs.length)
+ } else {
+ logs = await models.walletLog.findMany({
+ where: {
+ userId: me.id,
+ wallet: type ?? undefined,
+ createdAt: {
+ lte: decodedCursor.time
+ }
+ },
+ orderBy: [
+ { createdAt: 'desc' },
+ { id: 'desc' }
+ ],
+ take: LIMIT,
+ skip: decodedCursor.offset
+ })
+ nextCursor = logs.length === LIMIT ? nextCursorEncoded(decodedCursor, logs.length) : null
+ }
+
+ return {
+ cursor: nextCursor,
+ entries: logs
+ }
}
},
Wallet: {
diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js
index 5a683813..d87af0e0 100644
--- a/api/typeDefs/wallet.js
+++ b/api/typeDefs/wallet.js
@@ -66,7 +66,7 @@ const typeDefs = `
wallets: [Wallet!]!
wallet(id: ID!): Wallet
walletByType(type: String!): Wallet
- walletLogs: [WalletLog]!
+ walletLogs(type: String, from: String, to: String, cursor: String): WalletLog!
}
extend type Mutation {
@@ -154,6 +154,11 @@ const typeDefs = `
}
type WalletLog {
+ entries: [WalletLogEntry!]!
+ cursor: String
+ }
+
+ type WalletLogEntry {
id: ID!
createdAt: Date!
wallet: ID!
diff --git a/components/use-indexeddb.js b/components/use-indexeddb.js
new file mode 100644
index 00000000..1733970e
--- /dev/null
+++ b/components/use-indexeddb.js
@@ -0,0 +1,273 @@
+import { useState, useEffect, useCallback, useRef } from 'react'
+
+function useIndexedDB (dbName, storeName, version = 1, indices = []) {
+ const [db, setDb] = useState(null)
+ const [error, setError] = useState(null)
+ const operationQueue = useRef([])
+
+ const handleError = useCallback((error) => {
+ console.error('IndexedDB error:', error)
+ setError(error)
+ }, [])
+
+ const processQueue = useCallback((db) => {
+ if (!db) return
+
+ try {
+ // try to run a noop to see if the db is ready
+ db.transaction(storeName)
+ while (operationQueue.current.length > 0) {
+ const operation = operationQueue.current.shift()
+ operation(db)
+ }
+ } catch (error) {
+ handleError(error)
+ }
+ }, [storeName, handleError])
+
+ useEffect(() => {
+ let isMounted = true
+ const request = window.indexedDB.open(dbName, version)
+
+ request.onerror = (event) => {
+ handleError(new Error('Error opening database'))
+ }
+
+ request.onsuccess = (event) => {
+ if (isMounted) {
+ const database = event.target.result
+ database.onversionchange = () => {
+ database.close()
+ setDb(null)
+ handleError(new Error('Database is outdated, please reload the page'))
+ }
+ setDb(database)
+ processQueue(database)
+ }
+ }
+
+ request.onupgradeneeded = (event) => {
+ const database = event.target.result
+ try {
+ const store = database.createObjectStore(storeName, { keyPath: 'id', autoIncrement: true })
+
+ indices.forEach(index => {
+ store.createIndex(index.name, index.keyPath, index.options)
+ })
+ } catch (error) {
+ handleError(new Error('Error upgrading database: ' + error.message))
+ }
+ }
+
+ return () => {
+ isMounted = false
+ if (db) {
+ db.close()
+ }
+ }
+ }, [dbName, storeName, version, indices, handleError, processQueue])
+
+ const queueOperation = useCallback((operation) => {
+ return new Promise((resolve, reject) => {
+ const wrappedOperation = (db) => {
+ try {
+ const result = operation(db)
+ resolve(result)
+ } catch (error) {
+ reject(error)
+ }
+ }
+
+ operationQueue.current.push(wrappedOperation)
+ processQueue(db)
+ })
+ }, [processQueue, db])
+
+ const add = useCallback((value) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readwrite')
+ const store = transaction.objectStore(storeName)
+ const request = store.add(value)
+
+ request.onerror = () => reject(new Error('Error adding data'))
+ request.onsuccess = () => resolve(request.result)
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const get = useCallback((key) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readonly')
+ const store = transaction.objectStore(storeName)
+ const request = store.get(key)
+
+ request.onerror = () => reject(new Error('Error getting data'))
+ request.onsuccess = () => resolve(request.result ? request.result : undefined)
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const getAll = useCallback(() => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readonly')
+ const store = transaction.objectStore(storeName)
+ const request = store.getAll()
+
+ request.onerror = () => reject(new Error('Error getting all data'))
+ request.onsuccess = () => resolve(request.result)
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const update = useCallback((key, value) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readwrite')
+ const store = transaction.objectStore(storeName)
+ const request = store.get(key)
+
+ request.onerror = () => reject(new Error('Error updating data'))
+ request.onsuccess = () => {
+ const updatedValue = { ...request.result, ...value }
+ const updateRequest = store.put(updatedValue)
+ updateRequest.onerror = () => reject(new Error('Error updating data'))
+ updateRequest.onsuccess = () => resolve(updateRequest.result)
+ }
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const remove = useCallback((key) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readwrite')
+ const store = transaction.objectStore(storeName)
+ const request = store.delete(key)
+
+ request.onerror = () => reject(new Error('Error removing data'))
+ request.onsuccess = () => resolve()
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const clear = useCallback((indexName = null, query = null) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readwrite')
+ const store = transaction.objectStore(storeName)
+
+ if (!query) {
+ // Clear all data if no query is provided
+ const request = store.clear()
+ request.onerror = () => reject(new Error('Error clearing all data'))
+ request.onsuccess = () => resolve()
+ } else {
+ // Clear data based on the query
+ const index = indexName ? store.index(indexName) : store
+ const request = index.openCursor(query)
+ let deletedCount = 0
+
+ request.onerror = () => reject(new Error('Error clearing data based on query'))
+ request.onsuccess = (event) => {
+ const cursor = event.target.result
+ if (cursor) {
+ const deleteRequest = cursor.delete()
+ deleteRequest.onerror = () => reject(new Error('Error deleting item'))
+ deleteRequest.onsuccess = () => {
+ deletedCount++
+ cursor.continue()
+ }
+ } else {
+ resolve(deletedCount)
+ }
+ }
+ }
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const getByIndex = useCallback((indexName, key) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readonly')
+ const store = transaction.objectStore(storeName)
+ const index = store.index(indexName)
+ const request = index.get(key)
+
+ request.onerror = () => reject(new Error('Error getting data by index'))
+ request.onsuccess = () => resolve(request.result)
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const getAllByIndex = useCallback((indexName, query, direction = 'next', limit = Infinity) => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readonly')
+ const store = transaction.objectStore(storeName)
+ const index = store.index(indexName)
+ const request = index.openCursor(query, direction)
+ const results = []
+
+ request.onerror = () => reject(new Error('Error getting data by index'))
+ request.onsuccess = (event) => {
+ const cursor = event.target.result
+ if (cursor && results.length < limit) {
+ results.push(cursor.value)
+ cursor.continue()
+ } else {
+ resolve(results)
+ }
+ }
+ })
+ })
+ }, [queueOperation, storeName])
+
+ const getPage = useCallback((page = 1, pageSize = 10, indexName = null, query = null, direction = 'next') => {
+ return queueOperation((db) => {
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(storeName, 'readonly')
+ const store = transaction.objectStore(storeName)
+ const target = indexName ? store.index(indexName) : store
+ const request = target.openCursor(query, direction)
+ const results = []
+ let skipped = 0
+ let hasMore = false
+
+ request.onerror = () => reject(new Error('Error getting page'))
+ request.onsuccess = (event) => {
+ const cursor = event.target.result
+ if (cursor) {
+ if (skipped < (page - 1) * pageSize) {
+ skipped++
+ cursor.continue()
+ } else if (results.length < pageSize) {
+ results.push(cursor.value)
+ cursor.continue()
+ } else {
+ hasMore = true
+ }
+ }
+ if (hasMore || !cursor) {
+ const countRequest = target.count()
+ countRequest.onsuccess = () => {
+ resolve({
+ data: results,
+ total: countRequest.result,
+ hasMore
+ })
+ }
+ countRequest.onerror = () => reject(new Error('Error counting items'))
+ }
+ }
+ })
+ })
+ }, [queueOperation, storeName])
+
+ return { add, get, getAll, update, remove, clear, getByIndex, getAllByIndex, getPage, error }
+}
+
+export default useIndexedDB
diff --git a/components/wallet-logger.js b/components/wallet-logger.js
index 9e623e4f..569445b0 100644
--- a/components/wallet-logger.js
+++ b/components/wallet-logger.js
@@ -1,18 +1,22 @@
import LogMessage from './log-message'
-import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
import styles from '@/styles/log.module.css'
import { Button } from 'react-bootstrap'
import { useToast } from './toast'
import { useShowModal } from './modal'
import { WALLET_LOGS } from '@/fragments/wallet'
import { getWalletByType } from 'wallets'
-import { gql, useMutation, useQuery } from '@apollo/client'
+import { gql, useLazyQuery, useMutation } from '@apollo/client'
import { useMe } from './me'
+import useIndexedDB from './use-indexeddb'
+import { SSR } from '@/lib/constants'
export function WalletLogs ({ wallet, embedded }) {
- const logs = useWalletLogs(wallet)
+ const { logs, setLogs, hasMore, loadMore, loadLogs, loading } = useWalletLogs(wallet)
+ useEffect(() => {
+ loadLogs()
+ }, [wallet])
- const tableRef = useRef()
const showModal = useShowModal()
return (
@@ -21,13 +25,12 @@ export function WalletLogs ({ wallet, embedded }) {
{
- showModal(onClose =>