mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
@@ -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(<FeedSection />);
|
||||
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(<FeedSection />);
|
||||
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(<FeedSection />);
|
||||
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(<FeedSection />);
|
||||
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(<FeedSection />);
|
||||
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(<FeedSection />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<FeedState>({ status: 'loading' });
|
||||
const [followState, setFollowState] = useState<Record<string, boolean>>({});
|
||||
const [followLoading, setFollowLoading] = useState<Record<string, boolean>>({});
|
||||
@@ -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 = (
|
||||
<div className="space-y-3">
|
||||
{feedState.items.map(item => (
|
||||
{items.map(item => (
|
||||
<PostCard
|
||||
key={item.post.postId}
|
||||
item={item}
|
||||
@@ -831,6 +947,24 @@ export default function FeedSection() {
|
||||
onDeletePost={handleDeletePost}
|
||||
/>
|
||||
))}
|
||||
|
||||
{moreError && (
|
||||
<p className="text-center text-xs text-red-600 dark:text-red-400">
|
||||
{t('agentWorld.feed.loadMoreError')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={loadingMore}
|
||||
onClick={() => loadMore(nextOffset)}>
|
||||
{loadingMore ? t('agentWorld.feed.loadingMore') : t('agentWorld.feed.loadMore')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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': 'جارٍ تحميل المزيد…',
|
||||
|
||||
@@ -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': 'আরও লোড হচ্ছে…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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': 'और लोड हो रहा है…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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': '더 불러오는 중…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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': 'Загрузка…',
|
||||
|
||||
@@ -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': '正在加载…',
|
||||
|
||||
Reference in New Issue
Block a user