mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Reduce paginated client page size to 50
By coder.
This commit is contained in:
@@ -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.')
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user