mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
improved component composition and added tests.
This commit is contained in:
@@ -8,13 +8,13 @@
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import type { Account } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { Feed } from '$lib/stores/feed.svelte'
|
||||
import { fetchDirectory, instanceDomain } from '$lib/api/endpoints'
|
||||
import { instanceDomain } from '$lib/api/endpoints'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
import PersonList from '$components/people/PersonList.svelte'
|
||||
import { router } from '$lib/router.svelte'
|
||||
|
||||
const { endpoints, router, session } = useAppServices()
|
||||
let order = $state<'active' | 'new'>('active')
|
||||
let localOnly = $state(true)
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
feed = new Feed<Account>(async (cursor) => {
|
||||
if (!cursor.max_id) offset = 0
|
||||
const limit = cursor.limit ?? 20
|
||||
const items = await fetchDirectory(session.api, {
|
||||
const items = await endpoints.fetchDirectory(session.api, {
|
||||
offset,
|
||||
limit,
|
||||
order: currentOrder,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
/** A full-page composer, reachable from the nav and from "Send Message". */
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { router } from '$lib/router.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
import Composer from '$components/blog/Composer.svelte'
|
||||
|
||||
@@ -12,6 +11,7 @@
|
||||
|
||||
let { to }: Props = $props()
|
||||
|
||||
const { router, session } = useAppServices()
|
||||
const prefill = $derived(to ? `@${to.replace(/^@/, '')} ` : '')
|
||||
</script>
|
||||
|
||||
@@ -26,12 +26,15 @@
|
||||
|
||||
<div class="layout--single">
|
||||
<Module title={to ? 'New message' : 'New entry'}>
|
||||
<Composer
|
||||
initialText={prefill}
|
||||
placeholder={to ? 'Say something…' : 'What are you up to?'}
|
||||
submitLabel={to ? 'Send' : 'Post Entry'}
|
||||
onposted={(status) => router.go(`#/blog/${status.id}`)}
|
||||
/>
|
||||
{#key to}
|
||||
<Composer
|
||||
initialText={prefill}
|
||||
initialVisibility={to ? 'direct' : 'public'}
|
||||
placeholder={to ? 'Say something…' : 'What are you up to?'}
|
||||
submitLabel={to ? 'Send' : 'Post Entry'}
|
||||
onposted={(status) => router.go(`#/blog/${status.id}`)}
|
||||
/>
|
||||
{/key}
|
||||
</Module>
|
||||
|
||||
{#if !session.signedIn}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { APP_SERVICES } from '$lib/app-services'
|
||||
import { account, session, testServices } from '$test/fixtures'
|
||||
import Compose from './Compose.svelte'
|
||||
|
||||
describe('Compose route', () => {
|
||||
it('resets addressing and defaults to direct when the recipient changes', async () => {
|
||||
const services = testServices({
|
||||
session: session({ token: 'token', me: account(), signedIn: true }),
|
||||
})
|
||||
const view = render(Compose, {
|
||||
props: { to: 'alice@example.test' },
|
||||
context: new Map([[APP_SERVICES, services]]),
|
||||
})
|
||||
|
||||
expect(view.getByRole('textbox', { name: 'Entry text' })).toHaveValue('@alice@example.test ')
|
||||
expect(view.getByRole('combobox', { name: 'Who can see this' })).toHaveValue('direct')
|
||||
|
||||
await view.rerender({ to: 'bob@example.test' })
|
||||
|
||||
expect(view.getByRole('textbox', { name: 'Entry text' })).toHaveValue('@bob@example.test ')
|
||||
expect(view.getByRole('combobox', { name: 'Who can see this' })).toHaveValue('direct')
|
||||
})
|
||||
})
|
||||
+45
-19
@@ -8,11 +8,8 @@
|
||||
* what its local timeline looks like right now.
|
||||
*/
|
||||
import type { Account, Notification, Status } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import {
|
||||
fetchFollowing,
|
||||
fetchNotifications,
|
||||
fetchTimeline,
|
||||
instanceDomain,
|
||||
instanceStats,
|
||||
instanceThumbnail,
|
||||
@@ -25,6 +22,8 @@
|
||||
import RichText from '$components/common/RichText.svelte'
|
||||
import Composer from '$components/blog/Composer.svelte'
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
|
||||
let friendStatus = $state<Status[]>([])
|
||||
let bulletins = $state<Status[]>([])
|
||||
let following = $state<Account[]>([])
|
||||
@@ -38,6 +37,7 @@
|
||||
*/
|
||||
let friendStatusError = $state<string | null>(null)
|
||||
let bulletinError = $state<string | null>(null)
|
||||
let loadGeneration = 0
|
||||
|
||||
const me = $derived(session.me)
|
||||
const domain = $derived(session.host ? instanceDomain(session.instance, session.host) : '')
|
||||
@@ -65,13 +65,18 @@
|
||||
const host = session.host
|
||||
const signedIn = session.signedIn
|
||||
if (!host) {
|
||||
loadGeneration += 1
|
||||
loading = false
|
||||
return
|
||||
}
|
||||
void load(signedIn)
|
||||
const generation = ++loadGeneration
|
||||
void load(signedIn, host, generation)
|
||||
return () => {
|
||||
if (generation === loadGeneration) loadGeneration += 1
|
||||
}
|
||||
})
|
||||
|
||||
async function load(signedIn: boolean): Promise<void> {
|
||||
async function load(signedIn: boolean, host = session.host, generation = ++loadGeneration): Promise<void> {
|
||||
loading = true
|
||||
error = null
|
||||
friendStatusError = null
|
||||
@@ -81,32 +86,49 @@
|
||||
// Everything here is independent; a failure in one shouldn't blank the page.
|
||||
const [statusPage, bulletinPage] = await Promise.all([
|
||||
signedIn
|
||||
? fetchTimeline(session.api, 'home', { limit: 10 }).catch((cause) => {
|
||||
friendStatusError = messageOf(cause)
|
||||
? endpoints.fetchTimeline(session.api, 'home', { limit: 10 }).catch((cause) => {
|
||||
if (generation === loadGeneration) friendStatusError = messageOf(cause)
|
||||
return { items: [], links: {} }
|
||||
})
|
||||
: Promise.resolve({ items: [], links: {} }),
|
||||
fetchTimeline(session.api, 'local', { limit: 10 }).catch((cause) => {
|
||||
bulletinError = messageOf(cause)
|
||||
endpoints.fetchTimeline(session.api, 'local', { limit: 10 }).catch((cause) => {
|
||||
if (generation === loadGeneration) bulletinError = messageOf(cause)
|
||||
return { items: [], links: {} }
|
||||
}),
|
||||
])
|
||||
|
||||
if (generation !== loadGeneration || session.host !== host) return
|
||||
friendStatus = statusPage.items
|
||||
bulletins = bulletinPage.items
|
||||
|
||||
if (signedIn && session.me) {
|
||||
void fetchFollowing(session.api, session.me.id, { limit: 12 })
|
||||
.then((page) => (following = page.items))
|
||||
.catch(() => (following = []))
|
||||
void fetchNotifications(session.api, { limit: 40 })
|
||||
.then((page) => (notifications = page.items))
|
||||
.catch(() => (notifications = []))
|
||||
const accountId = session.me.id
|
||||
void endpoints
|
||||
.fetchFollowing(session.api, accountId, { limit: 12 })
|
||||
.then((page) => {
|
||||
if (generation === loadGeneration) following = page.items
|
||||
})
|
||||
.catch(() => {
|
||||
if (generation === loadGeneration) following = []
|
||||
})
|
||||
void endpoints
|
||||
.fetchNotifications(session.api, { limit: 40 })
|
||||
.then((page) => {
|
||||
if (generation === loadGeneration) notifications = page.items
|
||||
})
|
||||
.catch(() => {
|
||||
if (generation === loadGeneration) notifications = []
|
||||
})
|
||||
} else {
|
||||
following = []
|
||||
notifications = []
|
||||
}
|
||||
} catch (cause) {
|
||||
error = cause instanceof Error ? cause.message : 'Could not load your home page.'
|
||||
if (generation === loadGeneration) {
|
||||
error = cause instanceof Error ? cause.message : 'Could not load your home page.'
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
if (generation === loadGeneration) loading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +236,11 @@
|
||||
<div class="layout-column layout-column--main">
|
||||
{#if session.signedIn}
|
||||
<Module title="Post a bulletin">
|
||||
<Composer placeholder="What are you up to?" submitLabel="Post" onposted={() => void load(true)} />
|
||||
<Composer
|
||||
placeholder="What are you up to?"
|
||||
submitLabel="Post"
|
||||
onposted={() => void load(true)}
|
||||
/>
|
||||
</Module>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* server and redirects to its consent screen, while "just look around" only
|
||||
* needs the host and works on any server that allows anonymous API reads.
|
||||
*/
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { normalizeHost } from '$lib/api/client'
|
||||
import { router } from '$lib/router.svelte'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
|
||||
const { router, session } = useAppServices()
|
||||
|
||||
let host = $state(session.host)
|
||||
let busy = $state(false)
|
||||
let error = $state<string | null>(null)
|
||||
|
||||
+11
-12
@@ -9,19 +9,14 @@
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import type { Account, Notification } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { Feed } from '$lib/stores/feed.svelte'
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
fetchFollowRequests,
|
||||
fetchNotifications,
|
||||
rejectFollowRequest,
|
||||
} from '$lib/api/endpoints'
|
||||
import { displayNameOf, fullHandle, profilePath } from '$lib/util/profile'
|
||||
import { toPlainText } from '$lib/util/html'
|
||||
import { stampDate } from '$lib/util/time'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
import Pager from '$components/common/Pager.svelte'
|
||||
import Avatar from '$components/common/Avatar.svelte'
|
||||
|
||||
interface Props {
|
||||
folder?: string
|
||||
@@ -29,6 +24,7 @@
|
||||
|
||||
let { folder = 'inbox' }: Props = $props()
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
interface Folder {
|
||||
key: string
|
||||
label: string
|
||||
@@ -73,11 +69,11 @@
|
||||
|
||||
untrack(() => {
|
||||
if (key === 'requests') {
|
||||
requests = new Feed<Account>((cursor) => fetchFollowRequests(session.api, cursor))
|
||||
requests = new Feed<Account>((cursor) => endpoints.fetchFollowRequests(session.api, cursor))
|
||||
void requests.reload()
|
||||
} else {
|
||||
notifications = new Feed<Notification>((cursor) =>
|
||||
fetchNotifications(session.api, cursor, types.length > 0 ? types : undefined),
|
||||
endpoints.fetchNotifications(session.api, cursor, types.length > 0 ? types : undefined),
|
||||
)
|
||||
void notifications.reload()
|
||||
}
|
||||
@@ -90,7 +86,10 @@
|
||||
busyIds = { ...busyIds, [account.id]: true }
|
||||
actionError = null
|
||||
try {
|
||||
await (approve ? authorizeFollowRequest : rejectFollowRequest)(session.api, account.id)
|
||||
await (approve ? endpoints.authorizeFollowRequest : endpoints.rejectFollowRequest)(
|
||||
session.api,
|
||||
account.id,
|
||||
)
|
||||
requests.remove(account.id)
|
||||
} catch (cause) {
|
||||
actionError = cause instanceof Error ? cause.message : 'That didn’t work.'
|
||||
@@ -156,7 +155,7 @@
|
||||
<tr class="mail-row" data-kind="follow_request" data-account={account.acct}>
|
||||
<td class="mail-table-from">
|
||||
<a href={profilePath(account)}>
|
||||
<img src={account.avatar_static || account.avatar} alt="" loading="lazy" />
|
||||
<Avatar {account} plain />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@@ -210,7 +209,7 @@
|
||||
<td class="mail-table-date">{stampDate(item.created_at)}</td>
|
||||
<td class="mail-table-from">
|
||||
<a href={profilePath(item.account)}>
|
||||
<img src={item.account.avatar_static || item.account.avatar} alt="" loading="lazy" />
|
||||
<Avatar account={item.account} plain />
|
||||
</a>
|
||||
</td>
|
||||
<td class="mail-table-subject">
|
||||
|
||||
+37
-27
@@ -11,15 +11,9 @@
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import type { Account, Relationship, Status } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { Feed } from '$lib/stores/feed.svelte'
|
||||
import { theme, profileCssFromFields } from '$lib/stores/theme.svelte'
|
||||
import {
|
||||
fetchAccountStatuses,
|
||||
fetchFollowers,
|
||||
fetchRelationship,
|
||||
lookupAccount,
|
||||
} from '$lib/api/endpoints'
|
||||
import { profileCssFromFields } from '$lib/stores/theme.svelte'
|
||||
import {
|
||||
buildProfileView,
|
||||
displayNameOf,
|
||||
@@ -46,6 +40,7 @@
|
||||
|
||||
let { acct, view = 'profile' }: Props = $props()
|
||||
|
||||
const { endpoints, session, theme } = useAppServices()
|
||||
let account = $state<Account | null>(null)
|
||||
let relationship = $state<Relationship | null>(null)
|
||||
let loading = $state(true)
|
||||
@@ -53,6 +48,7 @@
|
||||
|
||||
let friends = $state<Account[]>([])
|
||||
let friendsLoading = $state(false)
|
||||
let loadGeneration = 0
|
||||
|
||||
// Recreated whenever the account changes, so the feed never shows one
|
||||
// person's entries under another's name.
|
||||
@@ -77,20 +73,32 @@
|
||||
const host = session.host
|
||||
if (!host) return
|
||||
|
||||
untrack(() => void load(handle, currentView))
|
||||
const generation = ++loadGeneration
|
||||
theme.clearProfileCss()
|
||||
untrack(() => void load(handle, currentView, host, generation))
|
||||
return () => {
|
||||
if (generation === loadGeneration) loadGeneration += 1
|
||||
theme.clearProfileCss()
|
||||
}
|
||||
})
|
||||
|
||||
async function load(handle: string, currentView: Props['view']): Promise<void> {
|
||||
async function load(
|
||||
handle: string,
|
||||
currentView: Props['view'],
|
||||
host: string,
|
||||
generation: number,
|
||||
): Promise<void> {
|
||||
loading = true
|
||||
error = null
|
||||
account = null
|
||||
relationship = null
|
||||
friends = []
|
||||
friendsLoading = false
|
||||
|
||||
try {
|
||||
const found = await lookupAccount(session.api, handle)
|
||||
const found = await endpoints.lookupAccount(session.api, handle)
|
||||
// A newer navigation won the race.
|
||||
if (acct !== handle) return
|
||||
if (generation !== loadGeneration || acct !== handle || session.host !== host) return
|
||||
|
||||
account = found
|
||||
document.title = `${displayNameOf(found)} | plspace`
|
||||
@@ -99,7 +107,7 @@
|
||||
|
||||
entries = new Feed<Status>(
|
||||
(cursor) =>
|
||||
fetchAccountStatuses(session.api, found.id, cursor, {
|
||||
endpoints.fetchAccountStatuses(session.api, found.id, cursor, {
|
||||
// The profile page mirrors "Latest Blog Entries": top-level posts.
|
||||
exclude_replies: currentView !== 'blog',
|
||||
}),
|
||||
@@ -107,43 +115,45 @@
|
||||
)
|
||||
void entries.reload()
|
||||
|
||||
void loadFriends(found, currentView)
|
||||
void loadRelationship(found)
|
||||
void loadFriends(found, currentView, generation)
|
||||
void loadRelationship(found, generation)
|
||||
} catch (cause) {
|
||||
if (acct !== handle) return
|
||||
if (generation !== loadGeneration) return
|
||||
error = cause instanceof Error ? cause.message : 'Could not load that profile.'
|
||||
} finally {
|
||||
if (acct === handle) loading = false
|
||||
if (generation === loadGeneration) loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFriends(target: Account, currentView: Props['view']): Promise<void> {
|
||||
async function loadFriends(
|
||||
target: Account,
|
||||
currentView: Props['view'],
|
||||
generation: number,
|
||||
): Promise<void> {
|
||||
if (followersHidden(target)) return
|
||||
friendsLoading = true
|
||||
try {
|
||||
const page = await fetchFollowers(session.api, target.id, {
|
||||
const page = await endpoints.fetchFollowers(session.api, target.id, {
|
||||
limit: currentView === 'friends' ? 40 : FRIEND_PREVIEW,
|
||||
})
|
||||
if (account?.id === target.id) friends = page.items
|
||||
if (generation === loadGeneration && account?.id === target.id) friends = page.items
|
||||
} catch {
|
||||
// Hidden or unavailable follower lists are normal; the count still shows.
|
||||
if (account?.id === target.id) friends = []
|
||||
if (generation === loadGeneration && account?.id === target.id) friends = []
|
||||
} finally {
|
||||
friendsLoading = false
|
||||
if (generation === loadGeneration) friendsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRelationship(target: Account): Promise<void> {
|
||||
async function loadRelationship(target: Account, generation: number): Promise<void> {
|
||||
try {
|
||||
const found = await fetchRelationship(session.api, target.id)
|
||||
if (account?.id === target.id) relationship = found
|
||||
const found = await endpoints.fetchRelationship(session.api, target.id)
|
||||
if (generation === loadGeneration && account?.id === target.id) relationship = found
|
||||
} catch {
|
||||
/* relationships need auth; absence is fine */
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => () => theme.clearProfileCss())
|
||||
|
||||
/** One line of an entry, for the headline list on the profile page. */
|
||||
function teaserFor(entry: Status): string {
|
||||
const text = toPlainText(entry.spoiler_text || entry.content)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { act, render } from '@testing-library/svelte'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { APP_SERVICES } from '$lib/app-services'
|
||||
import type { Account } from '$lib/api/types'
|
||||
import { account, deferred, session, testServices, theme } from '$test/fixtures'
|
||||
import Profile from './Profile.svelte'
|
||||
|
||||
describe('Profile', () => {
|
||||
it('does not apply profile CSS after it has unmounted', async () => {
|
||||
const lookup = deferred<Account>()
|
||||
const applyProfileCss = vi.fn()
|
||||
const services = testServices({
|
||||
session: session(),
|
||||
theme: theme({ applyProfileCss }),
|
||||
endpoints: { lookupAccount: vi.fn(() => lookup.promise) },
|
||||
})
|
||||
const view = render(Profile, {
|
||||
props: { acct: 'alice' },
|
||||
context: new Map([[APP_SERVICES, services]]),
|
||||
})
|
||||
|
||||
view.unmount()
|
||||
await act(() => lookup.resolve(account({ fields: [{ name: 'css', value: '.x { color: red }' }] })))
|
||||
|
||||
expect(applyProfileCss).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -7,9 +7,8 @@
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import type { SearchResults } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { search } from '$lib/api/endpoints'
|
||||
import { router, routeTo } from '$lib/router.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { routeTo } from '$lib/router.svelte'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
import PersonRow from '$components/people/PersonRow.svelte'
|
||||
import BlogEntry from '$components/blog/BlogEntry.svelte'
|
||||
@@ -21,32 +20,43 @@
|
||||
|
||||
let { q = '' }: Props = $props()
|
||||
|
||||
const { endpoints, router, session } = useAppServices()
|
||||
// Seeded from the route, then kept in sync by the effect below.
|
||||
let input = $state(untrack(() => q))
|
||||
let results = $state<SearchResults | null>(null)
|
||||
let loading = $state(false)
|
||||
let error = $state<string | null>(null)
|
||||
let generation = 0
|
||||
|
||||
$effect(() => {
|
||||
const query = q
|
||||
input = query
|
||||
if (!query || !session.host) {
|
||||
generation += 1
|
||||
results = null
|
||||
loading = false
|
||||
error = null
|
||||
return
|
||||
}
|
||||
untrack(() => void run(query))
|
||||
return () => {
|
||||
generation += 1
|
||||
}
|
||||
})
|
||||
|
||||
async function run(query: string): Promise<void> {
|
||||
const currentGeneration = ++generation
|
||||
loading = true
|
||||
error = null
|
||||
try {
|
||||
const found = await search(session.api, query, { limit: 20 })
|
||||
if (q === query) results = found
|
||||
const found = await endpoints.search(session.api, query, { limit: 20 })
|
||||
if (currentGeneration === generation) results = found
|
||||
} catch (cause) {
|
||||
if (q === query) error = cause instanceof Error ? cause.message : 'Search failed.'
|
||||
if (currentGeneration === generation) {
|
||||
error = cause instanceof Error ? cause.message : 'Search failed.'
|
||||
}
|
||||
} finally {
|
||||
if (q === query) loading = false
|
||||
if (currentGeneration === generation) loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { APP_SERVICES } from '$lib/app-services'
|
||||
import type { SearchResults } from '$lib/api/types'
|
||||
import { deferred, session, testServices } from '$test/fixtures'
|
||||
import Search from './Search.svelte'
|
||||
|
||||
describe('Search', () => {
|
||||
it('returns to the empty state when a pending query is cleared', async () => {
|
||||
const pending = deferred<SearchResults>()
|
||||
const services = testServices({
|
||||
session: session(),
|
||||
endpoints: { search: vi.fn(() => pending.promise) },
|
||||
})
|
||||
const view = render(Search, {
|
||||
props: { q: 'alice' },
|
||||
context: new Map([[APP_SERVICES, services]]),
|
||||
})
|
||||
|
||||
expect(await view.findByText('Searching…')).toBeInTheDocument()
|
||||
await view.rerender({ q: '' })
|
||||
|
||||
expect(view.getByText('Enter something to search for.')).toBeInTheDocument()
|
||||
expect(view.queryByText('Searching…')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -7,13 +7,15 @@
|
||||
* locally), and whether the CSS other people publish on their profiles is
|
||||
* honoured when you visit them.
|
||||
*/
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { theme, CSS_FIELD_NAMES, PROFILE_SCOPE } from '$lib/stores/theme.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { CSS_FIELD_NAMES, PROFILE_SCOPE } from '$lib/stores/theme.svelte'
|
||||
import { PRESETS, EXAMPLE_CSS } from '$lib/themes'
|
||||
import { instanceDomain } from '$lib/api/endpoints'
|
||||
import { displayNameOf, profilePath } from '$lib/util/profile'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
|
||||
const { session, theme } = useAppServices()
|
||||
|
||||
let draft = $state(theme.viewerCss)
|
||||
let saved = $state(false)
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import type { Status } from '$lib/api/types'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { fetchContext, fetchStatus } from '$lib/api/endpoints'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { displayNameOf, profilePath } from '$lib/util/profile'
|
||||
import { stampDate, isoDate } from '$lib/util/time'
|
||||
import { toPlainText } from '$lib/util/html'
|
||||
@@ -25,11 +24,13 @@
|
||||
|
||||
let { id }: Props = $props()
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
let status = $state<Status | null>(null)
|
||||
let ancestors = $state<Status[]>([])
|
||||
let descendants = $state<Status[]>([])
|
||||
let loading = $state(true)
|
||||
let error = $state<string | null>(null)
|
||||
let loadGeneration = 0
|
||||
|
||||
interface ThreadedReply {
|
||||
status: Status
|
||||
@@ -80,28 +81,32 @@
|
||||
const currentId = id
|
||||
const host = session.host
|
||||
if (!host) return
|
||||
untrack(() => void load(currentId))
|
||||
const generation = ++loadGeneration
|
||||
untrack(() => void load(currentId, host, generation))
|
||||
return () => {
|
||||
if (generation === loadGeneration) loadGeneration += 1
|
||||
}
|
||||
})
|
||||
|
||||
async function load(currentId: string): Promise<void> {
|
||||
async function load(currentId: string, host: string, generation: number): Promise<void> {
|
||||
loading = true
|
||||
error = null
|
||||
try {
|
||||
const [entry, context] = await Promise.all([
|
||||
fetchStatus(session.api, currentId),
|
||||
fetchContext(session.api, currentId).catch(() => ({ ancestors: [], descendants: [] })),
|
||||
endpoints.fetchStatus(session.api, currentId),
|
||||
endpoints.fetchContext(session.api, currentId).catch(() => ({ ancestors: [], descendants: [] })),
|
||||
])
|
||||
if (id !== currentId) return
|
||||
if (generation !== loadGeneration || id !== currentId || session.host !== host) return
|
||||
|
||||
status = entry
|
||||
ancestors = context.ancestors
|
||||
descendants = context.descendants
|
||||
document.title = `${toPlainText(entry.content).slice(0, 60)} | plspace`
|
||||
} catch (cause) {
|
||||
if (id !== currentId) return
|
||||
if (generation !== loadGeneration) return
|
||||
error = cause instanceof Error ? cause.message : 'Could not load that entry.'
|
||||
} finally {
|
||||
if (id === currentId) loading = false
|
||||
if (generation === loadGeneration) loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import { untrack } from 'svelte'
|
||||
import type { Status } from '$lib/api/types'
|
||||
import type { TimelineKind } from '$lib/api/endpoints'
|
||||
import { session } from '$lib/stores/session.svelte'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { Feed } from '$lib/stores/feed.svelte'
|
||||
import { fetchTimeline, instanceDomain } from '$lib/api/endpoints'
|
||||
import { instanceDomain } from '$lib/api/endpoints'
|
||||
import Module from '$components/common/Module.svelte'
|
||||
import TabBar from '$components/common/TabBar.svelte'
|
||||
import BlogList from '$components/blog/BlogList.svelte'
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
let { kind, tag }: Props = $props()
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
let feed = $state<Feed<Status>>(new Feed<Status>(async () => ({ items: [], links: {} })))
|
||||
|
||||
const domain = $derived(session.host ? instanceDomain(session.instance, session.host) : '')
|
||||
@@ -54,7 +55,7 @@
|
||||
// Home needs a token; fall back rather than showing a 401.
|
||||
const resolved: TimelineKind = currentKind === 'home' && !authed ? 'local' : currentKind
|
||||
feed = new Feed<Status>((cursor) =>
|
||||
fetchTimeline(session.api, resolved, cursor, { tag: currentTag }),
|
||||
endpoints.fetchTimeline(session.api, resolved, cursor, { tag: currentTag }),
|
||||
)
|
||||
void feed.reload()
|
||||
document.title = `${title} | plspace`
|
||||
|
||||
Reference in New Issue
Block a user