diff --git a/app/src/agentworld/pages/FeedSection.test.tsx b/app/src/agentworld/pages/FeedSection.test.tsx index c07027c6f..9e8d93a52 100644 --- a/app/src/agentworld/pages/FeedSection.test.tsx +++ b/app/src/agentworld/pages/FeedSection.test.tsx @@ -17,7 +17,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { PaymentRequiredError } from '../../lib/agentworld/invokeApiClient'; import { fetchWalletStatus } from '../../services/walletApi'; import { apiClient } from '../AgentWorldShell'; -import FeedSection from './FeedSection'; +import FeedSection, { FEED_PAGE_SIZE } from './FeedSection'; vi.mock('../AgentWorldShell', () => ({ apiClient: { @@ -739,3 +739,123 @@ describe('delete actions', () => { expect(vi.mocked(apiClient.graphql.post)).toHaveBeenCalledTimes(2); }); }); + +// ── Pagination (offset-based "Load more", #4923) ───────────────────────────── + +/** Build a home-feed page of `n` items with sequential ids from `start`. */ +function buildFeedPage(n: number, start = 0) { + const items = Array.from({ length: n }, (_, i) => { + const idx = start + i; + return { + ...sampleFeedItem, + post: { + ...samplePost, + postId: `post-${String(idx).padStart(4, '0')}`, + // Shared body so pages are countable via getAllByText; distinct, + // decreasing timestamps keep the newest-first sort deterministic. + body: 'PAGEDPOST', + createdAt: new Date(Date.UTC(2026, 0, 1) - idx * 60_000).toISOString(), + }, + }; + }); + return { items, count: 1000 }; +} + +describe('Feed pagination', () => { + test('requests the first page with limit + offset 0', async () => { + vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue({ items: [sampleFeedItem], count: 1 }); + render(); + await waitFor(() => { + expect(screen.getByText(samplePost.body)).toBeInTheDocument(); + }); + expect(apiClient.graphql.homeFeed).toHaveBeenNthCalledWith(1, { + limit: FEED_PAGE_SIZE, + offset: 0, + includeSelf: true, + }); + }); + + test('hides Load more when the first page is shorter than a full page', async () => { + vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue(buildFeedPage(3)); + render(); + await waitFor(() => { + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(3); + }); + expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument(); + }); + + test('shows Load more when the first page fills the page size', async () => { + vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue(buildFeedPage(FEED_PAGE_SIZE)); + render(); + await waitFor(() => { + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); + }); + + test('clicking Load more fetches the next offset, appends items, then stops', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.graphql.homeFeed) + .mockResolvedValueOnce(buildFeedPage(FEED_PAGE_SIZE, 0)) + .mockResolvedValueOnce(buildFeedPage(3, FEED_PAGE_SIZE)); + + render(); + await waitFor(() => { + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(FEED_PAGE_SIZE); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // Second page appended (50 + 3 = 53) and the control disappears because the + // short page signals the feed is exhausted. + await waitFor(() => { + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(FEED_PAGE_SIZE + 3); + }); + expect(apiClient.graphql.homeFeed).toHaveBeenNthCalledWith(2, { + limit: FEED_PAGE_SIZE, + offset: FEED_PAGE_SIZE, + includeSelf: true, + }); + expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument(); + }); + + test('deduplicates overlapping items across pages', async () => { + const user = userEvent.setup(); + // Second page repeats the last id of the first page (post-0049) plus new rows. + vi.mocked(apiClient.graphql.homeFeed) + .mockResolvedValueOnce(buildFeedPage(FEED_PAGE_SIZE, 0)) + .mockResolvedValueOnce(buildFeedPage(FEED_PAGE_SIZE, FEED_PAGE_SIZE - 1)); + + render(); + await waitFor(() => { + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(FEED_PAGE_SIZE); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // 50 initial + 50 returned − 1 overlapping (post-0049) = 99 unique items. + await waitFor(() => { + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(2 * FEED_PAGE_SIZE - 1); + }); + }); + + test('keeps items and surfaces an error when Load more fails', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.graphql.homeFeed) + .mockResolvedValueOnce(buildFeedPage(FEED_PAGE_SIZE, 0)) + .mockRejectedValueOnce(new Error('network failure')); + + render(); + await waitFor(() => { + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); + + await user.click(screen.getByRole('button', { name: /load more/i })); + + // Existing items stay; an error message appears; the control remains for retry. + await waitFor(() => { + expect(screen.getByText(/could not load more posts/i)).toBeInTheDocument(); + }); + expect(screen.getAllByText('PAGEDPOST')).toHaveLength(FEED_PAGE_SIZE); + expect(screen.getByRole('button', { name: /load more/i })).toBeInTheDocument(); + }); +}); diff --git a/app/src/agentworld/pages/FeedSection.tsx b/app/src/agentworld/pages/FeedSection.tsx index 36ae93cfe..8afa9ac52 100644 --- a/app/src/agentworld/pages/FeedSection.tsx +++ b/app/src/agentworld/pages/FeedSection.tsx @@ -25,10 +25,12 @@ import Button from '../../components/ui/Button'; import { type GqlComment, type GqlHomeFeedItem, + type GqlHomeFeedResult, type GqlPost, type LikeResult, PaymentRequiredError, } from '../../lib/agentworld/invokeApiClient'; +import { useT } from '../../lib/i18n/I18nContext'; import { fetchWalletStatus } from '../../services/walletApi'; import { apiClient } from '../AgentWorldShell'; import ConfirmDialog from '../components/ConfirmDialog'; @@ -36,6 +38,15 @@ import { relativeTime } from './relativeTime'; const log = debug('agentworld:feed'); +/** + * Home-feed items fetched per page (also the initial page size). The + * `tinyplace_graphql_home_feed` RPC accepts `limit`/`offset` + * (`src/openhuman/tinyplace/manifest.rs`), so the feed is loaded a page at a + * time and extended via an offset-based "Load more" control. A page shorter + * than this size means the feed is exhausted (`hasMore=false`). + */ +export const FEED_PAGE_SIZE = 50; + // ── State types ─────────────────────────────────────────────────────────────── type FeedState = @@ -43,7 +54,41 @@ type FeedState = | { status: 'wallet_unconfigured' } | { status: 'payment_required'; challenge: unknown } | { status: 'error'; message: string } - | { status: 'ok'; items: GqlHomeFeedItem[] }; + | { + status: 'ok'; + items: GqlHomeFeedItem[]; + // Server-side cursor, in request units: how many rows to skip on the next + // page. Advances by FEED_PAGE_SIZE per fetch, decoupled from the client + // item count so dedupe never desyncs the offset. + nextOffset: number; + // A full page came back, so more items may exist. + hasMore: boolean; + // A "Load more" fetch is in flight. + loadingMore: boolean; + // Non-null when the most recent "Load more" fetch failed (existing items + // stay visible; the user can retry). + moreError: string | null; + }; + +/** + * Build the first-page `ok` state from a home-feed result. Used by the initial + * fetch and by every post-mutation refetch (compose / delete), all of which + * reset pagination to page one. `hasMore` is derived from the raw returned page + * length so a full page signals that older items may still be reachable. + */ +function firstPageFeedState(result: GqlHomeFeedResult | null | undefined): FeedState { + const items = sortedHomeFeedItems(result); + const received = Array.isArray(result?.items) ? result.items.length : 0; + const hasMore = received >= FEED_PAGE_SIZE; + return { + status: 'ok', + items, + nextOffset: FEED_PAGE_SIZE, + hasMore, + loadingMore: false, + moreError: null, + }; +} /** * Result of resolving the local wallet on mount. @@ -588,6 +633,7 @@ function CommentRow({ // ── FeedSection (main export) ───────────────────────────────────────────────── export default function FeedSection() { + const { t } = useT(); const [feedState, setFeedState] = useState({ status: 'loading' }); const [followState, setFollowState] = useState>({}); const [followLoading, setFollowLoading] = useState>({}); @@ -599,6 +645,17 @@ export default function FeedSection() { const { agentId: myAgentId, configured: walletConfigured } = useWalletResolution(); + // Guards async setState after unmount. The initial fetch effect uses its own + // `cancelled` flag; "Load more" fetches outlive no single effect, so they read + // this ref instead. + const mountedRef = useRef(true); + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + // ── Hydrate follow state from the server ─────────────────────────────────── // The home feed doesn't carry "am I following this author?", so seed the // follow map from the wallet's actual following list. Without this, the @@ -654,18 +711,24 @@ export default function FeedSection() { // one they just created via the composer) never appear. Without this the // composer looks broken: Post succeeds server-side but the refetch can't // show it (#4059). + log('loading first feed page', { limit: FEED_PAGE_SIZE }); void apiClient.graphql - .homeFeed({ limit: 50, includeSelf: true }) + .homeFeed({ limit: FEED_PAGE_SIZE, offset: 0, includeSelf: true }) .then(result => { if (cancelled) return; - const items = sortedHomeFeedItems(result); - setFeedState({ status: 'ok', items }); + const next = firstPageFeedState(result); + log('loaded first feed page', { + received: Array.isArray(result?.items) ? result.items.length : 0, + hasMore: next.status === 'ok' ? next.hasMore : false, + }); + setFeedState(next); }) .catch((err: unknown) => { if (cancelled) return; if (err instanceof PaymentRequiredError) { setFeedState({ status: 'payment_required', challenge: err.challenge }); } else { + log('first feed page failed', { error: String(err) }); setFeedState({ status: 'error', message: String(err) }); } }); @@ -675,6 +738,54 @@ export default function FeedSection() { }; }, [walletConfigured]); + // ── Fetch the next page and append it ────────────────────────────────────── + // `offset` is passed in from the rendered 'ok' state so the cursor stays a + // pure function of pages requested. Reentry is prevented by disabling the + // button while `loadingMore` is set. + const loadMore = useCallback((offset: number) => { + log('loading more feed items', { offset, limit: FEED_PAGE_SIZE }); + setFeedState(prev => + prev.status === 'ok' ? { ...prev, loadingMore: true, moreError: null } : prev + ); + + void apiClient.graphql + .homeFeed({ limit: FEED_PAGE_SIZE, offset, includeSelf: true }) + .then(result => { + if (!mountedRef.current) return; + const page = Array.isArray(result?.items) ? result.items : []; + const hasMore = page.length >= FEED_PAGE_SIZE; + setFeedState(prev => { + if (prev.status !== 'ok') return prev; + // Dedupe by postId: if items shifted between page fetches the overlap + // must not produce duplicate React keys or double-counted posts. + const seen = new Set(prev.items.map(item => item.post.postId)); + const fresh = page.filter(item => !seen.has(item.post.postId)); + const merged = sortedHomeFeedItems({ items: [...prev.items, ...fresh] }); + log('appended feed items', { + received: page.length, + fresh: fresh.length, + total: merged.length, + hasMore, + }); + return { + status: 'ok', + items: merged, + nextOffset: offset + FEED_PAGE_SIZE, + hasMore, + loadingMore: false, + moreError: null, + }; + }); + }) + .catch((err: unknown) => { + if (!mountedRef.current) return; + log('load more feed failed', { error: String(err) }); + setFeedState(prev => + prev.status === 'ok' ? { ...prev, loadingMore: false, moreError: String(err) } : prev + ); + }); + }, []); + // ── Follow / Unfollow ────────────────────────────────────────────────────── const handleToggleFollow = async (cryptoId: string) => { @@ -741,11 +852,13 @@ export default function FeedSection() { .then(({ ok }) => { if (!ok) throw new Error('Post deletion was not accepted by the backend'); // Return the refresh promise so its rejection reaches `.catch` (rather - // than resolving the delete as "done" before the feed is reloaded). - return apiClient.graphql.homeFeed({ limit: 50, includeSelf: true }).then(result => { - const items = sortedHomeFeedItems(result); - setFeedState({ status: 'ok', items }); - }); + // than resolving the delete as "done" before the feed is reloaded). A + // mutation invalidates offsets, so reset pagination to the first page. + return apiClient.graphql + .homeFeed({ limit: FEED_PAGE_SIZE, offset: 0, includeSelf: true }) + .then(result => { + setFeedState(firstPageFeedState(result)); + }); }) .catch(err => console.error('[FeedSection] delete post failed:', err)) .finally(() => { @@ -756,11 +869,13 @@ export default function FeedSection() { // ── Refetch feed ─────────────────────────────────────────────────────────── + // A fresh compose invalidates offsets, so reset pagination to the first page. const refetchFeed = () => { - void apiClient.graphql.homeFeed({ limit: 50, includeSelf: true }).then(result => { - const items = sortedHomeFeedItems(result); - setFeedState({ status: 'ok', items }); - }); + void apiClient.graphql + .homeFeed({ limit: FEED_PAGE_SIZE, offset: 0, includeSelf: true }) + .then(result => { + setFeedState(firstPageFeedState(result)); + }); }; // ── Render ───────────────────────────────────────────────────────────────── @@ -812,9 +927,10 @@ export default function FeedSection() { /> ); } else { + const { items, hasMore, loadingMore, moreError, nextOffset } = feedState; body = (
- {feedState.items.map(item => ( + {items.map(item => ( ))} + + {moreError && ( +

+ {t('agentWorld.feed.loadMoreError')} +

+ )} + + {hasMore && ( +
+ +
+ )}
); } diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index c3c55272f..8e77d6b43 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -439,6 +439,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'العالم', 'agentWorld.world.rooms.outside.description': 'ساحة مفتوحة كبيرة تحيط بها المباني.', 'agentWorld.feed': 'التغذية', + 'agentWorld.feed.loadMore': 'تحميل المزيد', + 'agentWorld.feed.loadingMore': 'جارٍ تحميل المزيد…', + 'agentWorld.feed.loadMoreError': 'تعذّر تحميل المزيد من المنشورات. حاول مرة أخرى.', 'agentWorld.ledger': 'السجل', 'agentWorld.ledger.loadMore': 'تحميل المزيد', 'agentWorld.ledger.loadingMore': 'جارٍ تحميل المزيد…', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 867299d24..4b5d0d924 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -455,6 +455,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'বিশ্ব', 'agentWorld.world.rooms.outside.description': 'ভবনঘেরা বড় খোলা প্লাজা।', 'agentWorld.feed': 'ফিড', + 'agentWorld.feed.loadMore': 'আরও লোড করুন', + 'agentWorld.feed.loadingMore': 'আরও লোড হচ্ছে…', + 'agentWorld.feed.loadMoreError': 'আরও পোস্ট লোড করা যায়নি। আবার চেষ্টা করুন।', 'agentWorld.ledger': 'লেজার', 'agentWorld.ledger.loadMore': 'আরও লোড করুন', 'agentWorld.ledger.loadingMore': 'আরও লোড হচ্ছে…', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 58d708c91..eea090bc6 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -479,6 +479,10 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Welt', 'agentWorld.world.rooms.outside.description': 'Ein großer offener Platz mit Gebäuden ringsum.', 'agentWorld.feed': 'Feed', + 'agentWorld.feed.loadMore': 'Mehr laden', + 'agentWorld.feed.loadingMore': 'Wird geladen…', + 'agentWorld.feed.loadMoreError': + 'Weitere Beiträge konnten nicht geladen werden. Bitte erneut versuchen.', 'agentWorld.ledger': 'Kontobuch', 'agentWorld.ledger.loadMore': 'Mehr laden', 'agentWorld.ledger.loadingMore': 'Wird geladen…', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index a44e66941..9682c18cb 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -184,6 +184,9 @@ const en: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'World', 'agentWorld.world.rooms.outside.description': 'A large open plaza ringed with buildings.', 'agentWorld.feed': 'Feed', + 'agentWorld.feed.loadMore': 'Load more', + 'agentWorld.feed.loadingMore': 'Loading more…', + 'agentWorld.feed.loadMoreError': 'Could not load more posts. Try again.', 'agentWorld.ledger': 'Ledger', 'agentWorld.ledger.loadMore': 'Load more', 'agentWorld.ledger.loadingMore': 'Loading more…', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 453fa5c58..79422861c 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -465,6 +465,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Mundo', 'agentWorld.world.rooms.outside.description': 'Una gran plaza abierta rodeada de edificios.', 'agentWorld.feed': 'Noticias', + 'agentWorld.feed.loadMore': 'Cargar más', + 'agentWorld.feed.loadingMore': 'Cargando más…', + 'agentWorld.feed.loadMoreError': 'No se pudieron cargar más publicaciones. Inténtalo de nuevo.', 'agentWorld.ledger': 'Libro mayor', 'agentWorld.ledger.loadMore': 'Cargar más', 'agentWorld.ledger.loadingMore': 'Cargando más…', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 2944fbebf..fd4745f7c 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -475,6 +475,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Monde', 'agentWorld.world.rooms.outside.description': 'Une grande place ouverte entourée de bâtiments.', 'agentWorld.feed': 'Fil', + 'agentWorld.feed.loadMore': 'Charger plus', + 'agentWorld.feed.loadingMore': 'Chargement…', + 'agentWorld.feed.loadMoreError': 'Impossible de charger plus de publications. Réessayez.', 'agentWorld.ledger': 'Grand livre', 'agentWorld.ledger.loadMore': 'Charger plus', 'agentWorld.ledger.loadingMore': 'Chargement…', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 2d4a3d3b3..385652c40 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -455,6 +455,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'दुनिया', 'agentWorld.world.rooms.outside.description': 'इमारतों से घिरा बड़ा खुला प्लाजा।', 'agentWorld.feed': 'फ़ीड', + 'agentWorld.feed.loadMore': 'और लोड करें', + 'agentWorld.feed.loadingMore': 'और लोड हो रहा है…', + 'agentWorld.feed.loadMoreError': 'अधिक पोस्ट लोड नहीं हो सके। फिर से प्रयास करें।', 'agentWorld.ledger': 'खाता बही', 'agentWorld.ledger.loadMore': 'और लोड करें', 'agentWorld.ledger.loadingMore': 'और लोड हो रहा है…', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index c84ddbf8b..94f410555 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -461,6 +461,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Dunia', 'agentWorld.world.rooms.outside.description': 'Plaza terbuka besar yang dikelilingi gedung.', 'agentWorld.feed': 'Feed', + 'agentWorld.feed.loadMore': 'Muat lebih banyak', + 'agentWorld.feed.loadingMore': 'Memuat lagi…', + 'agentWorld.feed.loadMoreError': 'Tidak dapat memuat postingan lainnya. Coba lagi.', 'agentWorld.ledger': 'Buku Besar', 'agentWorld.ledger.loadMore': 'Muat lebih banyak', 'agentWorld.ledger.loadingMore': 'Memuat lagi…', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 406b6eb43..99c30c995 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -468,6 +468,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Mondo', 'agentWorld.world.rooms.outside.description': 'Una grande piazza aperta circondata da edifici.', 'agentWorld.feed': 'Feed', + 'agentWorld.feed.loadMore': 'Carica altro', + 'agentWorld.feed.loadingMore': 'Caricamento…', + 'agentWorld.feed.loadMoreError': 'Impossibile caricare altri post. Riprova.', 'agentWorld.ledger': 'Registro', 'agentWorld.ledger.loadMore': 'Carica altro', 'agentWorld.ledger.loadingMore': 'Caricamento…', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index acb9e186d..eeb6c5b83 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -448,6 +448,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': '월드', 'agentWorld.world.rooms.outside.description': '건물로 둘러싸인 넓은 열린 광장.', 'agentWorld.feed': '피드', + 'agentWorld.feed.loadMore': '더 보기', + 'agentWorld.feed.loadingMore': '더 불러오는 중…', + 'agentWorld.feed.loadMoreError': '게시물을 더 불러오지 못했습니다. 다시 시도하세요.', 'agentWorld.ledger': '원장', 'agentWorld.ledger.loadMore': '더 보기', 'agentWorld.ledger.loadingMore': '더 불러오는 중…', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index aefd8560e..eb2d48eee 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -466,6 +466,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Świat', 'agentWorld.world.rooms.outside.description': 'Duży otwarty plac otoczony budynkami.', 'agentWorld.feed': 'Kanał', + 'agentWorld.feed.loadMore': 'Załaduj więcej', + 'agentWorld.feed.loadingMore': 'Ładowanie…', + 'agentWorld.feed.loadMoreError': 'Nie udało się załadować kolejnych postów. Spróbuj ponownie.', 'agentWorld.ledger': 'Księga', 'agentWorld.ledger.loadMore': 'Załaduj więcej', 'agentWorld.ledger.loadingMore': 'Ładowanie…', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index dc84c0bf5..070eef3f1 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -460,6 +460,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Mundo', 'agentWorld.world.rooms.outside.description': 'Uma grande praça aberta cercada por edifícios.', 'agentWorld.feed': 'Feed', + 'agentWorld.feed.loadMore': 'Carregar mais', + 'agentWorld.feed.loadingMore': 'Carregando mais…', + 'agentWorld.feed.loadMoreError': 'Não foi possível carregar mais publicações. Tente novamente.', 'agentWorld.ledger': 'Livro-razão', 'agentWorld.ledger.loadMore': 'Carregar mais', 'agentWorld.ledger.loadingMore': 'Carregando mais…', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index c1d7222ff..e2e06a07e 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -460,6 +460,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': 'Мир', 'agentWorld.world.rooms.outside.description': 'Большая открытая площадь, окруженная зданиями.', 'agentWorld.feed': 'Лента', + 'agentWorld.feed.loadMore': 'Загрузить ещё', + 'agentWorld.feed.loadingMore': 'Загрузка…', + 'agentWorld.feed.loadMoreError': 'Не удалось загрузить больше публикаций. Попробуйте ещё раз.', 'agentWorld.ledger': 'Реестр', 'agentWorld.ledger.loadMore': 'Загрузить ещё', 'agentWorld.ledger.loadingMore': 'Загрузка…', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 337097b89..68f38625e 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -424,6 +424,9 @@ const messages: TranslationMap = { 'agentWorld.world.rooms.outside.name': '世界', 'agentWorld.world.rooms.outside.description': '一座被建筑环绕的大型开放广场。', 'agentWorld.feed': '动态', + 'agentWorld.feed.loadMore': '加载更多', + 'agentWorld.feed.loadingMore': '正在加载…', + 'agentWorld.feed.loadMoreError': '无法加载更多帖子,请重试。', 'agentWorld.ledger': '账本', 'agentWorld.ledger.loadMore': '加载更多', 'agentWorld.ledger.loadingMore': '正在加载…',