post interactions!
This commit is contained in:
		
							parent
							
								
									648f53f40c
								
							
						
					
					
						commit
						681ef74f95
					
				
					 11 changed files with 354 additions and 75 deletions
				
			
		|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|     "name": "spacesocial-client", |     "name": "spacesocial-client", | ||||||
|     "version": "0.2.0_rev1", |     "version": "0.2.0_rev2", | ||||||
|     "description": "social media for the galaxy-wide-web! 🌌", |     "description": "social media for the galaxy-wide-web! 🌌", | ||||||
|     "type": "module", |     "type": "module", | ||||||
|     "scripts": { |     "scripts": { | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (client.app && client.app.token) { |     if (client.app && client.app.token) { | ||||||
|  |         // this triggers the client actually getting the authenticated user's data. | ||||||
|         client.verifyCredentials().then(res => { |         client.verifyCredentials().then(res => { | ||||||
|             if (res) { |             if (res) { | ||||||
|                 console.log(`Logged in as @${client.user.username}@${client.user.host}`); |                 console.log(`Logged in as @${client.user.username}@${client.user.host}`); | ||||||
|  |  | ||||||
|  | @ -131,6 +131,83 @@ export async function getPostContext(post_id) { | ||||||
|     return data; |     return data; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function boostPost(post_id) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/reblog`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function unboostPost(post_id) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreblog`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function favouritePost(post_id) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/favourite`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function unfavouritePost(post_id) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unfavourite`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function reactPost(post_id, shortcode) { | ||||||
|  |     // for whatever reason (at least in my testing on iceshrimp)
 | ||||||
|  |     // using shortcodes for external emoji results in a fallback
 | ||||||
|  |     // to the default like emote.
 | ||||||
|  |     // identical api calls on chuckya instances do not display
 | ||||||
|  |     // this behaviour.
 | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function unreactPost(post_id, shortcode) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; | ||||||
|  |     const data = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { "Authorization": "Bearer " + client.app.token } | ||||||
|  |     }).then(res => { return res.ok ? res.json() : false }); | ||||||
|  | 
 | ||||||
|  |     if (data === false) return false; | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function parsePost(data, parent_replies, child_replies) { | export async function parsePost(data, parent_replies, child_replies) { | ||||||
|     let client = get(Client.get()); |     let client = get(Client.get()); | ||||||
|     let post = new Post(); |     let post = new Post(); | ||||||
|  | @ -166,6 +243,9 @@ export async function parsePost(data, parent_replies, child_replies) { | ||||||
|     post.warning = data.spoiler_text; |     post.warning = data.spoiler_text; | ||||||
|     post.boost_count = data.reblogs_count; |     post.boost_count = data.reblogs_count; | ||||||
|     post.reply_count = data.replies_count; |     post.reply_count = data.replies_count; | ||||||
|  |     post.favourite_count = data.favourites_count; | ||||||
|  |     post.favourited = data.favourited; | ||||||
|  |     post.boosted = data.boosted; | ||||||
|     post.mentions = data.mentions; |     post.mentions = data.mentions; | ||||||
|     post.files = data.media_attachments; |     post.files = data.media_attachments; | ||||||
|     post.url = data.url; |     post.url = data.url; | ||||||
|  | @ -185,33 +265,7 @@ export async function parsePost(data, parent_replies, child_replies) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (data.reactions && client.instance.capabilities.includes(capabilities.REACTIONS)) { |     if (data.reactions && client.instance.capabilities.includes(capabilities.REACTIONS)) { | ||||||
|         post.reactions = []; |         post.reactions = parseReactions(data.reactions); | ||||||
|         data.reactions.forEach(reaction_data => { |  | ||||||
|             if (/^[\w\-.@]+$/g.exec(reaction_data.name)) { |  | ||||||
|                 let name = reaction_data.name.split('@')[0]; |  | ||||||
|                 let host = reaction_data.name.includes('@') ? reaction_data.name.split('@')[1] : client.instance.host; |  | ||||||
|                 post.reactions.push({ |  | ||||||
|                     count: reaction_data.count, |  | ||||||
|                     emoji: parseEmoji({ |  | ||||||
|                         id: name + '@' + host, |  | ||||||
|                         name: name, |  | ||||||
|                         host: host, |  | ||||||
|                         url: reaction_data.url, |  | ||||||
|                     }), |  | ||||||
|                     me: reaction_data.me, |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 if (reaction_data.name == '❤') reaction_data.name = '❤️'; // stupid heart unicode
 |  | ||||||
|                 post.reactions.push({ |  | ||||||
|                     count: reaction_data.count, |  | ||||||
|                     emoji: { |  | ||||||
|                         html: reaction_data.name, |  | ||||||
|                         name: reaction_data.name, |  | ||||||
|                     }, |  | ||||||
|                     me: reaction_data.me, |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|     return post; |     return post; | ||||||
| } | } | ||||||
|  | @ -251,6 +305,21 @@ export async function parseUser(data) { | ||||||
|     return user; |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function parseReactions(data) { | ||||||
|  |     let client = get(Client.get()); | ||||||
|  |     let reactions = []; | ||||||
|  |     data.forEach(reaction_data => { | ||||||
|  |         let reaction = { | ||||||
|  |             count: reaction_data.count, | ||||||
|  |             name: reaction_data.name, | ||||||
|  |             me: reaction_data.me, | ||||||
|  |         }; | ||||||
|  |         if (reaction_data.url) reaction.url = reaction_data.url; | ||||||
|  |         reactions.push(reaction); | ||||||
|  |     }); | ||||||
|  |     return reactions; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function parseEmoji(data) { | export function parseEmoji(data) { | ||||||
|     let emoji = new Emoji( |     let emoji = new Emoji( | ||||||
|         data.id, |         data.id, | ||||||
|  |  | ||||||
|  | @ -100,6 +100,30 @@ export class Client { | ||||||
|         return await api.getPost(post_id, parent_replies, child_replies); |         return await api.getPost(post_id, parent_replies, child_replies); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async boostPost(post_id) { | ||||||
|  |         return await api.boostPost(post_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async unboostPost(post_id) { | ||||||
|  |         return await api.unboostPost(post_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async favouritePost(post_id) { | ||||||
|  |         return await api.favouritePost(post_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async unfavouritePost(post_id) { | ||||||
|  |         return await api.unfavouritePost(post_id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async reactPost(post_id, shortcode) { | ||||||
|  |         return await api.reactPost(post_id, shortcode); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async unreactPost(post_id, shortcode) { | ||||||
|  |         return await api.unreactPost(post_id, shortcode); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     putCacheUser(user) { |     putCacheUser(user) { | ||||||
|         this.cache.users[user.id] = user; |         this.cache.users[user.id] = user; | ||||||
|         client.set(this); |         client.set(this); | ||||||
|  | @ -148,7 +172,6 @@ export class Client { | ||||||
|         if (!json) return false; |         if (!json) return false; | ||||||
|         let saved = JSON.parse(json); |         let saved = JSON.parse(json); | ||||||
|         if (!saved.version || saved.version !== APP_VERSION) { |         if (!saved.version || saved.version !== APP_VERSION) { | ||||||
|             localStorage.setItem(save_name + '-backup', json); |  | ||||||
|             localStorage.removeItem(save_name); |             localStorage.removeItem(save_name); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -5,9 +5,7 @@ export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; | ||||||
| export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; | export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; | ||||||
| 
 | 
 | ||||||
| export default class Emoji { | export default class Emoji { | ||||||
|     id; |  | ||||||
|     name; |     name; | ||||||
|     host; |  | ||||||
|     url; |     url; | ||||||
| 
 | 
 | ||||||
|     constructor(id, name, host, url) { |     constructor(id, name, host, url) { | ||||||
|  | @ -18,7 +16,10 @@ export default class Emoji { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     get html() { |     get html() { | ||||||
|         return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`; |         if (this.url) | ||||||
|  |             return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`; | ||||||
|  |         else | ||||||
|  |             return `${this.name}`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,9 @@ export default class Post { | ||||||
|     warning; |     warning; | ||||||
|     boost_count; |     boost_count; | ||||||
|     reply_count; |     reply_count; | ||||||
|  |     favourite_count; | ||||||
|  |     favourited; | ||||||
|  |     boosted; | ||||||
|     mentions; |     mentions; | ||||||
|     reactions; |     reactions; | ||||||
|     emojis; |     emojis; | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| const sounds = { | const sounds = { | ||||||
|     "default": new Audio("sound/log.ogg"), |     "default": new Audio("/sound/log.ogg"), | ||||||
|     "post": new Audio("sound/success.ogg"), |     "post": new Audio("/sound/success.ogg"), | ||||||
|     "boost": new Audio("sound/hello.ogg"), |     "boost": new Audio("/sound/hello.ogg"), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function play_sound(name) { | export function play_sound(name) { | ||||||
|  |     if (name === false) return; | ||||||
|     if (!name) name = "default"; |     if (!name) name = "default"; | ||||||
|     const sound = sounds[name]; |     const sound = sounds[name]; | ||||||
|     if (!sound) { |     if (!sound) { | ||||||
|  |  | ||||||
|  | @ -1,21 +1,36 @@ | ||||||
| <script> | <script> | ||||||
|     import { play_sound } from '../../sound.js'; |     import { play_sound } from '../../sound.js'; | ||||||
|  |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|     export let icon = "🔧"; |  | ||||||
|     export let type = "action"; |     export let type = "action"; | ||||||
|     export let label = "Action"; |     export let label = "Action"; | ||||||
|     export let title = label; |     export let title = label; | ||||||
|     export let count = 0; |     export let count = 0; | ||||||
|  |     export let active = false; | ||||||
|  |     export let disabled = false; | ||||||
|     export let sound = "default"; |     export let sound = "default"; | ||||||
|  | 
 | ||||||
|  |     function click() { | ||||||
|  |         if (disabled) return; | ||||||
|  |         play_sound(sound); | ||||||
|  |         dispatch('click'); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button | <button | ||||||
|         type="button" |         type="button" | ||||||
|         class="{type}" |         class={[ | ||||||
|  |         type, | ||||||
|  |         active ? "active" : "", | ||||||
|  |         disabled ? "disabled" : "", | ||||||
|  |         ].join(' ')} | ||||||
|         aria-label="{label}" |         aria-label="{label}" | ||||||
|         title="{title}" |         title="{title}" | ||||||
|         on:click|stopPropagation={() => (play_sound(sound))}> |         on:click={click}> | ||||||
|         <span class="icon">{@html icon}</span> |         <span class="icon"> | ||||||
|  |             <slot/> | ||||||
|  |         </span> | ||||||
|         {#if count} |         {#if count} | ||||||
|             <span class="count">{count}</span> |             <span class="count">{count}</span> | ||||||
|         {/if} |         {/if} | ||||||
|  | @ -28,24 +43,34 @@ | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         gap: 4px; |         gap: 4px; | ||||||
|  |         font-family: inherit; | ||||||
|         font-size: 1em; |         font-size: 1em; | ||||||
|         background: none; |         background: none; | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         border: none; |         border: none; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|  |         transition: background-color .1s, color .1s; | ||||||
|  |         cursor: pointer; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button.active { |     button.active { | ||||||
|         background: var(--accent); |         background-color: color-mix(in srgb, transparent, var(--accent) 50%); | ||||||
|         color: var(--bg0); |         color: var(--bg-1000); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button:hover { |     button:not(.disabled):hover { | ||||||
|         background: #8881; |         background-color: var(--bg-600); | ||||||
|  |         color: var(--text); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button:active { |     button:not(.disabled):active { | ||||||
|         background: #0001; |         background-color: var(--bg-1000); | ||||||
|  |         color: var(--text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button.disabled { | ||||||
|  |         opacity: .5; | ||||||
|  |         cursor: initial; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .icon { |     .icon { | ||||||
|  |  | ||||||
|  | @ -8,6 +8,9 @@ | ||||||
|     import { parseOne as parseEmoji } from '../../emoji.js'; |     import { parseOne as parseEmoji } from '../../emoji.js'; | ||||||
|     import { play_sound } from '../../sound.js'; |     import { play_sound } from '../../sound.js'; | ||||||
|     import { onMount } from 'svelte'; |     import { onMount } from 'svelte'; | ||||||
|  |     import { get } from 'svelte/store'; | ||||||
|  |     import { Client } from '../../client/client.js'; | ||||||
|  |     import * as api from '../../client/api.js'; | ||||||
| 
 | 
 | ||||||
|     export let post_data; |     export let post_data; | ||||||
|     export let focused = false; |     export let focused = false; | ||||||
|  | @ -25,6 +28,55 @@ | ||||||
|         location = `/post/${post.id}`; |         location = `/post/${post.id}`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async function toggleBoost() { | ||||||
|  |         let client = get(Client.get()); | ||||||
|  |         let data; | ||||||
|  |         if (post.boosted) | ||||||
|  |             data = await client.unboostPost(post.id); | ||||||
|  |         else | ||||||
|  |             data = await client.boostPost(post.id); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to boost post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.boosted = data.boosted; | ||||||
|  |         post.boost_count = data.reblogs_count; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function toggleFavourite() { | ||||||
|  |         let client = get(Client.get()); | ||||||
|  |         let data; | ||||||
|  |         if (post.favourited) | ||||||
|  |             data = await client.unfavouritePost(post.id); | ||||||
|  |         else | ||||||
|  |             data = await client.favouritePost(post.id); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.favourited = data.favourited; | ||||||
|  |         post.favourite_count = data.favourites_count; | ||||||
|  |         if (data.reactions) post.reactions = api.parseReactions(data.reactions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function toggleReaction(reaction) { | ||||||
|  |         if (reaction.name.includes('@')) return; | ||||||
|  |         let client = get(Client.get()); | ||||||
|  | 
 | ||||||
|  |         let data; | ||||||
|  |         if (reaction.me) | ||||||
|  |             data = await client.unreactPost(post.id, reaction.name); | ||||||
|  |         else | ||||||
|  |             data = await client.reactPost(post.id, reaction.name); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.favourited = data.favourited; | ||||||
|  |         post.favourite_count = data.favourites_count; | ||||||
|  |         if (data.reactions) post.reactions = api.parseReactions(data.reactions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     let el; |     let el; | ||||||
|     onMount(() => { |     onMount(() => { | ||||||
|         if (focused) { |         if (focused) { | ||||||
|  | @ -46,18 +98,31 @@ | ||||||
|         <PostHeader post={post} /> |         <PostHeader post={post} /> | ||||||
|         <Body post={post} /> |         <Body post={post} /> | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions"> |             <div class="post-reactions" on:click|stopPropagation> | ||||||
|                 {#each post.reactions as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <ReactionButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> |                     <ReactionButton | ||||||
|  |                             type="reaction" | ||||||
|  |                             on:click={() => toggleReaction(reaction)} | ||||||
|  |                             bind:active={reaction.me} | ||||||
|  |                             bind:count={reaction.count} | ||||||
|  |                             disabled={reaction.name.includes('@')} | ||||||
|  |                             title={reaction.name} | ||||||
|  |                             label=""> | ||||||
|  |                         {#if reaction.url} | ||||||
|  |                             <img src={reaction.url} class="emoji" height="20" title={reaction.name} alt={reaction.name}> | ||||||
|  |                         {:else} | ||||||
|  |                             {reaction.name} | ||||||
|  |                         {/if} | ||||||
|  |                     </ReactionButton> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions"> |             <div class="post-actions" on:click|stopPropagation> | ||||||
|                 <ActionButton icon="🗨️" type="reply" label="Reply" bind:count={post.reply_count} sound="post" /> |                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||||
|                 <ActionButton icon="🔁" type="boost" label="Boost" bind:count={post.boost_count} sound="boost" /> |                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||||
|                 <ActionButton icon="⭐" type="favourite" label="Favourite" /> |                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||||
|                 <ActionButton icon="😃" type="react" label="React" /> |                 <ActionButton type="react" label="React" disabled>😃</ActionButton> | ||||||
|                 <ActionButton icon="🗣️" type="quote" label="Quote" /> |                 <ActionButton type="quote" label="Quote" disabled>🗣️</ActionButton> | ||||||
|                 <ActionButton icon="🛠️" type="more" label="More" /> |                 <ActionButton type="more" label="More" disabled>🛠️</ActionButton> | ||||||
|             </div> |             </div> | ||||||
|         </footer> |         </footer> | ||||||
|     </article> |     </article> | ||||||
|  | @ -97,14 +162,18 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     :global(.post-reactions) { |     :global(.post-reactions) { | ||||||
|  |         width: fit-content; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  |         gap: 4px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     :global(.post-actions) { |     :global(.post-actions) { | ||||||
|  |         width: fit-content; | ||||||
|         margin-top: 8px; |         margin-top: 8px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  |         gap: 2px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-container :global(.emoji) { |     .post-container :global(.emoji) { | ||||||
|  |  | ||||||
|  | @ -1,21 +1,35 @@ | ||||||
| <script> | <script> | ||||||
|     import { play_sound } from '../../sound.js'; |     import { play_sound } from '../../sound.js'; | ||||||
|  |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|     export let icon = "🔧"; |     export let type = "react"; | ||||||
|     export let type = "action"; |     export let label = "React"; | ||||||
|     export let label = "Action"; |  | ||||||
|     export let title = label; |     export let title = label; | ||||||
|     export let count = 0; |     export let count = 0; | ||||||
|  |     export let active = false; | ||||||
|  |     export let disabled = false; | ||||||
|     export let sound = "default"; |     export let sound = "default"; | ||||||
|  | 
 | ||||||
|  |     function click() { | ||||||
|  |         play_sound(sound); | ||||||
|  |         dispatch('click'); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button | <button | ||||||
|         type="button" |         type="button" | ||||||
|         class="{type}" |         class={[ | ||||||
|  |         type, | ||||||
|  |         active ? "active" : "", | ||||||
|  |         disabled ? "disabled" : "", | ||||||
|  |         ].join(' ')} | ||||||
|         aria-label="{label}" |         aria-label="{label}" | ||||||
|         title="{title}" |         title="{title}" | ||||||
|         on:click|stopPropagation={() => (play_sound(sound))}> |         on:click={click}> | ||||||
|         <span class="icon">{@html icon}</span> |         <span class="icon"> | ||||||
|  |             <slot/> | ||||||
|  |         </span> | ||||||
|         {#if count} |         {#if count} | ||||||
|             <span class="count">{count}</span> |             <span class="count">{count}</span> | ||||||
|         {/if} |         {/if} | ||||||
|  | @ -33,19 +47,27 @@ | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         border: none; |         border: none; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|  |         transition: background-color .1s, color .1s; | ||||||
|  |         cursor: pointer; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button.active { |     button.active { | ||||||
|         background: var(--accent); |         background-color: color-mix(in srgb, transparent, var(--accent) 50%); | ||||||
|         color: var(--bg0); |         color: var(--bg-1000); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button:hover { |     button:not(.disabled):hover { | ||||||
|         background: #8881; |         background-color: var(--bg-600); | ||||||
|  |         color: var(--text); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button:active { |     button:not(.disabled):active { | ||||||
|         background: #0001; |         background-color: var(--bg-1000); | ||||||
|  |         color: var(--text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     button.disabled { | ||||||
|  |         cursor: initial; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .icon { |     .icon { | ||||||
|  |  | ||||||
|  | @ -6,6 +6,9 @@ | ||||||
|     import Post from './Post.svelte'; |     import Post from './Post.svelte'; | ||||||
|     import { parseText as parseEmojis, parseOne as parseEmoji } from '../../emoji.js'; |     import { parseText as parseEmojis, parseOne as parseEmoji } from '../../emoji.js'; | ||||||
|     import { shorthand as short_time } from '../../time.js'; |     import { shorthand as short_time } from '../../time.js'; | ||||||
|  |     import { get } from 'svelte/store'; | ||||||
|  |     import { Client } from '../../client/client.js'; | ||||||
|  |     import * as api from '../../client/api.js'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|     let time_string = post.created_at.toLocaleString(); |     let time_string = post.created_at.toLocaleString(); | ||||||
|  | @ -13,6 +16,55 @@ | ||||||
|     function gotoPost() { |     function gotoPost() { | ||||||
|         location = `/post/${post.id}`; |         location = `/post/${post.id}`; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     async function toggleBoost() { | ||||||
|  |         let client = get(Client.get()); | ||||||
|  |         let data; | ||||||
|  |         if (post.boosted) | ||||||
|  |             data = await client.unboostPost(post.id); | ||||||
|  |         else | ||||||
|  |             data = await client.boostPost(post.id); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to boost post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.boosted = data.boosted; | ||||||
|  |         post.boost_count = data.reblogs_count; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function toggleFavourite() { | ||||||
|  |         let client = get(Client.get()); | ||||||
|  |         let data; | ||||||
|  |         if (post.favourited) | ||||||
|  |             data = await client.unfavouritePost(post.id); | ||||||
|  |         else | ||||||
|  |             data = await client.favouritePost(post.id); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.favourited = data.favourited; | ||||||
|  |         post.favourite_count = data.favourites_count; | ||||||
|  |         if (data.reactions) post.reactions = api.parseReactions(data.reactions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async function toggleReaction(reaction) { | ||||||
|  |         if (reaction.name.includes('@')) return; | ||||||
|  |         let client = get(Client.get()); | ||||||
|  | 
 | ||||||
|  |         let data; | ||||||
|  |         if (reaction.me) | ||||||
|  |             data = await client.unreactPost(post.id, reaction.name); | ||||||
|  |         else | ||||||
|  |             data = await client.reactPost(post.id, reaction.name); | ||||||
|  |         if (!data) { | ||||||
|  |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         post.favourited = data.favourited; | ||||||
|  |         post.favourite_count = data.favourites_count; | ||||||
|  |         if (data.reactions) post.reactions = api.parseReactions(data.reactions); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if post.reply} | {#if post.reply} | ||||||
|  | @ -28,18 +80,31 @@ | ||||||
|         <Body post={post} /> |         <Body post={post} /> | ||||||
| 
 | 
 | ||||||
|         <footer class="post-footer"> |         <footer class="post-footer"> | ||||||
|             <div class="post-reactions"> |             <div class="post-reactions" on:click|stopPropagation> | ||||||
|                 {#each post.reactions as reaction} |                 {#each post.reactions as reaction} | ||||||
|                     <ReactionButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> |                     <ReactionButton | ||||||
|  |                             type="reaction" | ||||||
|  |                             on:click={() => toggleReaction(reaction)} | ||||||
|  |                             bind:active={reaction.me} | ||||||
|  |                             bind:count={reaction.count} | ||||||
|  |                             disabled={reaction.name.includes('@')} | ||||||
|  |                             title={reaction.name} | ||||||
|  |                             label=""> | ||||||
|  |                         {#if reaction.url} | ||||||
|  |                             <img src={reaction.url} class="emoji" height="20" title={reaction.name} alt={reaction.name}> | ||||||
|  |                         {:else} | ||||||
|  |                             {reaction.name} | ||||||
|  |                         {/if} | ||||||
|  |                     </ReactionButton> | ||||||
|                 {/each} |                 {/each} | ||||||
|             </div> |             </div> | ||||||
|             <div class="post-actions"> |             <div class="post-actions" on:click|stopPropagation> | ||||||
|                 <ActionButton icon="🗨️" type="reply" label="Reply" bind:count={post.reply_count} sound="post" /> |                 <ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>🗨️</ActionButton> | ||||||
|                 <ActionButton icon="🔁" type="boost" label="Boost" bind:count={post.boost_count} sound="boost" /> |                 <ActionButton type="boost" label="Boost" on:click={() => toggleBoost()} bind:active={post.boosted} bind:count={post.boost_count} sound="boost">🔁</ActionButton> | ||||||
|                 <ActionButton icon="⭐" type="favourite" label="Favourite" /> |                 <ActionButton type="favourite" label="Favourite" on:click={() => toggleFavourite()} bind:active={post.favourited} bind:count={post.favourite_count}>⭐</ActionButton> | ||||||
|                 <ActionButton icon="😃" type="react" label="React" /> |                 <ActionButton type="react" label="React" disabled>😃</ActionButton> | ||||||
|                 <ActionButton icon="🗣️" type="quote" label="Quote" /> |                 <ActionButton type="quote" label="Quote" disabled>🗣️</ActionButton> | ||||||
|                 <ActionButton icon="🛠️" type="more" label="More" /> |                 <ActionButton type="more" label="More" disabled>🛠️</ActionButton> | ||||||
|             </div> |             </div> | ||||||
|         </footer> |         </footer> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue