forked from blisstown/campfire
332 lines
9.3 KiB
Svelte
332 lines
9.3 KiB
Svelte
<script>
|
|
import Button from '@cf/ui/Button.svelte';
|
|
import HomeIcon from '@cf/icons/unlisted.svg';
|
|
import MoreIcon from '@cf/icons/more.svg';
|
|
import LockIcon from '@cf/icons/lock.svg';
|
|
import BotIcon from '@cf/icons/bot.svg';
|
|
import Lang from '$lib/lang';
|
|
import * as api from '$lib/api.js';
|
|
import { server, createServer } from '$lib/client/server.js';
|
|
import { app } from '$lib/client/app.js';
|
|
import { parseAccount } from '$lib/account.js';
|
|
import { parsePost } from '$lib/post.js';
|
|
import { account } from '$lib/stores/account';
|
|
import { goto, afterNavigate } from '$app/navigation';
|
|
import { base } from '$app/paths';
|
|
import Post from '../../../lib/ui/post/Post.svelte';
|
|
import { writable } from 'svelte/store';
|
|
|
|
export let data;
|
|
|
|
const lang = Lang('en_GB');
|
|
|
|
let profile_pinned_posts = writable([]);
|
|
let profile_posts_max_id = null;
|
|
let profile_posts = writable([]);
|
|
let profile = fetchProfile(data.account_handle);
|
|
let error = false;
|
|
let previous_page = base;
|
|
|
|
let post_replies = false;
|
|
let post_boosts = true;
|
|
let post_media = false;
|
|
|
|
afterNavigate(({from}) => {
|
|
previous_page = from?.url.pathname || previous_page;
|
|
profile = fetchProfile(data.account_handle);
|
|
})
|
|
|
|
async function getPosts(profile, max_id) {
|
|
const posts = await api.getUserPosts(
|
|
$server.host,
|
|
$app.token,
|
|
profile.id,
|
|
max_id,
|
|
post_replies,
|
|
post_boosts,
|
|
post_media
|
|
);
|
|
let parsed_posts = [];
|
|
for (let post of posts) {
|
|
parsed_posts.push(await parsePost(post, 1));
|
|
}
|
|
profile_posts.update(posts => {
|
|
posts.push(...parsed_posts);
|
|
return posts;
|
|
});
|
|
return parsed_posts.length > 0 ? parsed_posts[parsed_posts.length - 1].id : null;
|
|
}
|
|
|
|
async function fetchProfile(handle) {
|
|
let token = $app ? $app.token : null;
|
|
|
|
profile_posts.set([]);
|
|
profile_pinned_posts.set([]);
|
|
|
|
if (!$server || $server.host !== data.server_host) {
|
|
server.set(await createServer(data.server_host));
|
|
if (!$server) {
|
|
error = lang.string('error.connection_failed', data.server_host);
|
|
throw new Error(lang.string('logs.connection_failed', data.server_host));
|
|
}
|
|
}
|
|
|
|
let profile_data;
|
|
try {
|
|
profile_data = await api.lookupUser($server.host, token, handle);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
|
|
if (!profile_data || profile_data.error) {
|
|
error = lang.string('error.profile_fetch_failed_id', handle);
|
|
throw new Error(lang.string('logs.profile_fetch_failed_id', handle));
|
|
}
|
|
let profile = await parseAccount(profile_data, 0);
|
|
|
|
api.getUserPinnedPosts(
|
|
$server.host,
|
|
token,
|
|
profile.id,
|
|
).then(async posts => {
|
|
for (let post of posts) {
|
|
const parsedPost = await parsePost(post, 1);
|
|
profile_pinned_posts.update(posts => {
|
|
posts.push(parsedPost);
|
|
return posts;
|
|
});
|
|
}
|
|
});
|
|
|
|
let post_lock = false; // `true` == "locked"
|
|
getPosts(profile, null).then(last_id => {
|
|
profile_posts_max_id = last_id;
|
|
post_lock = false;
|
|
});
|
|
|
|
document.addEventListener("scroll", () => {
|
|
if (window.innerHeight + window.scrollY < document.body.offsetHeight - 2048)
|
|
return;
|
|
if ($profile_posts.length == 0)
|
|
return;
|
|
if (profile_posts_max_id == null)
|
|
return;
|
|
if (profile_posts_max_id != $profile_posts[$profile_posts.length - 1].id)
|
|
return;
|
|
|
|
if (post_lock) return;
|
|
post_lock = true;
|
|
|
|
getPosts(profile, profile_posts_max_id).then(last_id => {
|
|
profile_posts_max_id = last_id;
|
|
post_lock = false;
|
|
});
|
|
});
|
|
|
|
return profile;
|
|
}
|
|
</script>
|
|
|
|
{#await profile}
|
|
<div class="loading throb">
|
|
<span>{lang.string('profile.loading')}</span>
|
|
</div>
|
|
{:then profile}
|
|
<header data-banner="{profile.banner_url}">
|
|
<img src="{profile.banner_url}" class="profile-banner" alt="">
|
|
<div class="profile-tag">
|
|
<!-- svelte-ignore a11y-img-redundant-alt -->
|
|
<img src="{profile.avatar_url}" alt="">
|
|
<div class="profile-tag-names">
|
|
<div class="profile-tag-display-name">
|
|
<h1>
|
|
{@html profile.rich_name}
|
|
{#if profile.locked}
|
|
<span title="{lang.string('profile.locked')}">
|
|
<LockIcon width="22px"/>
|
|
</span>
|
|
{/if}
|
|
{#if profile.bot}
|
|
<span title="{lang.string('profile.bot')}">
|
|
<BotIcon width="22px"/>
|
|
</span>
|
|
{/if}
|
|
</h1>
|
|
</div>
|
|
<p>{profile.fqn}</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div class="profile-info">
|
|
<p class="profile-bio">{@html profile.bio}</p>
|
|
<ul class="profile-counts">
|
|
<li><b>{lang.string('profile.followers')}</b> {profile.followers_count}</li>
|
|
<li><b>{lang.string('profile.following')}</b> {profile.following_count}</li>
|
|
<li><b>{lang.string('profile.posts')}</b> {profile.posts_count}</li>
|
|
</ul>
|
|
<div class="profile-actions">
|
|
{#if $account && profile.fqn !== $account.fqn}
|
|
<Button filled disabled label="{lang.string('profile.follow')} {profile.nickname}" class="profile-btn-follow">
|
|
{lang.string('profile.follow')}
|
|
</Button>
|
|
{/if}
|
|
<Button label="{lang.string('profile.home_instance')}" href="{profile.url}">
|
|
<HomeIcon width="24px"/>
|
|
</Button>
|
|
<Button>
|
|
<MoreIcon width="24px"/>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="profile-post-categories">
|
|
<Button active>
|
|
{lang.string('profile.posts')}
|
|
</Button>
|
|
<Button>
|
|
{lang.string('profile.replies')}
|
|
</Button>
|
|
<Button>
|
|
{lang.string('profile.media')}
|
|
</Button>
|
|
</div>
|
|
|
|
<div class="profile-pinned-posts">
|
|
{#if profile_pinned_posts}
|
|
{#each $profile_pinned_posts as post}
|
|
<Post post_data={post} pinned />
|
|
{/each}
|
|
<br/><hr/><br/>
|
|
{/if}
|
|
</div>
|
|
<div class="profile-posts">
|
|
{#each $profile_posts as post}
|
|
<Post post_data={post} />
|
|
{/each}
|
|
</div>
|
|
{:catch error}
|
|
<p class="error">{error}</p>
|
|
{/await}
|
|
|
|
<style>
|
|
header {
|
|
margin-top: 1rem;
|
|
width: 100%;
|
|
height: 215px;
|
|
position: relative;
|
|
}
|
|
|
|
.profile-banner {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
background-color: var(--bg-700);
|
|
}
|
|
|
|
.profile-tag {
|
|
position: absolute;
|
|
bottom: 16px;
|
|
left: 16px;
|
|
background: color-mix(in srgb, transparent, var(--bg-1000) 50%);
|
|
backdrop-filter: blur(8px);
|
|
width: fit-content;
|
|
display: flex;
|
|
height: 64px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.profile-tag img {
|
|
aspect-ratio: 1;
|
|
border-top-left-radius: 8px;
|
|
border-bottom-left-radius: 8px;
|
|
}
|
|
|
|
.profile-tag-names {
|
|
padding: 8px 16px;
|
|
align-self: center;
|
|
}
|
|
|
|
.profile-tag-names * {
|
|
margin: 0;
|
|
}
|
|
|
|
.profile-tag-names h1 {
|
|
font-size: 1.15rem
|
|
}
|
|
|
|
.profile-tag-names h1 :global(svg) {
|
|
height: 1.2em;
|
|
width: 1.2em;
|
|
margin: -1em -.1em 0 -.1em;
|
|
transform: translateY(.2em);
|
|
}
|
|
|
|
.profile-info {
|
|
background-color: var(--bg-800);
|
|
padding: 16px;
|
|
border-bottom-left-radius: 8px;
|
|
border-bottom-right-radius: 8px;
|
|
}
|
|
|
|
.profile-bio {
|
|
margin: 0;
|
|
|
|
/* !! may not be required in prod */
|
|
white-space: pre-line;
|
|
}
|
|
:global(.profile-bio p:first-of-type) {
|
|
margin: 0;
|
|
}
|
|
|
|
.profile-counts {
|
|
padding: 0;
|
|
}
|
|
.profile-counts li {
|
|
display: inline-block;
|
|
}
|
|
.profile-counts > *:not(.profile-counts:first-child) {
|
|
margin-right: 16px;
|
|
}
|
|
|
|
.profile-actions {
|
|
width: fit-content;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
}
|
|
|
|
.profile-actions :global(button.profile-btn-follow) {
|
|
padding: 0 32px;
|
|
}
|
|
|
|
.profile-actions :global(a) {
|
|
width: fit-content;
|
|
height: 16px;
|
|
}
|
|
.profile-actions :global(button) {
|
|
width: fit-content;
|
|
height: 42px;
|
|
}
|
|
|
|
.profile-post-categories {
|
|
display: flex;
|
|
padding: 1rem 0;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.loading {
|
|
width: 100%;
|
|
height: 80vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.error {
|
|
padding: 4em 0;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
</style>
|