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> | ||||
|     <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"> | ||||
|         <div class="profile-tag-names"> | ||||
|             <h1>mae</h1> | ||||
|             <p>mae@f.mae.wtf</p> | ||||
|         </div> | ||||
| {#await profile} | ||||
|     <div class="loading throb"> | ||||
|         <span>{lang.string('profile.loading')}</span> | ||||
|     </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> | ||||
| {: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"> | ||||
|                 <h1>{@html profile.rich_name}</h1> | ||||
|                 <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"> | ||||
|             <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