forked from blisstown/campfire
initial profile implementation!
This commit is contained in:
parent
667b11f2f4
commit
449a11ee55
14 changed files with 212 additions and 57 deletions
|
@ -94,7 +94,9 @@
|
|||
|
||||
"posts": "Posts",
|
||||
"replies": "Replies",
|
||||
"media": "Media"
|
||||
"media": "Media",
|
||||
|
||||
"loading": "loading profile..."
|
||||
},
|
||||
|
||||
"logs": {
|
||||
|
@ -104,9 +106,12 @@
|
|||
"no_hostname": "Attempted to connect to a server without providing a hostname",
|
||||
"no_https": "Cowardly refusing to connect to an insecure server",
|
||||
"connection_failed": "Failed to connect to %1",
|
||||
"post_fetch_failed": "Failed to fetch post %1",
|
||||
"post_fetch_failed": "Failed to fetch post",
|
||||
"post_fetch_failed_id": "Failed to fetch post %1",
|
||||
"post_parse_failed": "Failed to parse post",
|
||||
"post_parse_failed_id": "Failed to parse post %1",
|
||||
"profile_fetch_failed": "Failed to fetch profile",
|
||||
"profile_fetch_failed_id": "Failed to fetch profile %1",
|
||||
"token_revoke_failed": "Token revocation failed! Dumping data anyways",
|
||||
"sound_does_not_exist": "Attempted to play sound \"%1\", which does not exist!",
|
||||
"account_data_empty": "Attempted to parse account data but no data was provided",
|
||||
|
|
|
@ -27,7 +27,12 @@ export function parseAccount(data) {
|
|||
account.username = data.username;
|
||||
account.name = account.nickname || account.username;
|
||||
account.avatar_url = data.avatar;
|
||||
account.banner_url = data.header;
|
||||
account.url = data.url;
|
||||
account.followers_count = data.followers_count;
|
||||
account.following_count = data.following_count;
|
||||
account.posts_count = data.statuses_count;
|
||||
account.bio = data.note;
|
||||
|
||||
if (data.acct.includes('@'))
|
||||
account.host = data.acct.split('@')[1];
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
const errors = {
|
||||
AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED",
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/v1/instance
|
||||
* @param {string} host - The domain of the target server.
|
||||
|
@ -421,3 +425,26 @@ export async function getUser(host, token, user_id) {
|
|||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/accounts/lookup?acct={handle}
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} handle - The handle of the user to fetch.
|
||||
*/
|
||||
export async function lookupUser(host, token, handle) {
|
||||
let url = `https://${host}/api/v1/accounts/lookup?acct=${handle}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
if (json.error = errors.AUTHENTICATION_FAILED)
|
||||
throw new Error("This method requires authentication");
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ import * as api from '$lib/api.js';
|
|||
import { writable } from 'svelte/store';
|
||||
import { app_name } from '$lib/config.js';
|
||||
import { browser } from "$app/environment";
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
||||
const server_types = {
|
||||
UNSUPPORTED: "unsupported",
|
||||
|
|
|
@ -18,9 +18,21 @@
|
|||
function click() {
|
||||
if (disabled) return;
|
||||
if (href) {
|
||||
location = href;
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.dispatchEvent(new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
altKey: event.altKey,
|
||||
button: event.button,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
playSound(sound);
|
||||
dispatch('click');
|
||||
}
|
||||
|
|
|
@ -90,7 +90,8 @@
|
|||
|
||||
<div class="composer">
|
||||
<div class="composer-header-container">
|
||||
<a href={$account.url} target="_blank" class="composer-avatar-container" on:mouseup|stopPropagation>
|
||||
<!-- TODO: account switcher in composer -->
|
||||
<a href="" class="composer-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={$account.avatar_url} type={$account.avatar_type} alt="" width="48" height="48" class="composer-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<header class="composer-header">
|
||||
|
|
|
@ -61,6 +61,16 @@
|
|||
goto(route);
|
||||
}
|
||||
|
||||
function gotoProfile() {
|
||||
if (!$account) return;
|
||||
playSound();
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
goto(`/${$server.host}/${$account.username}`);
|
||||
}
|
||||
|
||||
async function log_out() {
|
||||
if (!confirm("This will log you out. Are you sure?")) return;
|
||||
|
||||
|
@ -171,9 +181,9 @@
|
|||
</div>
|
||||
|
||||
<div id="account-button">
|
||||
<img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => playSound()}>
|
||||
<img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => gotoProfile()}>
|
||||
<div class="account-name" aria-hidden="true">
|
||||
<a href={$account.url} class="nickname" title={$account.nickname}>{@html $account.rich_name}</a>
|
||||
<a href="/{$server.host}/{$account.username}" class="nickname" title={$account.nickname}>{@html $account.rich_name}</a>
|
||||
<span class="username" title={`@${$account.username}@${$account.host}`}>
|
||||
{$account.fqn}
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { shorthand as short_time } from '$lib/time.js';
|
||||
import { server } from '$lib/client/server';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
@ -14,7 +15,7 @@
|
|||
<span class="post-context-action">
|
||||
{ @html
|
||||
lang.string('post.boosted',
|
||||
`<a href={${post.account.url}} target="_blank"><span class="name">${post.account.rich_name}</span></a>`)
|
||||
`<a href="/${$server.host}/${post.account.fqn}"><span class="name">${post.account.rich_name}</span></a>`)
|
||||
}
|
||||
</span>
|
||||
<span class="post-context-time">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { shorthand as short_time } from '$lib/time.js';
|
||||
import { server } from '$lib/client/server';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
@ -11,12 +12,12 @@
|
|||
</script>
|
||||
|
||||
<div class={"post-header-container" + (reply ? " reply" : "")}>
|
||||
<a href={post.account.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation>
|
||||
<a href="/{$server.host}/{post.account.fqn}" class="post-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<header class="post-header">
|
||||
<div class="post-user-info" on:mouseup|stopPropagation>
|
||||
<a href={post.account.url} target="_blank" class="name">{@html post.account.rich_name}</a>
|
||||
<a href="/{$server.host}/{post.account.fqn}" class="name">{@html post.account.rich_name}</a>
|
||||
<span class="username">{post.account.mention}</span>
|
||||
</div>
|
||||
<div class="post-info" on:mouseup|stopPropagation>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export async function load({ params }) {
|
||||
return {
|
||||
server_domain: params.server
|
||||
server_host: params.server
|
||||
};
|
||||
}
|
||||
|
|
10
src/routes/[server]/[account]/+page.js
Normal file
10
src/routes/[server]/[account]/+page.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export async function load({ params }) {
|
||||
let handle = params.account;
|
||||
if (handle.startsWith('@'))
|
||||
handle = handle.substring(1);
|
||||
|
||||
return {
|
||||
server_host: params.server,
|
||||
account_handle: handle,
|
||||
};
|
||||
}
|
|
@ -3,45 +3,86 @@
|
|||
import HomeIcon from '@cf/icons/unlisted.svg';
|
||||
import MoreIcon from '@cf/icons/more.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 { account } from '$lib/stores/account';
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let data;
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
||||
let profile = fetchProfile(data.account_handle);
|
||||
let error = false;
|
||||
let previous_page = base;
|
||||
|
||||
afterNavigate(({from}) => {
|
||||
previous_page = from?.url.pathname || previous_page;
|
||||
profile = fetchProfile(data.account_handle);
|
||||
})
|
||||
|
||||
async function fetchProfile(handle) {
|
||||
let token = $app ? $app.token : null;
|
||||
|
||||
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);
|
||||
console.debug(profile_data);
|
||||
} 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);
|
||||
|
||||
return profile;
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
{#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="https://f.mae.wtf/proxy/avatar.webp?url=https%3A%2F%2Ff.mae.wtf%2Ffiles%2F9cf9f3f1-70f6-4ecc-be2b-34ae6588bbdc&avatar=1" alt="Profile picture">
|
||||
<img src="{profile.avatar_url}" alt="">
|
||||
<div class="profile-tag-names">
|
||||
<h1>mae</h1>
|
||||
<p>mae@f.mae.wtf</p>
|
||||
<h1>{@html profile.rich_name}</h1>
|
||||
<p>{profile.fqn}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="profile-info">
|
||||
<div>
|
||||
<p class="profile-bio">
|
||||
music maker and coder!
|
||||
she/they/it - 18 - 🏳️⚧️🇬🇧
|
||||
robotgirl in training
|
||||
|
||||
feel free to follow req if ur cool
|
||||
|
||||
https://mae.wtf/
|
||||
also <a href="" class="mention">@mae</a>
|
||||
|
||||
"solid 7.5/10 motherliness rating" - <a href="" class="mention">@ellie</a>
|
||||
</p>
|
||||
<p class="profile-counts">
|
||||
<span>
|
||||
<b>{lang.string('profile.followers')}</b> 1225
|
||||
</span>
|
||||
<span>
|
||||
<b>{lang.string('profile.following')}</b> 1345
|
||||
</span>
|
||||
</p>
|
||||
</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">
|
||||
<Button label="{lang.string('profile.follow')}" class="profile-btn-follow">{lang.string('profile.follow')}</Button>
|
||||
<Button label="{lang.string('profile.home_instance')}">
|
||||
{#if $account && profile.fqn !== $account.fqn}
|
||||
<Button disabled filled 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>
|
||||
|
@ -49,25 +90,37 @@ also <a href="" class="mention">@mae</a>
|
|||
</Button>
|
||||
</div>
|
||||
</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-post-categories">
|
||||
<Button active>
|
||||
{lang.string('profile.posts')}
|
||||
</Button>
|
||||
<Button>
|
||||
{lang.string('profile.replies')}
|
||||
</Button>
|
||||
<Button>
|
||||
{lang.string('profile.media')}
|
||||
</Button>
|
||||
</div>
|
||||
{:catch error}
|
||||
<p class="error">{error}</p>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
header {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
height: 215px;
|
||||
background-image: url("https://f.mae.wtf/files/42bcf2ba-4256-4d22-b22f-019dfb2c0008.webp");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: var(--bg-700);
|
||||
}
|
||||
|
||||
.profile-tag {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
|
@ -112,7 +165,16 @@ also <a href="" class="mention">@mae</a>
|
|||
/* !! 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;
|
||||
}
|
||||
|
@ -124,10 +186,8 @@ also <a href="" class="mention">@mae</a>
|
|||
gap: .5rem;
|
||||
}
|
||||
|
||||
.profile-actions :global(button:first-child) {
|
||||
.profile-btn-follow {
|
||||
padding: 0 32px;
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-900);
|
||||
}
|
||||
|
||||
.profile-actions :global(button) {
|
||||
|
@ -140,4 +200,20 @@ also <a href="" class="mention">@mae</a>
|
|||
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>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
export async function load({ params }) {
|
||||
let handle = params.account;
|
||||
if (handle.startsWith('@'))
|
||||
handle = handle.substring(1);
|
||||
|
||||
return {
|
||||
server_host: params.server,
|
||||
account_handle: params.account,
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
|
||||
const post_data = await api.getPost($server.host, token, post_id);
|
||||
if (!post_data || post_data.error) {
|
||||
error = lang.string('error.post_fetch_failed', post_id);
|
||||
console.error(lang.string('logs.post_fetch_failed', post_id));
|
||||
error = lang.string('error.post_fetch_failed_id', post_id);
|
||||
console.error(lang.string('logs.post_fetch_failed_id', post_id));
|
||||
return;
|
||||
}
|
||||
let post = await parsePost(post_data, 0);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue