Reduce paginated client page size to 50

By coder.
This commit is contained in:
Chris Troutner
2026-09-04 10:00:19 -07:00
parent 81cedabcdf
commit 59cce4426d
23 changed files with 593 additions and 48 deletions
+273 -15
View File
@@ -42,6 +42,7 @@ const TopicDiscoveryPage = require('../../src/services/topic-discovery-page')
const TopicFeedPage = require('../../src/services/topic-feed-page')
const SearchPage = require('../../src/services/search-page')
const NotificationsPage = require('../../src/services/notifications-page')
const RecentProfilesPage = require('../../src/services/recent-profiles-page')
const MemoTopicFollow = require('../../src/services/memo-topic-follow')
const MemoTopicPost = require('../../src/services/memo-topic-post')
const TopicPostPage = require('../../src/services/topic-post-page')
@@ -296,7 +297,7 @@ function makeMemoDb () {
}
return addrs
},
async search (q) {
async search (q, { limit = 50, offset = 0 } = {}) {
const normalized = String(q).trim().toLowerCase()
if (normalized.length === 0) {
return { posts: [], profiles: [], pagination: { total: 0, hasMore: false } }
@@ -308,18 +309,19 @@ function makeMemoDb () {
(typeof p.name === 'string' && p.name.toLowerCase().includes(normalized)) ||
(typeof p.text === 'string' && p.text.toLowerCase().includes(normalized))
)
const total = matchedPosts.length + matchedProfiles.length
const total = matchedPosts.length
const page = matchedPosts.slice(offset, offset + limit)
return {
posts: matchedPosts,
posts: page,
profiles: matchedProfiles,
pagination: { total, hasMore: false }
pagination: { total, limit, offset, hasMore: offset + page.length < total }
}
},
async getRecentPosts ({ limit = 100, offset = 0 } = {}) {
async getRecentPosts ({ limit = 50, offset = 0 } = {}) {
const page = posts.slice(offset, offset + limit)
return { posts: page, pagination: { total: posts.length, limit, offset, hasMore: offset + page.length < posts.length } }
},
async getPostsByAddr (addr, { limit = 100, offset = 0 } = {}) {
async getPostsByAddr (addr, { limit = 50, offset = 0 } = {}) {
const filtered = posts.filter((p) => p.addr === addr)
const page = filtered.slice(offset, offset + limit)
return { posts: page, pagination: { total: filtered.length, limit, offset, hasMore: offset + page.length < filtered.length } }
@@ -341,12 +343,12 @@ function makeMemoDb () {
list.sort((a, b) => a.room.localeCompare(b.room))
return { topics: list }
},
async getTopicPosts (room, { limit = 100, offset = 0 } = {}) {
async getTopicPosts (room, { limit = 50, offset = 0 } = {}) {
const all = topicPosts[room] || []
const page = all.slice(offset, offset + limit)
return { posts: page, pagination: { total: all.length, limit, offset, hasMore: offset + page.length < all.length } }
},
async getNotifications (addr, { limit = 100, offset = 0 } = {}) {
async getNotifications (addr, { limit = 50, offset = 0 } = {}) {
const notifications = []
for (const reply of replies) {
@@ -392,7 +394,11 @@ function makeMemoDb () {
const page = notifications.slice(offset, offset + limit)
return { notifications: page, pagination: { total, limit, offset, hasMore: offset + page.length < total } }
},
async getFollowingFeed (addr, { limit = 100, offset = 0 } = {}) {
async getRecentProfiles ({ limit = 50, offset = 0 } = {}) {
const page = profiles.slice(offset, offset + limit)
return { profiles: page, pagination: { total: profiles.length, limit, offset, hasMore: offset + page.length < profiles.length } }
},
async getFollowingFeed (addr, { limit = 50, offset = 0 } = {}) {
const followees = new Set()
for (const [key, following] of Object.entries(followState)) {
if (!following) continue
@@ -410,7 +416,7 @@ function makeMemoDb () {
// Fresh world/state object for a single scenario execution.
function createWorld () {
const wallet = makeWallet('')
const wallet = makeWallet('bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d')
const feed = makeFeed()
const profiles = makeProfiles()
const memoPost = new MemoPost({ wallet, feed })
@@ -456,6 +462,7 @@ function createWorld () {
memoDb,
navigate: (path) => { world.currentPath = path }
})
world.recentProfilesPage = new RecentProfilesPage({ memoDb })
// The New Post Page controller wraps the memo post behavior. Its navigate
// adapter updates the world's current path so navigation can be asserted.
@@ -2632,13 +2639,264 @@ const handlers = [
}
},
{
name: 'API serves post with address and text',
pattern: /^the psf-memo-db API serves a post with txid (.+) authored by the address (.+) with text (.+)$/,
name: 'API serves N recent posts',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) recent posts$/,
run (m, example, world) {
const txid = resolveParam(m[1], example)
const count = parseInt(resolveParam(m[1], example), 10)
for (let i = 0; i < count; i++) {
world.memoDb.addPost({
txid: `recent-post-${i + 1}`.padEnd(64, '0'),
addr: `addr-${i + 1}`,
text: `Recent post ${i + 1}`,
blockHeight: 100 + i
})
}
}
},
{
name: 'API serves N posts by address',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) posts authored by the address (.+)$/,
run (m, example, world) {
const count = parseInt(resolveParam(m[1], example), 10)
const addr = resolveParam(m[2], example)
const text = resolveText(m[3], example)
world.memoDb.addPost({ txid, addr, text, blockHeight: 100 })
for (let i = 0; i < count; i++) {
world.memoDb.addPost({
txid: `${addr}-post-${i + 1}`.padEnd(64, '0'),
addr,
text: `Post ${i + 1}`,
blockHeight: 100 + i
})
}
}
},
{
name: 'API serves N posts in topic',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) posts in the topic (.+)$/,
run (m, example, world) {
const count = parseInt(resolveParam(m[1], example), 10)
const room = resolveParam(m[2], example)
for (let i = 0; i < count; i++) {
world.memoDb.addTopicPost(room, {
txid: `${room}-post-${i + 1}`.padEnd(64, '0'),
addr: `addr-${i + 1}`,
text: `Topic post ${i + 1}`,
blockHeight: 100 + i
})
}
}
},
{
name: 'API serves N replies to post',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) replies to the post with txid (.+)$/,
run (m, example, world) {
const count = parseInt(resolveParam(m[1], example), 10)
const parentTxid = resolveParam(m[2], example)
for (let i = 0; i < count; i++) {
world.memoDb.addReply({
txid: `reply-${i + 1}`.padEnd(64, '0'),
parentTxid,
text: `Reply ${i + 1}`,
addr: 'bitcoincash:reply-author',
blockHeight: 100 + i
})
}
}
},
{
name: 'API serves N search posts',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) search posts matching (.+)$/,
run (m, example, world) {
const count = parseInt(resolveParam(m[1], example), 10)
const query = resolveParam(m[2], example)
for (let i = 0; i < count; i++) {
world.memoDb.addSearchPost({
txid: `search-post-${i + 1}`.padEnd(64, '0'),
addr: `addr-${i + 1}`,
text: `${query} search result ${i + 1}`,
blockHeight: 100 + i
})
}
}
},
{
name: 'API serves N profiles',
pattern: /^the psf-memo-db API serves (<[A-Za-z0-9_]+>) profiles$/,
run (m, example, world) {
const count = parseInt(resolveParam(m[1], example), 10)
for (let i = 0; i < count; i++) {
world.memoDb.profiles.push({
addr: `profile-addr-${i + 1}`,
text: `Profile ${i + 1}`,
txid: `profile-txid-${i + 1}`.padEnd(64, '0'),
blockHeight: 100 + i,
seen: Date.now()
})
}
}
},
{
name: 'recent feed shows 50 posts',
pattern: /^the recent feed shows 50 posts$/,
run (m, example, world) {
const actual = world.recentFeedPage.posts.length
if (actual !== 50) {
throw new Error(`Expected 50 posts in recent feed, got ${actual}.`)
}
}
},
{
name: 'recent feed can load more posts',
pattern: /^the recent feed can load more posts$/,
run (m, example, world) {
if (!world.recentFeedPage.canLoadMore || !world.recentFeedPage.canLoadMore()) {
throw new Error('Expected recent feed to have more posts, but pagination says there are none.')
}
}
},
{
name: 'following feed shows 50 posts',
pattern: /^the following feed shows 50 posts$/,
run (m, example, world) {
const actual = world.followingFeedPage.posts.length
if (actual !== 50) {
throw new Error(`Expected 50 posts in following feed, got ${actual}.`)
}
}
},
{
name: 'following feed can load more posts',
pattern: /^the following feed can load more posts$/,
run (m, example, world) {
if (!world.followingFeedPage.canLoadMore()) {
throw new Error('Expected following feed to have more posts, but pagination says there are none.')
}
}
},
{
name: 'topic feed shows 50 posts',
pattern: /^the topic feed shows 50 posts$/,
run (m, example, world) {
const actual = world.topicFeedPage.posts.length
if (actual !== 50) {
throw new Error(`Expected 50 posts in topic feed, got ${actual}.`)
}
}
},
{
name: 'topic feed can load more posts',
pattern: /^the topic feed can load more posts$/,
run (m, example, world) {
if (!world.topicFeedPage.canLoadMore()) {
throw new Error('Expected topic feed to have more posts, but pagination says there are none.')
}
}
},
{
name: 'notifications show 50 notifications',
pattern: /^the notifications show 50 notifications$/,
run (m, example, world) {
const actual = world.notificationsPage.notifications.length
if (actual !== 50) {
throw new Error(`Expected 50 notifications, got ${actual}.`)
}
}
},
{
name: 'search results show 50 posts',
pattern: /^the search results show 50 posts$/,
run (m, example, world) {
const actual = world.searchPage.posts.length
if (actual !== 50) {
throw new Error(`Expected 50 posts in search results, got ${actual}.`)
}
}
},
{
name: 'search results can load more posts',
pattern: /^the search results can load more posts$/,
run (m, example, world) {
if (!world.searchPage.canLoadMore || !world.searchPage.canLoadMore()) {
throw new Error('Expected search results to have more pages, but pagination says there are none.')
}
}
},
{
name: 'profile page shows 50 posts',
pattern: /^the profile page shows 50 posts$/,
run (m, example, world) {
const actual = world.profilePage.posts.length
if (actual !== 50) {
throw new Error(`Expected 50 posts on profile page, got ${actual}.`)
}
}
},
{
name: 'profile page can load more posts',
pattern: /^the profile page can load more posts$/,
run (m, example, world) {
if (!world.profilePage.canLoadMore || !world.profilePage.canLoadMore()) {
throw new Error('Expected profile page to have more posts, but pagination says there are none.')
}
}
},
{
name: 'recent profiles page shows 50 profiles',
pattern: /^the recent profiles page shows 50 profiles$/,
run (m, example, world) {
const actual = world.recentProfilesPage.profiles.length
if (actual !== 50) {
throw new Error(`Expected 50 profiles on recent profiles page, got ${actual}.`)
}
}
},
{
name: 'recent profiles page can load more profiles',
pattern: /^the recent profiles page can load more profiles$/,
run (m, example, world) {
if (!world.recentProfilesPage.canLoadMore()) {
throw new Error('Expected recent profiles page to have more profiles, but pagination says there are none.')
}
}
},
{
name: 'open recent profiles page',
pattern: /^I open the recent profiles page$/,
async run (m, example, world) {
await world.recentProfilesPage.load()
world.currentPath = RecentProfilesPage.RECENT_PROFILES_PATH
}
},
{
name: 'open profile page for address',
pattern: /^I open the profile page for the address (.+)$/,
async run (m, example, world) {
const addr = resolveParam(m[1], example)
const myAddr = world.wallet.walletInfo.cashAddress
world.profilePage = new ProfilePage({
memoDb: world.memoDb,
addr,
myAddr,
memoFollow: world.memoFollow,
memoMute: world.memoMute
})
await world.profilePage.load()
world.currentPath = `${ProfilePage.PROFILE_PATH_PREFIX}/${encodeURIComponent(addr)}`
}
},
{
name: 'open topic feed for topic',
pattern: /^I open the topic feed for (.+)$/,
async run (m, example, world) {
const room = resolveParam(m[1], example)
const myAddr = world.wallet.walletInfo.cashAddress
world.topicFeedPage = new TopicFeedPage({
memoDb: world.memoDb,
room,
myAddr,
memoTopicFollow: world.memoTopicFollow
})
await world.topicFeedPage.load()
world.currentPath = TopicFeedPage.topicFeedPath(room)
}
},
{
@@ -18,7 +18,7 @@ import {
import '../../../App.css'
import '../../post-feed/post-feed.css'
const PAGE_SIZE = 100
const PAGE_SIZE = 50
function FollowingFeed (props) {
const { appData } = props
@@ -12,7 +12,7 @@ import MemoDb from '../../../services/memo-db'
import NotificationsPage from '../../../services/notifications-page'
import '../../../App.css'
const PAGE_SIZE = 100
const PAGE_SIZE = 50
function notificationText (n) {
if (n.type === 'reply') {
@@ -17,7 +17,7 @@ import {
import '../../../App.css'
import '../../post-feed/post-feed.css'
const PAGE_SIZE = 100
const PAGE_SIZE = 50
function RecentPosts (props) {
const { appData } = props
@@ -18,6 +18,8 @@ import PostThreadModal from '../../post-thread-modal'
import '../../../App.css'
import './profile.css'
const PAGE_SIZE = 50
function formatSeen (seen) {
if (!seen) return ''
const ms = seen > 1e12 ? seen : seen * 1000
@@ -64,6 +66,7 @@ function Profile (props) {
const [showThreadModal, setShowThreadModal] = useState(false)
const [profiles, setProfiles] = useState({})
const [profilePage, setProfilePage] = useState(null)
const [offset, setOffset] = useState(0)
const [busy, setBusy] = useState(false)
const openThread = (txid) => {
@@ -114,7 +117,7 @@ function Profile (props) {
const [profile, profilePic, pageData] = await Promise.all([
memoDb.getProfile(addr),
memoDb.getProfilePic(addr),
page.load()
page.load({ limit: PAGE_SIZE, offset })
])
setProfileText(profile?.text || '')
@@ -136,13 +139,24 @@ function Profile (props) {
setError('Missing profile address')
setLoading(false)
}
}, [addr, myAddr, wallet, appProfiles])
}, [addr, myAddr, wallet, appProfiles, offset])
const showFollowButton = profilePage && profilePage.canFollow() && !profilePage.isFollowing()
const showUnfollowButton = profilePage && profilePage.canFollow() && profilePage.isFollowing()
const showMuteButton = profilePage && profilePage.canMute() && !profilePage.isMuting()
const showUnmuteButton = profilePage && profilePage.canMute() && profilePage.isMuting()
const canGoBack = offset > 0
const canGoNext = pagination?.hasMore ?? false
const handlePrevious = () => {
setOffset((prev) => Math.max(0, prev - PAGE_SIZE))
}
const handleNext = () => {
setOffset((prev) => prev + PAGE_SIZE)
}
return (
<Container fluid className='profile-page mt-4'>
{error && <p className='text-danger'>{error}</p>}
@@ -222,7 +236,7 @@ function Profile (props) {
<h2 className='profile-posts-title'>Posts</h2>
{pagination && (
<span className='text-muted'>
{pagination.total} post{pagination.total === 1 ? '' : 's'}
{pagination.offset + 1}{pagination.offset + posts.length} of {pagination.total} posts
</span>
)}
</div>
@@ -249,6 +263,26 @@ function Profile (props) {
</Card.Body>
</Card>
))}
{!loading && !error && (pagination || offset > 0) && (
<div className='profile-posts-pagination mt-3'>
<Button
variant='outline-dark'
onClick={handlePrevious}
disabled={!canGoBack}
>
Previous
</Button>
<Button
variant='outline-dark'
onClick={handleNext}
disabled={!canGoNext}
>
Next
</Button>
</div>
)}
</Col>
</Row>
)}
@@ -4,14 +4,16 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Container, Row, Col, Spinner, Table } from 'react-bootstrap'
import { Container, Row, Col, Spinner, Table, Button } from 'react-bootstrap'
// Local libraries
import MemoDb from '../../../services/memo-db'
import RecentProfilesPage from '../../../services/recent-profiles-page'
import AppUtil, { truncateAddr, truncateTxid } from '../../../util'
import '../../../App.css'
const appUtil = new AppUtil()
const PAGE_SIZE = 50
function formatSeen (seen) {
if (!seen) return ''
@@ -24,22 +26,39 @@ function RecentProfiles () {
const [error, setError] = useState(null)
const [profiles, setProfiles] = useState([])
const [pagination, setPagination] = useState(null)
const [offset, setOffset] = useState(0)
useEffect(() => {
const loadProfiles = async () => {
try {
setLoading(true)
setError(null)
const memoDb = new MemoDb()
const data = await memoDb.getRecentProfiles({ limit: 100, offset: 0 })
const page = new RecentProfilesPage({ memoDb })
const data = await page.load({ limit: PAGE_SIZE, offset })
setProfiles(data.profiles || [])
setPagination(data.pagination || null)
} catch (err) {
setError(err.message || 'Failed to load recent profiles')
setProfiles([])
setPagination(null)
}
setLoading(false)
}
loadProfiles()
}, [])
}, [offset])
const canGoBack = offset > 0
const canGoNext = pagination?.hasMore ?? false
const handlePrevious = () => {
setOffset((prev) => Math.max(0, prev - PAGE_SIZE))
}
const handleNext = () => {
setOffset((prev) => prev + PAGE_SIZE)
}
return (
<Container>
@@ -102,6 +121,26 @@ function RecentProfiles () {
</tbody>
</Table>
)}
{!loading && !error && (pagination || offset > 0) && (
<div className='recent-profiles-pagination'>
<Button
variant='outline-dark'
onClick={handlePrevious}
disabled={!canGoBack}
>
Previous
</Button>
<Button
variant='outline-dark'
onClick={handleNext}
disabled={!canGoNext}
>
Next
</Button>
</div>
)}
</Col>
</Row>
</Container>
@@ -12,6 +12,8 @@ import MemoDb from '../../../services/memo-db'
import SearchPage from '../../../services/search-page'
import '../../../App.css'
const PAGE_SIZE = 50
function SearchResults (props) {
const { posts, profiles, searched } = props
@@ -64,6 +66,8 @@ function Search (props) {
const [error, setError] = useState(null)
const [posts, setPosts] = useState([])
const [profiles, setProfiles] = useState([])
const [pagination, setPagination] = useState(null)
const [offset, setOffset] = useState(0)
const [searched, setSearched] = useState(false)
const handleSubmit = async (event) => {
@@ -71,23 +75,57 @@ function Search (props) {
setLoading(true)
setError(null)
setSearched(true)
setOffset(0)
try {
const memoDb = new MemoDb()
const page = new SearchPage({ memoDb })
page.setQuery(query)
const result = await page.submit()
const result = await page.submit({ limit: PAGE_SIZE, offset: 0 })
setPosts(result.posts || [])
setProfiles(result.profiles || [])
setPagination(result.pagination || null)
} catch (err) {
setError(err.message || 'Search failed')
setPosts([])
setProfiles([])
setPagination(null)
}
setLoading(false)
}
const canGoBack = offset > 0
const canGoNext = pagination?.hasMore ?? false
const loadPage = async (nextOffset) => {
setLoading(true)
setError(null)
try {
const memoDb = new MemoDb()
const page = new SearchPage({ memoDb })
page.setQuery(query)
const result = await page.submit({ limit: PAGE_SIZE, offset: nextOffset })
setPosts(result.posts || [])
setProfiles(result.profiles || [])
setPagination(result.pagination || null)
setOffset(nextOffset)
} catch (err) {
setError(err.message || 'Search failed')
}
setLoading(false)
}
const handlePreviousPage = () => {
loadPage(Math.max(0, offset - PAGE_SIZE))
}
const handleNextPage = () => {
loadPage(offset + PAGE_SIZE)
}
return (
<Container className='search-page'>
<Row className='justify-content-center'>
@@ -123,6 +161,26 @@ function Search (props) {
)}
{!loading && <SearchResults posts={posts} profiles={profiles} searched={searched} />}
{!loading && searched && (pagination || offset > 0) && (
<div className='search-pagination mt-3'>
<Button
variant='outline-dark'
onClick={handlePreviousPage}
disabled={!canGoBack}
>
Previous
</Button>
<Button
variant='outline-dark'
onClick={handleNextPage}
disabled={!canGoNext}
>
Next
</Button>
</div>
)}
</Col>
</Row>
</Container>
@@ -24,7 +24,7 @@ import { byteLength } from '../../../services/utf8'
import '../../../App.css'
import '../../post-feed/post-feed.css'
const PAGE_SIZE = 100
const PAGE_SIZE = 50
function TopicFeed (props) {
const { appData } = props
@@ -22,7 +22,7 @@ class FollowingFeedPage {
return this.wallet?.walletInfo?.cashAddress || null
}
async load ({ limit = 100, offset = 0 } = {}) {
async load ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Following feed page requires a memo db client.')
}
+4 -4
View File
@@ -10,11 +10,11 @@ class MemoDb {
this.axios = axios
}
async getRecentProfiles ({ limit = 100, offset = 0 } = {}) {
async getRecentProfiles ({ limit = 50, offset = 0 } = {}) {
return this.getRecent('/profile/recent', 'getRecentProfiles', { limit, offset })
}
async getRecentPosts ({ limit = 100, offset = 0 } = {}) {
async getRecentPosts ({ limit = 50, offset = 0 } = {}) {
return this.getRecent('/posts/recent', 'getRecentPosts', { limit, offset })
}
@@ -58,7 +58,7 @@ class MemoDb {
return this._getList(`/topics/${encodeURIComponent(room)}/followers`, 'getTopicFollowers', 'followers')
}
async search (q, { limit = 100, offset = 0 } = {}) {
async search (q, { limit = 50, offset = 0 } = {}) {
try {
const result = await this.axios.get(`${config.backend}/search`, {
params: { q, limit, offset }
@@ -108,7 +108,7 @@ class MemoDb {
}
// GET a paginated resource page at a full path.
async getPage (path, name, { limit = 100, offset = 0 } = {}) {
async getPage (path, name, { limit = 50, offset = 0 } = {}) {
try {
const result = await this.axios.get(`${config.backend}${path}`, {
params: { limit, offset }
@@ -22,7 +22,7 @@ class NotificationsPage {
return this.wallet?.walletInfo?.cashAddress || null
}
async load ({ limit = 100, offset = 0 } = {}) {
async load ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Notifications page requires a memo db client.')
}
+5 -1
View File
@@ -26,7 +26,7 @@ class ProfilePage {
this.muteState = null
}
async load ({ limit = 100, offset = 0 } = {}) {
async load ({ limit = 50, offset = 0 } = {}) {
this._assertReady()
const data = await this.memoDb.getPostsByAddr(this.addr, { limit, offset })
@@ -123,6 +123,10 @@ class ProfilePage {
getPost (txid) {
return this.posts.find((post) => post.txid === txid) || null
}
canLoadMore () {
return this.pagination?.hasMore ?? false
}
}
ProfilePage.PROFILE_PATH_PREFIX = PROFILE_PATH_PREFIX
@@ -19,7 +19,7 @@ class RecentFeedPage {
this.pagination = null
}
async load ({ limit = 100, offset = 0 } = {}) {
async load ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Recent feed page requires a memo db client.')
}
@@ -34,6 +34,10 @@ class RecentFeedPage {
getPost (txid) {
return this.posts.find((post) => post.txid === txid) || null
}
canLoadMore () {
return this.pagination?.hasMore ?? false
}
}
RecentFeedPage.RECENT_FEED_PATH = RECENT_FEED_PATH
@@ -0,0 +1,41 @@
/*
Recent Profiles Page behavior: load and display the most recent Memo profiles.
This is the testable controller behind the React "Recent Profiles" page. It
wraps the MemoDb client and exposes the loaded profiles and pagination so the
view can render them.
*/
const RECENT_PROFILES_PATH = '/profile/recent'
class RecentProfilesPage {
constructor (deps = {}) {
this.memoDb = deps.memoDb || null
this.profiles = []
this.pagination = null
}
async load ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Recent profiles page requires a memo db client.')
}
const data = await this.memoDb.getRecentProfiles({ limit, offset })
this.profiles = data.profiles || []
this.pagination = data.pagination || null
return { profiles: this.profiles, pagination: this.pagination }
}
canLoadMore () {
return this.pagination?.hasMore ?? false
}
getProfile (addr) {
return this.profiles.find((profile) => profile.addr === addr) || null
}
}
RecentProfilesPage.RECENT_PROFILES_PATH = RECENT_PROFILES_PATH
module.exports = RecentProfilesPage
+5 -1
View File
@@ -23,7 +23,7 @@ class SearchPage {
return this
}
async submit ({ limit = 100, offset = 0 } = {}) {
async submit ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Search page requires a memo db client.')
}
@@ -47,6 +47,10 @@ class SearchPage {
getProfile (addr) {
return this.profiles.find((profile) => profile.addr === addr) || null
}
canLoadMore () {
return this.pagination?.hasMore ?? false
}
}
SearchPage.SEARCH_PATH = SEARCH_PATH
@@ -18,7 +18,7 @@ class TopicFeedPage {
this.followers = []
}
async load ({ limit = 100, offset = 0 } = {}) {
async load ({ limit = 50, offset = 0 } = {}) {
if (!this.memoDb) {
throw new Error('Topic feed page requires a memo db client.')
}
@@ -81,6 +81,10 @@ class TopicFeedPage {
getPost (txid) {
return this.posts.find((post) => post.txid === txid) || null
}
canLoadMore () {
return this.pagination?.hasMore ?? false
}
}
TopicFeedPage.topicFeedPath = function (room) {
@@ -91,7 +91,7 @@ test('load forwards limit and offset to the memo db client', async () => {
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 10, offset: 20 } }])
})
test('load defaults limit to 100 and offset to 0', async () => {
test('load defaults limit to 50 and offset to 0', async () => {
const calls = []
const memoDb = {
async getFollowingFeed (addr, params) {
@@ -103,7 +103,7 @@ test('load defaults limit to 100 and offset to 0', async () => {
await page.load()
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 100, offset: 0 } }])
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 50, offset: 0 } }])
})
test('load throws when no memo db client is provided', async () => {
@@ -79,7 +79,7 @@ test('load forwards limit and offset to the memo db client', async () => {
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 10, offset: 20 } }])
})
test('load defaults limit to 100 and offset to 0', async () => {
test('load defaults limit to 50 and offset to 0', async () => {
const calls = []
const memoDb = {
async getNotifications (addr, params) {
@@ -91,7 +91,7 @@ test('load defaults limit to 100 and offset to 0', async () => {
await page.load()
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 100, offset: 0 } }])
assert.deepEqual(calls, [{ addr: MY_ADDRESS, params: { limit: 50, offset: 0 } }])
})
test('load throws when no memo db client is provided', async () => {
@@ -75,7 +75,7 @@ test('load throws when no address is provided', async () => {
)
})
test('load defaults limit to 100 and offset to 0', async () => {
test('load defaults limit to 50 and offset to 0', async () => {
const calls = []
const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
const memoDb = {
@@ -91,7 +91,7 @@ test('load defaults limit to 100 and offset to 0', async () => {
await page.load()
assert.deepEqual(calls, [{ a: addr, params: { limit: 100, offset: 0 } }])
assert.deepEqual(calls, [{ a: addr, params: { limit: 50, offset: 0 } }])
})
test('load sets pagination to null when the API returns none', async () => {
@@ -66,7 +66,7 @@ test('load throws when no memo db client is provided', async () => {
)
})
test('load defaults limit to 100 and offset to 0', async () => {
test('load defaults limit to 50 and offset to 0', async () => {
const calls = []
const memoDb = {
async getRecentPosts (params) {
@@ -78,5 +78,5 @@ test('load defaults limit to 100 and offset to 0', async () => {
await page.load()
assert.deepEqual(calls, [{ limit: 100, offset: 0 }])
assert.deepEqual(calls, [{ limit: 50, offset: 0 }])
})
@@ -0,0 +1,99 @@
/*
Unit tests for the recent profiles page controller.
The recent profiles page is a thin, testable wrapper around the MemoDb client.
It loads the paginated list of recent profiles and exposes each profile.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const RecentProfilesPage = require('../../src/services/recent-profiles-page')
function makeMemoDb (profiles, pagination) {
return {
async getRecentProfiles ({ limit, offset }) {
return { profiles, pagination }
}
}
}
test('load returns profiles and pagination', async () => {
const profiles = [
{ addr: 'bitcoincash:a', text: 'Alice' },
{ addr: 'bitcoincash:b', text: 'Bob' }
]
const page = new RecentProfilesPage({ memoDb: makeMemoDb(profiles, { total: 2 }) })
const result = await page.load()
assert.deepEqual(result.profiles, profiles)
assert.equal(result.pagination.total, 2)
})
test('load throws when no memo db client is provided', async () => {
const page = new RecentProfilesPage({})
await assert.rejects(
() => page.load(),
/requires a memo db client/
)
})
test('load forwards limit and offset to the memo db client', async () => {
const calls = []
const memoDb = {
async getRecentProfiles (params) {
calls.push(params)
return { profiles: [], pagination: {} }
}
}
const page = new RecentProfilesPage({ memoDb })
await page.load({ limit: 10, offset: 20 })
assert.deepEqual(calls, [{ limit: 10, offset: 20 }])
})
test('load defaults limit to 50 and offset to 0', async () => {
const calls = []
const memoDb = {
async getRecentProfiles (params) {
calls.push(params)
return { profiles: [], pagination: {} }
}
}
const page = new RecentProfilesPage({ memoDb })
await page.load()
assert.deepEqual(calls, [{ limit: 50, offset: 0 }])
})
test('canLoadMore reflects pagination.hasMore', async () => {
const pageMore = new RecentProfilesPage({
memoDb: makeMemoDb([], { hasMore: true })
})
await pageMore.load()
assert.equal(pageMore.canLoadMore(), true)
const pageDone = new RecentProfilesPage({
memoDb: makeMemoDb([], { hasMore: false })
})
await pageDone.load()
assert.equal(pageDone.canLoadMore(), false)
})
test('getProfile returns a loaded profile by address', async () => {
const profiles = [{ addr: 'bitcoincash:a', text: 'Alice' }]
const page = new RecentProfilesPage({ memoDb: makeMemoDb(profiles, {}) })
await page.load()
assert.equal(page.getProfile('bitcoincash:a').text, 'Alice')
})
test('exposes the recent profiles path', () => {
assert.equal(RecentProfilesPage.RECENT_PROFILES_PATH, '/profile/recent')
})
@@ -49,7 +49,7 @@ test('submit forwards query, limit and offset to the memo db client', async () =
assert.deepEqual(calls, [{ q: 'alice', params: { limit: 10, offset: 20 } }])
})
test('submit defaults limit to 100 and offset to 0', async () => {
test('submit defaults limit to 50 and offset to 0', async () => {
const calls = []
const memoDb = {
async search (q, params) {
@@ -62,7 +62,7 @@ test('submit defaults limit to 100 and offset to 0', async () => {
page.setQuery('memo')
await page.submit()
assert.deepEqual(calls, [{ q: 'memo', params: { limit: 100, offset: 0 } }])
assert.deepEqual(calls, [{ q: 'memo', params: { limit: 50, offset: 0 } }])
})
test('submit throws when no memo db client is provided', async () => {
@@ -71,7 +71,7 @@ test('load forwards limit and offset to the memo db client', async () => {
assert.deepEqual(calls, [{ room: 'bitcoin', params: { limit: 10, offset: 20 } }])
})
test('load defaults limit and offset', async () => {
test('load defaults limit and offset to 50 and 0', async () => {
const calls = []
const memoDb = {
async getTopicPosts (room, params) {
@@ -89,7 +89,7 @@ test('load defaults limit and offset', async () => {
await page.load()
assert.deepEqual(calls, [{ limit: 100, offset: 0 }])
assert.deepEqual(calls, [{ limit: 50, offset: 0 }])
})
test('stores the pagination returned by the memo db client', async () => {