show posts on profile page
This commit is contained in:
		
							parent
							
								
									d8efaccb30
								
							
						
					
					
						commit
						455679a525
					
				
					 7 changed files with 158 additions and 10 deletions
				
			
		|  | @ -60,6 +60,7 @@ | ||||||
| 
 | 
 | ||||||
|     "post": { |     "post": { | ||||||
|         "loading": "loading post...", |         "loading": "loading post...", | ||||||
|  |         "pinned": "📌 Pinned post", | ||||||
|         "by": "Post by %1", |         "by": "Post by %1", | ||||||
|         "time": "%1 ago", |         "time": "%1 ago", | ||||||
|         "boosted": "%1 boosted this post.", |         "boosted": "%1 boosted this post.", | ||||||
|  |  | ||||||
|  | @ -448,3 +448,51 @@ export async function lookupUser(host, token, handle) { | ||||||
| 
 | 
 | ||||||
|     return data; |     return data; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * GET /api/v1/accounts/{user_id}/statuses | ||||||
|  |  * @param {string} host - The domain of the target server. | ||||||
|  |  * @param {string} token - The application token. | ||||||
|  |  * @param {string} user_id - The ID of the user to fetch. | ||||||
|  |  * @param {string} max_id - If provided, only shows notifications before this ID. | ||||||
|  |  * @param {boolean} replies - If replies should be fetched. | ||||||
|  |  * @param {boolean} boosts - If boosts should be fetched. | ||||||
|  |  * @param {boolean} only_media - If only media should be fetched. | ||||||
|  |  */ | ||||||
|  | export async function getUserPosts(host, token, user_id, max_id, show_replies, show_boosts, only_media) { | ||||||
|  |     let url = new URL(`https://${host}/api/v1/accounts/${user_id}/statuses`); | ||||||
|  |     let query = []; | ||||||
|  |     if (!show_replies) | ||||||
|  |         query.push('exclude_replies=true'); | ||||||
|  |     if (!show_boosts) | ||||||
|  |         query.push('exclude_boosts=true'); | ||||||
|  |     if (only_media) | ||||||
|  |         query.push('only_media=true'); | ||||||
|  |     if (max_id) | ||||||
|  |         query.push(`max_id=${max_id}`); | ||||||
|  |     url.search = query.join('&'); | ||||||
|  | 
 | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||||
|  |     }).then(res => res.json()); | ||||||
|  | 
 | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * GET /api/v1/accounts/{user_id}/statuses?pinned=true | ||||||
|  |  * @param {string} host - The domain of the target server. | ||||||
|  |  * @param {string} token - The application token. | ||||||
|  |  * @param {string} user_id - The ID of the user to fetch. | ||||||
|  |  */ | ||||||
|  | export async function getUserPinnedPosts(host, token, user_id) { | ||||||
|  |     let url = `https://${host}/api/v1/accounts/${user_id}/statuses?pinned=true`; | ||||||
|  | 
 | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||||
|  |     }).then(res => res.json()); | ||||||
|  | 
 | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -81,6 +81,10 @@ img.emoji { | ||||||
|     margin: -.2em 0; |     margin: -.2em 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | hr { | ||||||
|  |     border-color: color-mix(in srgb, transparent, var(--accent) 50%); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .throb { | .throb { | ||||||
|     animation: .25s throb alternate infinite ease-in; |     animation: .25s throb alternate infinite ease-in; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,12 +4,10 @@ | ||||||
|     import { server } from '$lib/client/server.js'; |     import { server } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { playSound } from '$lib/sound.js'; |     import { playSound } from '$lib/sound.js'; | ||||||
|     import { getTimeline } from '$lib/timeline.js'; |  | ||||||
|     import { getNotifications } from '$lib/notifications.js'; |  | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|     import { notifications, unread_notif_count } from '$lib/notifications.js'; |     import { unread_notif_count } from '$lib/notifications.js'; | ||||||
|     import Lang from '$lib/lang'; |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import Logo from '$lib/../img/campfire-logo.svg'; | ||||||
|  | @ -81,7 +79,7 @@ | ||||||
|             {lang.string('navigation.timeline')} |             {lang.string('navigation.timeline')} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Notifications" |         <Button label="Notifications" | ||||||
|                 href="notifications"} |                 href="/notifications"} | ||||||
|                 active={$page.url.pathname === "/notifications"}> |                 active={$page.url.pathname === "/notifications"}> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <NotificationsIcon/> |                 <NotificationsIcon/> | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
|     const lang = Lang('en_GB'); |     const lang = Lang('en_GB'); | ||||||
| 
 | 
 | ||||||
|     let mention = (accounts) => { |     let mention = (accounts) => { | ||||||
|         let res = `<a href=${account.url}>${account.rich_name}</a>`; |         let res = `<a href="/${$server.host}/${account.fqn}">${account.rich_name}</a>`; | ||||||
|         if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1); |         if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1); | ||||||
|         return res; |         return res; | ||||||
|     }; |     }; | ||||||
|  | @ -90,7 +90,7 @@ | ||||||
|         </span> |         </span> | ||||||
|         <span class="notif-avatars"> |         <span class="notif-avatars"> | ||||||
|             {#if data.accounts.length == 1} |             {#if data.accounts.length == 1} | ||||||
|                 <a href={data.accounts[0].url} class="notif-avatar"> |                 <a href="/{$server.host}/{data.accounts[0].fqn}" class="notif-avatar"> | ||||||
|                     <img src={data.accounts[0].avatar_url} alt="" width="28" height="28" /> |                     <img src={data.accounts[0].avatar_url} alt="" width="28" height="28" /> | ||||||
|                 </a> |                 </a> | ||||||
|             {:else} |             {:else} | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|     import { onMount } from 'svelte'; |     import { onMount } from 'svelte'; | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
|     import { server } from '$lib/client/server'; |     import { server } from '$lib/client/server'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import BoostContext from './BoostContext.svelte'; |     import BoostContext from './BoostContext.svelte'; | ||||||
|     import ReplyContext from './ReplyContext.svelte'; |     import ReplyContext from './ReplyContext.svelte'; | ||||||
|  | @ -12,6 +13,9 @@ | ||||||
| 
 | 
 | ||||||
|     export let post_data; |     export let post_data; | ||||||
|     export let focused = false; |     export let focused = false; | ||||||
|  |     export let pinned = false; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
| 
 | 
 | ||||||
|     let post_context = undefined; |     let post_context = undefined; | ||||||
|     let post = post_data; |     let post = post_data; | ||||||
|  | @ -41,8 +45,6 @@ | ||||||
|             window.scrollTo(0, el.scrollHeight); |             window.scrollTo(0, el.scrollHeight); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-container"> | <div class="post-container"> | ||||||
|  | @ -51,12 +53,15 @@ | ||||||
|             <ReplyContext post={reply} /> |             <ReplyContext post={reply} /> | ||||||
|         {/await} |         {/await} | ||||||
|     {/if} |     {/if} | ||||||
|  |     {#if pinned} | ||||||
|  |         <p class="pinned">{lang.string('post.pinned')}</p> | ||||||
|  |     {/if} | ||||||
|     {#if is_boost && !post_context.text} |     {#if is_boost && !post_context.text} | ||||||
|         <BoostContext post={post_context} /> |         <BoostContext post={post_context} /> | ||||||
|     {/if} |     {/if} | ||||||
|     <article |     <article | ||||||
|             class={"post" + (focused ? " focused" : "")} |             class={"post" + (focused ? " focused" : "")} | ||||||
|             aria-label={aria_label} |             aria-label={post.account.username + '; ' + post.text + '; ' + post.created_at} | ||||||
|             bind:this={el} |             bind:this={el} | ||||||
|             on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}} |             on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}} | ||||||
|             on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost(e)}} |             on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost(e)}} | ||||||
|  | @ -83,6 +88,12 @@ | ||||||
|         border-top: none; |         border-top: none; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .pinned { | ||||||
|  |         margin: 1em 1.2em -.2em 1.2em; | ||||||
|  |         font-size: .8em; | ||||||
|  |         color: var(--accent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .post { |     .post { | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         transition: background-color .1s; |         transition: background-color .1s; | ||||||
|  |  | ||||||
|  | @ -9,26 +9,60 @@ | ||||||
|     import { server, createServer } from '$lib/client/server.js'; |     import { server, createServer } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { parseAccount } from '$lib/account.js'; |     import { parseAccount } from '$lib/account.js'; | ||||||
|  |     import { parsePost } from '$lib/post.js'; | ||||||
|     import { account } from '$lib/stores/account'; |     import { account } from '$lib/stores/account'; | ||||||
|     import { goto, afterNavigate } from '$app/navigation'; |     import { goto, afterNavigate } from '$app/navigation'; | ||||||
|     import { base } from '$app/paths'; |     import { base } from '$app/paths'; | ||||||
|  |     import Post from '../../../lib/ui/post/Post.svelte'; | ||||||
|  |     import { writable } from 'svelte/store'; | ||||||
| 
 | 
 | ||||||
|     export let data; |     export let data; | ||||||
| 
 | 
 | ||||||
|     const lang = Lang('en_GB'); |     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 profile = fetchProfile(data.account_handle); | ||||||
|     let error = false; |     let error = false; | ||||||
|     let previous_page = base; |     let previous_page = base; | ||||||
| 
 | 
 | ||||||
|  |     let post_replies = false; | ||||||
|  |     let post_boosts = true; | ||||||
|  |     let post_media = false; | ||||||
|  | 
 | ||||||
|     afterNavigate(({from}) => { |     afterNavigate(({from}) => { | ||||||
|         previous_page = from?.url.pathname || previous_page; |         previous_page = from?.url.pathname || previous_page; | ||||||
|         profile = fetchProfile(data.account_handle); |         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) { |     async function fetchProfile(handle) { | ||||||
|         let token = $app ? $app.token : null; |         let token = $app ? $app.token : null; | ||||||
| 
 | 
 | ||||||
|  |         profile_posts.set([]); | ||||||
|  |         profile_pinned_posts.set([]); | ||||||
|  | 
 | ||||||
|         if (!$server || $server.host !== data.server_host) { |         if (!$server || $server.host !== data.server_host) { | ||||||
|             server.set(await createServer(data.server_host)); |             server.set(await createServer(data.server_host)); | ||||||
|             if (!$server) { |             if (!$server) { | ||||||
|  | @ -40,7 +74,6 @@ | ||||||
|         let profile_data; |         let profile_data; | ||||||
|         try { |         try { | ||||||
|             profile_data = await api.lookupUser($server.host, token, handle); |             profile_data = await api.lookupUser($server.host, token, handle); | ||||||
|             console.debug(profile_data); |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             throw error; |             throw error; | ||||||
|         } |         } | ||||||
|  | @ -51,6 +84,45 @@ | ||||||
|         } |         } | ||||||
|         let profile = await parseAccount(profile_data, 0); |         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; |         return profile; | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
|  | @ -118,6 +190,20 @@ | ||||||
|             {lang.string('profile.media')} |             {lang.string('profile.media')} | ||||||
|         </Button> |         </Button> | ||||||
|     </div> |     </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} | {:catch error} | ||||||
|     <p class="error">{error}</p> |     <p class="error">{error}</p> | ||||||
| {/await} | {/await} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue