wumbo changes (proper mastodon API support and oauth login!)
This commit is contained in:
		
							parent
							
								
									b7c03381f7
								
							
						
					
					
						commit
						e17b26b075
					
				
					 20 changed files with 1935 additions and 1618 deletions
				
			
		
							
								
								
									
										117
									
								
								src/App.svelte
									
										
									
									
									
								
							
							
						
						
									
										117
									
								
								src/App.svelte
									
										
									
									
									
								
							|  | @ -1,84 +1,72 @@ | |||
| <script> | ||||
|     import Feed from './Feed.svelte'; | ||||
|     import Error from './Error.svelte'; | ||||
|     import Instance from './instance.js'; | ||||
|     import { Client, server_types } from './client/client.js'; | ||||
| 
 | ||||
|     let ready = false; | ||||
|     if (localStorage.getItem("fedi_host") && localStorage.getItem("fedi_token")) { | ||||
|         Instance.setup( | ||||
|             localStorage.getItem("fedi_host"), | ||||
|             localStorage.getItem("fedi_token"), | ||||
|             true | ||||
|         ).then(() => { | ||||
|             ready = true; | ||||
|     let ready = Client.get().app && Client.get().app.token; | ||||
| 
 | ||||
|     let auth_code = new URLSearchParams(location.search).get("code"); | ||||
|     if (auth_code) { | ||||
|         let client = Client.get(); | ||||
|         client.getToken(auth_code).then(() => { | ||||
|             client.save(); | ||||
|             location = location.origin; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function log_in(event) { | ||||
|         let client = Client.get(); | ||||
|         event.preventDefault(); | ||||
|         localStorage.setItem("fedi_host", event.target.instance_host.value); | ||||
|         localStorage.setItem("fedi_token", event.target.session_token.value); | ||||
|         location = location; | ||||
|         const host = event.target.host.value; | ||||
| 
 | ||||
|         client.init(host).then(() => { | ||||
|             if (client.instance.type === server_types.INCOMPATIBLE) { | ||||
|                 console.error("Server " + client.instance.host + " is not supported - " + client.instance.version); | ||||
|                 alert("Sorry, this app is not compatible with " + client.instance.host + "!"); | ||||
|                 return; | ||||
|             } | ||||
|             console.log("Server is \"" + client.instance.type + "\" (or compatible)."); | ||||
|             client.save(); | ||||
|             let oauth_url = client.getOAuthUrl(); | ||||
|             location = oauth_url; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     function log_out() { | ||||
|         localStorage.removeItem("fedi_host"); | ||||
|         localStorage.removeItem("fedi_token"); | ||||
|         location = location; | ||||
|         Client.get().logout().then(() => { | ||||
|             ready = false; | ||||
|         }); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|     <h1>space social</h1> | ||||
|     <p>social media for the galaxy-wide-web! 🌌</p> | ||||
|     <button id="log-out" on:click={log_out}>log out</button> | ||||
|     <button id="logout" on:click={log_out}>log out</button> | ||||
| </header> | ||||
| 
 | ||||
| <main> | ||||
|     {#if ready} | ||||
|         <Feed /> | ||||
|     {:else if !Instance.get_instance().ok} | ||||
|         <Error> | ||||
|             <p>this app requires a <strong>instance host</strong> and <strong>session token</strong> to work! you may enter these below:</p> | ||||
| 
 | ||||
|             <form on:submit={data => (log_in(data))}> | ||||
|                 <label for="instance host">instance host: </label> | ||||
|                 <input type="text" id="instance_host"> | ||||
|                 <br> | ||||
|                 <label for="session token">session token: </label> | ||||
|                 <input type="password" id="session_token"> | ||||
|                 <br> | ||||
|                 <button type="submit" id="log-in">log in</button> | ||||
|     {:else} | ||||
|         <div class="pane"> | ||||
|             <form on:submit={log_in} id="login"> | ||||
|                 <h1>welcome!</h1> | ||||
|                 <p>please enter your instance domain to log in.</p> | ||||
|                 <input type="text" id="host" aria-label="instance domain"> | ||||
|                 <button type="submit" id="login">log in</button> | ||||
|             </form> | ||||
| 
 | ||||
|             <hr> | ||||
| 
 | ||||
|             <h4>how do i get these?</h4> | ||||
|             <ul> | ||||
|                 <li> | ||||
|                     <strong>instance host</strong> refers to the domain of your fediverse instance. i.e. <code>ice.arimelody.me</code>. | ||||
|                 </li> | ||||
|                 <li> | ||||
|                     a <strong>token</strong> is a unique code that grants applications permission to act on your behalf. | ||||
|                     you can find it in your browser's cookies for your instance. | ||||
|                     (instructions for <a href="https://support.mozilla.org/en-US/questions/1219653">firefox</a> | ||||
|                     and <a href="https://superuser.com/questions/1715037/how-can-i-view-the-content-of-cookies-in-chrome">chrome</a>) | ||||
|                 </li> | ||||
|             </ul> | ||||
| 
 | ||||
|             <p><small> | ||||
|                 your login credentials will not be saved to an external server. | ||||
|                 they are required for communication with the fediverse instance, and are saved entirely within your browser. | ||||
|                 a cleaner login flow will be built in the future. | ||||
|             </small></p> | ||||
|             <p><small> | ||||
|                 oh yeah i should also probably mention this is <strong><em>extremely experimental software</em></strong>; | ||||
|                 please note this is <strong><em>extremely experimental software</em></strong>; | ||||
|                 even if you use the exact same instance as me, you may encounter problems. | ||||
|                 if that's all cool with you, welcome aboard! | ||||
|             </small></p> | ||||
| 
 | ||||
|             <p>made with ❤️ by <a href="https://arimelody.me">ari melody</a>, 2024</p> | ||||
|         </Error> | ||||
|         </div> | ||||
|     {/if} | ||||
| </main> | ||||
| 
 | ||||
|  | @ -95,9 +83,12 @@ | |||
|         align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     header h1 { | ||||
|         margin: 0 16px 0 0; | ||||
|     } | ||||
| 
 | ||||
|     h1 { | ||||
|         color: var(--accent); | ||||
|         margin: 0 16px 0 0; | ||||
|     } | ||||
| 
 | ||||
|     main { | ||||
|  | @ -105,6 +96,19 @@ | |||
|         margin: 0 auto; | ||||
|     } | ||||
| 
 | ||||
|     div.pane { | ||||
|         margin-top: 16px; | ||||
|         padding: 20px 32px; | ||||
|         border: 1px solid #8884; | ||||
|         border-radius: 16px; | ||||
|         background-color: var(--bg1); | ||||
|     } | ||||
| 
 | ||||
|     form#login { | ||||
|         margin: 64px 0; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     a { | ||||
|         color: var(--accent); | ||||
|         text-decoration: none; | ||||
|  | @ -115,14 +119,14 @@ | |||
|     } | ||||
| 
 | ||||
|     input[type="text"], input[type="password"] { | ||||
|         margin-bottom: 8px; | ||||
|         margin: 8px 0; | ||||
|         padding: 4px 6px; | ||||
|         font-family: inherit; | ||||
|         border: none; | ||||
|         border-radius: 8px; | ||||
|     } | ||||
| 
 | ||||
|     button#log-in, button#log-out { | ||||
|     button#login, button#logout { | ||||
|         margin-left: auto; | ||||
|         padding: 8px 12px; | ||||
|         font-size: 1em; | ||||
|  | @ -134,17 +138,12 @@ | |||
|         transition: color .1s, background-color .1s; | ||||
|     } | ||||
| 
 | ||||
|     button#log-in.active, button#log-out.active { | ||||
|         background: var(--accent); | ||||
|         color: var(--bg0); | ||||
|     } | ||||
| 
 | ||||
|     button#log-in:hover, button#log-out:hover { | ||||
|     button#login:hover, button#logout:hover { | ||||
|         color: var(--bg0); | ||||
|         background: var(--fg0); | ||||
|     } | ||||
| 
 | ||||
|     button#log-in:active, button#log-out:active { | ||||
|     button#login:active, button#logout:active { | ||||
|         background: #0001; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,6 +21,4 @@ | |||
|         border-radius: 16px; | ||||
|         background-color: var(--bg1); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| <script> | ||||
|     import Post from './post/Post.svelte'; | ||||
|     import Error from './Error.svelte'; | ||||
|     import Instance from './instance.js'; | ||||
|     import { Client } from './client/client.js'; | ||||
| 
 | ||||
|     let client = Client.get(); | ||||
|     let posts = []; | ||||
|     let loading = false; | ||||
| 
 | ||||
|  | @ -13,15 +14,11 @@ | |||
|         loading = true; | ||||
| 
 | ||||
|         let new_posts = []; | ||||
|         if (posts.length === 0) new_posts = await Instance.get_timeline() | ||||
|         else new_posts = await Instance.get_timeline(posts[posts.length - 1].id); | ||||
|         if (posts.length === 0) new_posts = await client.getTimeline() | ||||
|         else new_posts = await client.getTimeline(posts[posts.length - 1].id); | ||||
|          | ||||
|         if (!new_posts) { | ||||
|             error = `sorry! the frontend is unable to communicate with your server. | ||||
| 
 | ||||
| this app is still in very early development, and is currently only built to support iceshrimp. | ||||
| 
 | ||||
| for more information, please consult the developer console.`; | ||||
|             console.error(`Failed to retrieve timeline posts.`); | ||||
|             loading = false; | ||||
|             return; | ||||
|         } | ||||
|  | @ -37,13 +34,53 @@ for more information, please consult the developer console.`; | |||
|             load_posts(); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     /* | ||||
|     client.getPost("9upf5wtam363h1tp", 1).then(post => { | ||||
|         posts = [...posts, post]; | ||||
|         console.log(post); | ||||
|     }); | ||||
|     */ | ||||
| </script> | ||||
| 
 | ||||
| <div id="feed"> | ||||
|     {#if error} | ||||
|         <Error msg={error.replaceAll('\n', '<br>')} /> | ||||
|     {/if} | ||||
|     {#if posts.length <= 0} | ||||
|         <div class="loading"> | ||||
|             <span>just a moment...</span> | ||||
|         </div> | ||||
|     {/if} | ||||
|     {#each posts as post} | ||||
|         <Post post={post} /> | ||||
|         <Post post_data={post} /> | ||||
|     {/each} | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         font-size: 2em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| 
 | ||||
|     .loading span { | ||||
|         animation: pulse 1s infinite; | ||||
|     } | ||||
| 
 | ||||
|     @keyframes pulse { | ||||
|         from { | ||||
|             opacity: .5; | ||||
|         } | ||||
|         50% { | ||||
|             opacity: 1; | ||||
|         } | ||||
|         to { | ||||
|             opacity: .5; | ||||
|         } | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										112
									
								
								src/api/firefish.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/api/firefish.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| import { Client } from '../client/client.js'; | ||||
| import Post from '../post/post.js'; | ||||
| import User from '../user/user.js'; | ||||
| import Emoji from '../emoji.js'; | ||||
| 
 | ||||
| import * as mastodonAPI from './mastodon.js'; | ||||
| 
 | ||||
| export async function createApp(host) { | ||||
|     return await mastodonAPI.createApp(host); | ||||
| } | ||||
| 
 | ||||
| export function getOAuthUrl() { | ||||
|     return mastodonAPI.getOAuthUrl(); | ||||
| } | ||||
| 
 | ||||
| export async function getToken(code) { | ||||
|     return await mastodonAPI.getToken(code); | ||||
| } | ||||
| 
 | ||||
| export async function revokeToken() { | ||||
|     return await mastodonAPI.revokeToken(); | ||||
| } | ||||
| 
 | ||||
| export async function getTimeline(last_post_id) { | ||||
|     return await mastodonAPI.getTimeline(last_post_id); | ||||
| } | ||||
| 
 | ||||
| export async function getPost(post_id, num_replies) { | ||||
|     return await mastodonAPI.getPost(post_id, num_replies); | ||||
| } | ||||
| 
 | ||||
| export async function parsePost(data, num_replies) { | ||||
|     let client = Client.get(); | ||||
|     let post = new Post() | ||||
|     post.id = data.id; | ||||
|     post.created_at = new Date(data.created_at); | ||||
|     post.user = await Client.get().api.parseUser(data.account); | ||||
|     post.text = data.text; | ||||
|     post.warning = data.spoiler_text; | ||||
|     post.boost_count = data.reblogs_count; | ||||
|     post.reply_count = data.replies_count; | ||||
|     post.mentions = data.mentions; | ||||
|     post.reactions = data.reactions; | ||||
|     post.files = data.media_attachments; | ||||
|     post.url = data.url; | ||||
|     post.reply = data.in_reply_to_id && num_replies > 0 ? await getPost(data.in_reply_to_id, num_replies - 1) : null; | ||||
|     post.boost = data.reblog ? await Client.get().api.parsePost(data.reblog, 1) : null; | ||||
|     post.emojis = []; | ||||
|     data.emojis.forEach(emoji_data => { | ||||
|         let name = emoji_data.shortcode.split('@')[0]; | ||||
|         post.emojis.push(Client.get().api.parseEmoji({ | ||||
|             id: name + '@' + post.user.host, | ||||
|             name: name, | ||||
|             host: post.user.host, | ||||
|             url: emoji_data.url, | ||||
|         })); | ||||
|     }); | ||||
|     post.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: Client.get().api.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; | ||||
| } | ||||
| 
 | ||||
| export async function parseUser(data) { | ||||
|     let user = new User(); | ||||
|     user.id = data.id; | ||||
|     user.nickname = data.display_name; | ||||
|     user.username = data.username; | ||||
|     user.host = data.fqn.split('@')[1]; | ||||
|     user.avatar_url = data.avatar; | ||||
|     user.emojis = []; | ||||
|     data.emojis.forEach(emoji_data => { | ||||
|         emoji_data.id = emoji_data.shortcode + '@' + user.host; | ||||
|         emoji_data.name = emoji_data.shortcode; | ||||
|         emoji_data.host = user.host; | ||||
|         user.emojis.push(Client.get().api.parseEmoji(emoji_data)); | ||||
|     }); | ||||
|     Client.get().putCacheUser(user); | ||||
|     return user; | ||||
| } | ||||
| 
 | ||||
| export function parseEmoji(data) { | ||||
|     return mastodonAPI.parseEmoji(data); | ||||
| } | ||||
| 
 | ||||
| export async function getUser(user_id) { | ||||
|     return mastodonAPI.getUser(user_id); | ||||
| } | ||||
							
								
								
									
										240
									
								
								src/api/mastodon.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/api/mastodon.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,240 @@ | |||
| import { Client } from '../client/client.js'; | ||||
| import Post from '../post/post.js'; | ||||
| import User from '../user/user.js'; | ||||
| import Emoji from '../emoji.js'; | ||||
| 
 | ||||
| export async function createApp(host) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_name", "space social"); | ||||
|     form.append("redirect_uris", `${location.origin}/callback`); | ||||
|     form.append("scopes", "read write push"); | ||||
|     form.append("website", "https://spacesocial.arimelody.me"); | ||||
| 
 | ||||
|     const res = await fetch(`https://${host}/api/v1/apps`, { | ||||
|         method: "POST", | ||||
|         body: form, | ||||
|     }) | ||||
|     .then(res => res.json()) | ||||
|     .catch(error => { | ||||
|         console.error(error); | ||||
|         return false; | ||||
|     }); | ||||
| 
 | ||||
|     if (!res || !res.client_id) return false; | ||||
| 
 | ||||
|     return { | ||||
|         id: res.client_id, | ||||
|         secret: res.client_secret, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function getOAuthUrl() { | ||||
|     let client = Client.get(); | ||||
|     return `https://${client.instance.host}/oauth/authorize` + | ||||
|         `?client_id=${client.app.id}` + | ||||
|         "&scope=read+write+push" + | ||||
|         `&redirect_uri=${location.origin}/callback` + | ||||
|         "&response_type=code"; | ||||
| } | ||||
| 
 | ||||
| export async function getToken(code) { | ||||
|     let client = Client.get(); | ||||
|     let form = new FormData(); | ||||
|     form.append("client_id", client.app.id); | ||||
|     form.append("client_secret", client.app.secret); | ||||
|     form.append("redirect_uri", `${location.origin}/callback`); | ||||
|     form.append("grant_type", "authorization_code"); | ||||
|     form.append("code", code); | ||||
|     form.append("scope", "read write push"); | ||||
| 
 | ||||
|     const res = await fetch(`https://${client.instance.host}/oauth/token`, { | ||||
|         method: "POST", | ||||
|         body: form, | ||||
|     }) | ||||
|     .then(res => res.json()) | ||||
|     .catch(error => { | ||||
|         console.error(error); | ||||
|         return false; | ||||
|     }); | ||||
| 
 | ||||
|     if (!res || !res.access_token) return false; | ||||
| 
 | ||||
|     return res.access_token; | ||||
| } | ||||
| 
 | ||||
| export async function revokeToken() { | ||||
|     let client = Client.get(); | ||||
|     let form = new FormData(); | ||||
|     form.append("client_id", client.app.id); | ||||
|     form.append("client_secret", client.app.secret); | ||||
|     form.append("token", client.app.token); | ||||
| 
 | ||||
|     const res = await fetch(`https://${client.instance.host}/oauth/revoke`, { | ||||
|         method: "POST", | ||||
|         body: form, | ||||
|     }) | ||||
|     .catch(error => { | ||||
|         console.error(error); | ||||
|         return false; | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) return false; | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| export async function getTimeline(last_post_id) { | ||||
|     let client = Client.get(); | ||||
|     let url = `https://${client.instance.host}/api/v1/timelines/home`; | ||||
|     if (last_post_id) url += "?max_id=" + last_post_id; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + client.app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     let posts = []; | ||||
|     for (let i in data) { | ||||
|         const post_data = data[i]; | ||||
|         const post = await client.api.parsePost(post_data, 1); | ||||
|         if (!post) { | ||||
|             if (post_data.id) { | ||||
|                 console.warn("Failed to parse post #" + post_data.id); | ||||
|             } else { | ||||
|                 console.warn("Failed to parse post:"); | ||||
|                 console.warn(post_data); | ||||
|             } | ||||
|             continue; | ||||
|         } | ||||
|         posts.push(post); | ||||
|     } | ||||
|     return posts; | ||||
| } | ||||
| 
 | ||||
| export async function getPost(post_id, num_replies) { | ||||
|     let client = Client.get(); | ||||
|     let url = `https://${client.instance.host}/api/v1/statuses/${post_id}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + client.app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     const post = await client.api.parsePost(data, num_replies); | ||||
|     if (!post) { | ||||
|         if (data.id) { | ||||
|             console.warn("Failed to parse post data #" + data.id); | ||||
|         } else { | ||||
|             console.warn("Failed to parse post data:"); | ||||
|             console.warn(data); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|     return post; | ||||
| } | ||||
| 
 | ||||
| export async function parsePost(data, num_replies) { | ||||
|     let client = Client.get(); | ||||
|     let post = new Post() | ||||
|     post.id = data.id; | ||||
|     post.created_at = new Date(data.created_at); | ||||
|     post.user = await Client.get().api.parseUser(data.account); | ||||
|     post.text = data.content; | ||||
|     post.warning = data.spoiler_text; | ||||
|     post.boost_count = data.reblogs_count; | ||||
|     post.reply_count = data.replies_count; | ||||
|     post.mentions = data.mentions; | ||||
|     post.reactions = data.reactions; | ||||
|     post.files = data.media_attachments; | ||||
|     post.url = data.url; | ||||
|     post.reply = data.in_reply_to_id && num_replies > 0 ? await getPost(data.in_reply_to_id, num_replies - 1) : null; | ||||
|     post.boost = data.reblog ? await Client.get().api.parsePost(data.reblog, 1) : null; | ||||
|     post.emojis = []; | ||||
|     data.emojis.forEach(emoji_data => { | ||||
|         let name = emoji_data.shortcode.split('@')[0]; | ||||
|         post.emojis.push(Client.get().api.parseEmoji({ | ||||
|             id: name + '@' + post.user.host, | ||||
|             name: name, | ||||
|             host: post.user.host, | ||||
|             url: emoji_data.url, | ||||
|         })); | ||||
|     }); | ||||
|     post.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: Client.get().api.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; | ||||
| } | ||||
| 
 | ||||
| export async function parseUser(data) { | ||||
|     let user = new User(); | ||||
|     user.id = data.id; | ||||
|     user.nickname = data.display_name; | ||||
|     user.username = data.username; | ||||
|     if (data.acct.includes('@')) | ||||
|         user.host = data.acct.split('@')[1]; | ||||
|     else | ||||
|         user.host = data.username + '@' + Client.get().instance.host; | ||||
|     user.avatar_url = data.avatar; | ||||
|     user.emojis = []; | ||||
|     data.emojis.forEach(emoji_data => { | ||||
|         emoji_data.id = emoji_data.shortcode + '@' + user.host; | ||||
|         emoji_data.name = emoji_data.shortcode; | ||||
|         emoji_data.host = user.host; | ||||
|         user.emojis.push(Client.get().api.parseEmoji(emoji_data)); | ||||
|     }); | ||||
|     Client.get().putCacheUser(user); | ||||
|     return user; | ||||
| } | ||||
| 
 | ||||
| export function parseEmoji(data) { | ||||
|     let emoji = new Emoji( | ||||
|         data.id, | ||||
|         data.name, | ||||
|         data.host, | ||||
|         data.url, | ||||
|     ); | ||||
|     Client.get().putCacheEmoji(emoji); | ||||
|     return emoji; | ||||
| } | ||||
| 
 | ||||
| export async function getUser(user_id) { | ||||
|     let client = Client.get(); | ||||
|     let url = `https://${client.instance.host}/api/v1/accounts/${user_id}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + client.app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     const user = await Client.get().api.parseUser(data); | ||||
|     if (!post) { | ||||
|         if (data.id) { | ||||
|             console.warn("Failed to parse user data #" + data.id); | ||||
|         } else { | ||||
|             console.warn("Failed to parse user data:"); | ||||
|             console.warn(data); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|     return user; | ||||
| } | ||||
							
								
								
									
										176
									
								
								src/client/client.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/client/client.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | |||
| import * as mastodonAPI from '../api/mastodon.js'; | ||||
| import * as firefishAPI from '../api/firefish.js'; | ||||
| import * as misskeyAPI from '../api/misskey.js'; | ||||
| 
 | ||||
| let client = false; | ||||
| 
 | ||||
| export const server_types = { | ||||
|     INCOMPATIBLE: "incompatible", | ||||
|     MASTODON: "mastodon", | ||||
|     FIREFISH: "firefish", | ||||
| }; | ||||
| 
 | ||||
| const save_name = "spacesocial"; | ||||
| 
 | ||||
| const versions_types = [ | ||||
|     { version: "mastodon", type: server_types.MASTODON }, | ||||
|     { version: "glitchsoc", type: server_types.MASTODON }, | ||||
|     { version: "chuckya", type: server_types.MASTODON }, | ||||
|     { version: "iceshrimp", type: server_types.FIREFISH }, | ||||
|     { version: "sharkey", type: server_types.FIREFISH }, | ||||
| ]; | ||||
| 
 | ||||
| export class Client { | ||||
|     #api; | ||||
|     instance; | ||||
|     app; | ||||
|     #cache; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.instance = null; | ||||
|         this.app = null; | ||||
|         this.cache = { | ||||
|             users: {}, | ||||
|             emojis: {}, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     static get() { | ||||
|         if (client) return client; | ||||
|         client = new Client(); | ||||
|         window.peekie = client; | ||||
|         client.load(); | ||||
|         if (client.instance) client.#configureAPI(); | ||||
|         return client; | ||||
|     } | ||||
| 
 | ||||
|     async init(host) { | ||||
|         if (host.startsWith("https://")) host = host.substring(8); | ||||
|         const url = "https://" + host + "/api/v1/instance"; | ||||
|         const data = await fetch(url).then(res => res.json()); | ||||
|         this.instance = { | ||||
|             host: host, | ||||
|             version: data.version, | ||||
|             type: server_types.INCOMPATIBLE, | ||||
|         }; | ||||
| 
 | ||||
|         for (let index in versions_types) { | ||||
|             const pair = versions_types[index]; | ||||
|             if (data.version.toLowerCase().includes(pair.version)) { | ||||
|                 this.instance.type = pair.type; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.#configureAPI(); | ||||
|         this.app = await this.api.createApp(host); | ||||
| 
 | ||||
|         if (!this.app || !this.instance) { | ||||
|             console.error("Failed to create app. Check the network logs for details."); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return this.auth; | ||||
|     } | ||||
| 
 | ||||
|     #configureAPI() { | ||||
|         switch (this.instance.type) { | ||||
|             case server_types.MASTODON: | ||||
|                 this.api = mastodonAPI; | ||||
|                 break; | ||||
|             case server_types.FIREFISH: | ||||
|                 this.api = firefishAPI; | ||||
|                 break; | ||||
|             /* not opening that can of worms for a while | ||||
|             case server_types.MISSKEY: | ||||
|                 this.api = misskeyAPI; | ||||
|                 break; */ | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     getOAuthUrl() { | ||||
|         return this.api.getOAuthUrl(this.app.secret); | ||||
|     } | ||||
| 
 | ||||
|     async getToken(code) { | ||||
|         const token = await this.api.getToken(code); | ||||
|         if (!token) { | ||||
|             console.error("Failed to obtain access token"); | ||||
|             return false; | ||||
|         } | ||||
|         this.app.token = token; | ||||
|     } | ||||
| 
 | ||||
|     async revokeToken() { | ||||
|         return await this.api.revokeToken(); | ||||
|     } | ||||
| 
 | ||||
|     async getTimeline(last_post_id) { | ||||
|         return await this.api.getTimeline(last_post_id); | ||||
|     } | ||||
| 
 | ||||
|     async getPost(post_id, num_replies) { | ||||
|         return await this.api.getPost(post_id, num_replies); | ||||
|     } | ||||
| 
 | ||||
|     putCacheUser(user) { | ||||
|         this.cache.users[user.id] = user; | ||||
|     } | ||||
| 
 | ||||
|     async getUser(user_id) { | ||||
|         let user = this.cache.users[user_id]; | ||||
|         if (user) return user; | ||||
| 
 | ||||
|         user = await this.api.getUser(user_id); | ||||
|         if (user) return user; | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     async getUserByMention(mention) { | ||||
|         let users = Object.values(this.cache.users); | ||||
|         for (let i in users) { | ||||
|             const user = users[i]; | ||||
|             if (user.mention == mention) return user; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     putCacheEmoji(emoji) { | ||||
|         this.cache.emojis[emoji.id] = emoji; | ||||
|     } | ||||
| 
 | ||||
|     getEmoji(emoji_id) { | ||||
|         let emoji = this.cache.emojis[emoji_id]; | ||||
|         if (!emoji) return false; | ||||
|         return emoji; | ||||
|     } | ||||
| 
 | ||||
|     save() { | ||||
|         localStorage.setItem(save_name, JSON.stringify({ | ||||
|             instance: this.instance, | ||||
|             app: this.app, | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     load() { | ||||
|         let json = localStorage.getItem(save_name); | ||||
|         if (!json) return false; | ||||
|         let saved = JSON.parse(json); | ||||
|         this.instance = saved.instance; | ||||
|         this.app = saved.app; | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     async logout() { | ||||
|         if (!this.instance || !this.app || !this.api) return; | ||||
|         if (!await this.revokeToken()) { | ||||
|             console.warn("Failed to log out correctly; ditching the old tokens anyways."); | ||||
|         } | ||||
|         localStorage.removeItem(save_name); | ||||
|         client = new Client(); | ||||
|         console.log("Logged out successfully."); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										99
									
								
								src/emoji.js
									
										
									
									
									
								
							
							
						
						
									
										99
									
								
								src/emoji.js
									
										
									
									
									
								
							|  | @ -1,93 +1,50 @@ | |||
| import Instance from './instance.js'; | ||||
| import { Client } from './client/client.js'; | ||||
| 
 | ||||
| const EMOJI_REGEX = /:[a-z0-9_\-]+:/g; | ||||
| 
 | ||||
| let emoji_cache = []; | ||||
| export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; | ||||
| export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; | ||||
| 
 | ||||
| export default class Emoji { | ||||
|     id; | ||||
|     name; | ||||
|     host; | ||||
|     url; | ||||
|     width; | ||||
|     height; | ||||
| 
 | ||||
|     static parse(data, host) { | ||||
|         const instance = Instance.get_instance(); | ||||
|         let emoji = null; | ||||
|         switch (instance.type) { | ||||
|             case Instance.types.ICESHRIMP: | ||||
|                 emoji = Emoji.#parse_iceshrimp(data); | ||||
|                 break; | ||||
|             case Instance.types.MASTODON: | ||||
|                 emoji = Emoji.#parse_mastodon(data); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         if (emoji !== null) emoji_cache.push(emoji); | ||||
|         return emoji; | ||||
|     constructor(id, name, host, url) { | ||||
|         this.id = id; | ||||
|         this.name = name; | ||||
|         this.host = host; | ||||
|         this.url = url; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_iceshrimp(data, host) { | ||||
|         let emoji = new Emoji() | ||||
|         emoji.name = data.name.substring(1, data.name.search('@')); | ||||
|         emoji.host = host; | ||||
|         emoji.url = data.url; | ||||
|         emoji.width = data.width; | ||||
|         emoji.height = data.height; | ||||
|         return emoji; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_mastodon(data, host) { | ||||
|         let emoji = new Emoji() | ||||
|         emoji.name = data.shortcode; | ||||
|         emoji.host = host; | ||||
|         emoji.url = data.url; | ||||
|         emoji.width = data.width; | ||||
|         emoji.height = data.height; | ||||
|         return emoji; | ||||
|     } | ||||
| 
 | ||||
|     get id() { | ||||
|         return this.name + '@' + this.host; | ||||
|     get html() { | ||||
|         return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function parse_text(text, ignore_instance) { | ||||
| export function parseText(text, host) { | ||||
|     if (!text) return text; | ||||
|      | ||||
|     let index = text.search(EMOJI_REGEX); | ||||
|     let index = text.search(EMOJI_NAME_REGEX); | ||||
|     if (index === -1) return text; | ||||
|     index++; | ||||
| 
 | ||||
|     // find the emoji name
 | ||||
|     let length = 0; | ||||
|     while (index + length < text.length && text[index + length] !== ':') length++; | ||||
|     let emoji_name = ':' + text.substring(index, index + length) + ':'; | ||||
|     let length = text.substring(index + 1).search(':'); | ||||
|     if (length <= 0) return text; | ||||
|     let emoji_name = text.substring(index + 1, index + length + 1); | ||||
|     let emoji = Client.get().getEmoji(emoji_name + '@' + host); | ||||
| 
 | ||||
|     // does this emoji exist?
 | ||||
|     let emoji; | ||||
|     for (let cached in emoji_cache) { | ||||
|         if (cached.id === emoji_name) { | ||||
|             emoji = cached; | ||||
|             break; | ||||
|         } | ||||
|     if (emoji) { | ||||
|         return text.substring(0, index) + emoji.html + | ||||
|             parseText(text.substring(index + length + 2), host); | ||||
|     } | ||||
| 
 | ||||
|     if (!emoji) return text.substring(0, index + length) + parse_text(text.substring(index + length)); | ||||
| 
 | ||||
|     // replace emoji code with <img>
 | ||||
|     const img = `<img src="${emoji.url}" class="emoji" width="26" height="26" title=":${emoji_name}:" alt="${emoji_name}">`; | ||||
|     return text.substring(0, index - 1) + img + | ||||
|         parse(text.substring(index + length + 1), emojis, ignore_instance); | ||||
|     return text.substring(0, index + length + 1) + | ||||
|         parseText(text.substring(index + length + 1), host); | ||||
| } | ||||
| 
 | ||||
| export function parse_one(reaction, emojis) { | ||||
|     if (reaction == '❤') return '❤️'; // stupid heart unicode
 | ||||
|     if (!reaction.startsWith(':') || !reaction.endsWith(':')) return reaction; | ||||
|     for (let i = 0; i < emojis.length; i++) { | ||||
|         if (emojis[i].name == reaction.substring(1, reaction.length - 1)) | ||||
|             return `<img src="${emojis[i].url}" class="emoji" width="26" height="26" title="${reaction}" alt="${emojis[i].name}">`; | ||||
|     } | ||||
|     return reaction; | ||||
| export function parseOne(emoji_id) { | ||||
|     if (emoji_id == '❤') return '❤️'; // stupid heart unicode
 | ||||
|     if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id; | ||||
|     let cached_emoji = Client.get().getEmoji(emoji_id); | ||||
|     if (!cached_emoji) return emoji_id; | ||||
|     return cached_emoji.html; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										107
									
								
								src/instance.js
									
										
									
									
									
								
							
							
						
						
									
										107
									
								
								src/instance.js
									
										
									
									
									
								
							|  | @ -1,107 +0,0 @@ | |||
| import Post from './post/post.js'; | ||||
| 
 | ||||
| let instance; | ||||
| 
 | ||||
| const ERR_UNSUPPORTED = "Unsupported server"; | ||||
| const ERR_SERVER_RESPONSE = "Unsupported response from the server"; | ||||
| 
 | ||||
| export default class Instance { | ||||
|     #host; | ||||
|     #token; | ||||
|     #type; | ||||
|     #secure; | ||||
| 
 | ||||
|     static types = { | ||||
|         ICESHRIMP: "iceshrimp", | ||||
|         MASTODON: "mastodon", | ||||
|         MISSKEY: "misskey", | ||||
|         AKKOMA: "akkoma", | ||||
|     }; | ||||
| 
 | ||||
|     static get_instance() { | ||||
|         if (!instance) instance = new Instance(); | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     static async setup(host, token, secure) { | ||||
|         instance = Instance.get_instance(); | ||||
|         instance.host = host; | ||||
|         instance.token = token; | ||||
|         instance.secure = secure; | ||||
|         await instance.#guess_type(); | ||||
|     } | ||||
| 
 | ||||
|     async #guess_type() { | ||||
|         const url = instance.#proto + instance.host + "/api/v1/instance"; | ||||
|         console.log("Snooping for instance information at " + url + "..."); | ||||
|         const res = await fetch(url); | ||||
|         const data = await res.json(); | ||||
|         const version = data.version.toLowerCase(); | ||||
| 
 | ||||
|         instance.type = Instance.types.MASTODON; | ||||
|         if (version.search("iceshrimp") !== -1) instance.type = Instance.types.ICESHRIMP; | ||||
|         if (version.search("misskey") !== -1) instance.type = Instance.types.MISSKEY; | ||||
|         if (version.search("akkoma") !== -1) instance.type = Instance.types.AKKOMA; | ||||
| 
 | ||||
|         console.log("Assumed server type to be \"" + instance.type + "\"."); | ||||
|     } | ||||
| 
 | ||||
|     static async get_timeline(last_post_id) { | ||||
|         let data = null; | ||||
|         switch (instance.type) { | ||||
|             case Instance.types.ICESHRIMP: | ||||
|                 data = await instance.#get_timeline_iceshrimp(last_post_id); | ||||
|                 break; | ||||
|             case Instance.types.MASTODON: | ||||
|                 data = await instance.#get_timeline_mastodon(last_post_id); | ||||
|                 break; | ||||
|             default: | ||||
|                 console.error(ERR_UNSUPPORTED); | ||||
|                 return null; | ||||
|         } | ||||
|         if (data.constructor != Array) { | ||||
|             console.error(ERR_SERVER_RESPONSE); | ||||
|             return null; | ||||
|         } | ||||
|         let posts = []; | ||||
|         data.forEach(post_data => { | ||||
|             const post = Post.parse(post_data); | ||||
|             if (!post) return; | ||||
|             posts = [...posts, post]; | ||||
|         }); | ||||
|         return posts; | ||||
|     } | ||||
| 
 | ||||
|     async #get_timeline_iceshrimp(last_post_id) { | ||||
|         let body = Object; | ||||
|         if (last_post_id) body.untilId = last_post_id; | ||||
|         const res = await fetch(this.#proto + this.host + "/api/notes/timeline", { | ||||
|             method: 'POST', | ||||
|             headers: { "Authorization": "Bearer " + this.token }, | ||||
|             body: JSON.stringify(body) | ||||
|         }); | ||||
|         return await res.json(); | ||||
|     } | ||||
| 
 | ||||
|     async #get_timeline_mastodon(last_post_id) { | ||||
|         let url = this.#proto + this.host + "/api/v1/timelines/home"; | ||||
|         if (last_post_id) url += "?max_id=" + last_post_id; | ||||
|         const res = await fetch(url, { | ||||
|             method: 'GET', | ||||
|             headers: { "Authorization": "Bearer " + this.token } | ||||
|         }); | ||||
|         return await res.json(); | ||||
|     } | ||||
| 
 | ||||
|     get #proto() { | ||||
|         if (this.secure) return "https://"; | ||||
|         return "http://"; | ||||
|     } | ||||
| 
 | ||||
|     static get ok() { | ||||
|         if (!instance) return false; | ||||
|         if (!instance.host) return false; | ||||
|         if (!instance.token) return false; | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,5 @@ | |||
| import './app.css'; | ||||
| import App from './App.svelte'; | ||||
| import Instance from './instance.js'; | ||||
| 
 | ||||
| const app = new App({ | ||||
|     target: document.getElementById('app') | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| <script> | ||||
|     export let post; | ||||
| 
 | ||||
|     let rich_text; | ||||
|     post.rich_text().then(res => {rich_text = res}); | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-body"> | ||||
|  | @ -7,7 +10,7 @@ | |||
|         <p class="post-warning"><strong>{post.warning}</strong></p> | ||||
|     {/if} | ||||
|     {#if post.text} | ||||
|         <span class="post-text">{@html post.rich_text}</span> | ||||
|         <span class="post-text">{@html rich_text}</span> | ||||
|     {/if} | ||||
|     <div class="post-media-container" data-count={post.files.length}> | ||||
|         {#each post.files as file} | ||||
|  | @ -40,6 +43,12 @@ | |||
|         word-wrap: break-word; | ||||
|     } | ||||
| 
 | ||||
|     .post-text :global(.emoji) { | ||||
|         position: relative; | ||||
|         top: 6px; | ||||
|         height: 24px!important; | ||||
|     } | ||||
| 
 | ||||
|     .post-text :global(code) { | ||||
|         font-size: 1.2em; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
|     import { parse_text as parse_emojis } from '../emoji.js'; | ||||
|     import { parseText as parseEmojis } from '../emoji.js'; | ||||
|     import { shorthand as short_time } from '../time.js'; | ||||
| 
 | ||||
|     export let post; | ||||
|  | @ -10,7 +10,7 @@ | |||
| <div class="post-context"> | ||||
|     <span class="post-context-icon">🔁</span> | ||||
|     <span class="post-context-action"> | ||||
|         <a href="/{post.user.mention}">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> boosted this post. | ||||
|         <a href="/{post.user.mention}">{@html parseEmojis(post.user.name)}</a> boosted this post. | ||||
|     </span> | ||||
|     <span class="post-context-time"> | ||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
|         aria-label="{label}" | ||||
|         title="{title}" | ||||
|         on:click={() => (play_sound(sound))}> | ||||
|         <span>{@html icon}</span> | ||||
|         <span class="icon">{@html icon}</span> | ||||
|         {#if count} | ||||
|             <span class="count">{count}</span> | ||||
|         {/if} | ||||
|  | @ -23,7 +23,11 @@ | |||
| 
 | ||||
| <style> | ||||
|     button { | ||||
|         height: 32px; | ||||
|         padding: 6px 8px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 4px; | ||||
|         font-size: 1em; | ||||
|         background: none; | ||||
|         color: inherit; | ||||
|  | @ -44,6 +48,14 @@ | |||
|         background: #0001; | ||||
|     } | ||||
| 
 | ||||
|     .icon { | ||||
|         width: 20px; | ||||
|         height: 20px; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     .count { | ||||
|         opacity: .5; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
|     import { parse_text as parse_emojis } from '../emoji.js'; | ||||
|     import { parseText as parseEmojis } from '../emoji.js'; | ||||
|     import { shorthand as short_time } from '../time.js'; | ||||
| 
 | ||||
|     export let post; | ||||
|  | @ -13,7 +13,7 @@ | |||
|     </a> | ||||
|     <header class="post-header"> | ||||
|         <div class="post-user-info"> | ||||
|             <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> | ||||
|             <a href="/{post.user.mention}" class="name">{@html post.user.rich_name}</a> | ||||
|             <span class="username">{post.user.mention}</span> | ||||
|         </div> | ||||
|         <div class="post-info"> | ||||
|  |  | |||
|  | @ -4,42 +4,42 @@ | |||
|     import Header from './Header.svelte'; | ||||
|     import Body from './Body.svelte'; | ||||
|     import FooterButton from './FooterButton.svelte'; | ||||
|     import { parse_one as parse_reaction } from '../emoji.js'; | ||||
|     import { parseOne as parseEmoji } from '../emoji.js'; | ||||
|     import { play_sound } from '../sound.js'; | ||||
| 
 | ||||
|     export let post; | ||||
|     export let post_data; | ||||
| 
 | ||||
|     let post_context = undefined; | ||||
|     let _post = post; | ||||
|     let post = post_data; | ||||
|     let is_boost = false; | ||||
|     if (_post.boost) { | ||||
|     if (post_data.boost) { | ||||
|         is_boost = true; | ||||
|         post_context = _post; | ||||
|         _post = _post.boost; | ||||
|         post_context = post_data; | ||||
|         post = post_data.boost; | ||||
|     } | ||||
| 
 | ||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-container" aria-label={aria_label}> | ||||
|     {#if _post.reply} | ||||
|         <ReplyContext post={_post.reply} /> | ||||
|     {#if post.reply} | ||||
|         <ReplyContext post={post.reply} /> | ||||
|     {/if} | ||||
|     {#if is_boost && !post_context.text} | ||||
|         <BoostContext post={post_context} /> | ||||
|     {/if} | ||||
|     <article class="post"> | ||||
|         <Header post={_post} /> | ||||
|         <Body post={_post} /> | ||||
|         <Header post={post} /> | ||||
|         <Body post={post} /> | ||||
|         <footer class="post-footer"> | ||||
|             <div class="post-reactions"> | ||||
|                 {#each Object.keys(_post.reactions) as reaction} | ||||
|                     <FooterButton icon={parse_reaction(reaction, _post.emojis)} type="reaction" bind:count={_post.reactions[reaction]} title={reaction} label="" /> | ||||
|                 {#each post.reactions as reaction} | ||||
|                     <FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> | ||||
|                 {/each} | ||||
|             </div> | ||||
|             <div class="post-actions"> | ||||
|                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={_post.reply_count} sound="post" /> | ||||
|                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={_post.boost_count} sound="boost" /> | ||||
|                 <FooterButton icon="🗨️" type="reply" label="Reply" bind:count={post.reply_count} sound="post" /> | ||||
|                 <FooterButton icon="🔁" type="boost" label="Boost" bind:count={post.boost_count} sound="boost" /> | ||||
|                 <FooterButton icon="⭐" type="favourite" label="Favourite" /> | ||||
|                 <FooterButton icon="😃" type="react" label="React" /> | ||||
|                 <FooterButton icon="🗣️" type="quote" label="Quote" /> | ||||
|  | @ -63,17 +63,19 @@ | |||
|         background-color: var(--bg2); | ||||
|     } | ||||
| 
 | ||||
|     .post-reactions { | ||||
|         margin-top: 8px; | ||||
|     :global(.post-reactions) { | ||||
|         margin-top: 16px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     .post-actions { | ||||
|     :global(.post-actions) { | ||||
|         margin-top: 8px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     .post-container :global(.emoji) { | ||||
|         position: relative; | ||||
|         top: 6px; | ||||
|         height: 26px; | ||||
|         height: 20px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
|     import Body from './Body.svelte'; | ||||
|     import FooterButton from './FooterButton.svelte'; | ||||
|     import Post from './Post.svelte'; | ||||
|     import { parse_text as parse_emojis, parse_one as parse_reaction } from '../emoji.js'; | ||||
|     import { parseText as parseEmojis, parseOne as parseEmoji } from '../emoji.js'; | ||||
|     import { shorthand as short_time } from '../time.js'; | ||||
| 
 | ||||
|     export let post; | ||||
|  | @ -24,7 +24,7 @@ | |||
|         <div class="post-header-container"> | ||||
|             <header class="post-header"> | ||||
|                 <div class="post-user-info"> | ||||
|                     <a href="/{post.user.mention}" class="name">{@html parse_emojis(post.user.name, post.user.emojis, true)}</a> | ||||
|                     <a href="/{post.user.mention}" class="name">{@html post.user.rich_name}</a> | ||||
|                     <span class="username">{post.user.mention}</span> | ||||
|                 </div> | ||||
|                 <div class="post-info"> | ||||
|  | @ -39,8 +39,8 @@ | |||
| 
 | ||||
|         <footer class="post-footer"> | ||||
|             <div class="post-reactions"> | ||||
|                 {#each Object.keys(post.reactions) as reaction} | ||||
|                     <FooterButton icon={parse_reaction(reaction, post.emojis)} type="reaction" bind:count={post.reactions[reaction]} title={reaction} label="" /> | ||||
|                 {#each post.reactions as reaction} | ||||
|                     <FooterButton icon={reaction.emoji.html} type="reaction" bind:count={reaction.count} title={reaction.emoji.id} label="" /> | ||||
|                 {/each} | ||||
|             </div> | ||||
|             <div class="post-actions"> | ||||
|  |  | |||
							
								
								
									
										126
									
								
								src/post/post.js
									
										
									
									
									
								
							
							
						
						
									
										126
									
								
								src/post/post.js
									
										
									
									
									
								
							|  | @ -1,9 +1,5 @@ | |||
| import Instance from '../instance.js'; | ||||
| import User from '../user/user.js'; | ||||
| 
 | ||||
| import { parse_one as parse_emoji } from '../emoji.js'; | ||||
| 
 | ||||
| let post_cache = Object; | ||||
| import { Client, server_types } from '../client/client.js'; | ||||
| import { parseOne as parseEmoji, EMOJI_REGEX } from '../emoji.js'; | ||||
| 
 | ||||
| export default class Post { | ||||
|     id; | ||||
|  | @ -21,70 +17,7 @@ export default class Post { | |||
|     reply; | ||||
|     boost; | ||||
| 
 | ||||
|     static resolve_id(id) { | ||||
|         return post_cache[id] || null; | ||||
|     } | ||||
| 
 | ||||
|     static parse(data) { | ||||
|         const instance = Instance.get_instance(); | ||||
|         let post = null; | ||||
|         switch (instance.type) { | ||||
|             case Instance.types.ICESHRIMP: | ||||
|                 post = Post.#parse_iceshrimp(data); | ||||
|                 break; | ||||
|             case Instance.types.MASTODON: | ||||
|                 post = Post.#parse_mastodon(data); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         if (!post) { | ||||
|             console.error("Error while parsing post data"); | ||||
|             return null; | ||||
|         } | ||||
|         post_cache[post.id] = post; | ||||
|         return post; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_iceshrimp(data) { | ||||
|         let post = new Post() | ||||
|         post.id = data.id; | ||||
|         post.created_at = new Date(data.createdAt); | ||||
|         post.user = User.parse(data.user); | ||||
|         post.text = data.text; | ||||
|         post.warning = data.cw; | ||||
|         post.boost_count = data.renoteCount; | ||||
|         post.reply_count = data.repliesCount; | ||||
|         post.mentions = data.mentions; | ||||
|         post.reactions = data.reactions; | ||||
|         post.emojis = data.emojis; | ||||
|         post.files = data.files; | ||||
|         post.url = data.url; | ||||
|         post.boost = data.renote ? Post.parse(data.renote) : null; | ||||
|         post.reply = data.reply ? Post.parse(data.reply) : null; | ||||
|         return post; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_mastodon(data) { | ||||
|         let post = new Post() | ||||
|         post.id = data.id; | ||||
|         post.created_at = new Date(data.created_at); | ||||
|         post.user = User.parse(data.account); | ||||
|         post.text = data.content; | ||||
|         post.warning = data.spoiler_text; | ||||
|         post.boost_count = data.reblogs_count; | ||||
|         post.reply_count = data.replies_count; | ||||
|         post.mentions = data.mentions; | ||||
|         post.reactions = data.reactions; | ||||
|         post.emojis = data.emojis; | ||||
|         post.files = data.media_attachments; | ||||
|         post.url = data.url; | ||||
|         post.boost = data.reblog ? Post.parse(data.reblog) : null; | ||||
|         post.reply = data.in_reply_to_id ? Post.resolve_id(data.in_reply_to_id) : null; | ||||
|         return post; | ||||
|     } | ||||
| 
 | ||||
|     get rich_text() { | ||||
|     async rich_text() { | ||||
|         let text = this.text; | ||||
|         if (!text) return text; | ||||
| 
 | ||||
|  | @ -112,7 +45,8 @@ export default class Post { | |||
|             } | ||||
| 
 | ||||
|             // handle mentions
 | ||||
|             if (allow_new && sample.match(/@[a-z0-9-_.]+@[a-z0-9-_.]+/g)) { | ||||
|             // TODO: setup a better system for handling different server capabilities
 | ||||
|             if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^@[\w\-.]+@[\w\-.]+/g)) { | ||||
|                 // find end of the mention
 | ||||
|                 let length = 1; | ||||
|                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; | ||||
|  | @ -122,11 +56,11 @@ export default class Post { | |||
|                 let mention = text.substring(index, index + length); | ||||
| 
 | ||||
|                 // attempt to resolve mention to a user
 | ||||
|                 let user = User.resolve_mention(mention); | ||||
|                 let user = await Client.get().getUserByMention(mention); | ||||
|                 if (user) { | ||||
|                     const out = `<a href="/${user.mention}" class="mention">` + | ||||
|                         `<img src="${user.avatar_url}" class="mention-avatar" width="20" height="20">` + | ||||
|                         "@" + user.name + "</a>"; | ||||
|                         '@' + user.username + '@' + user.host + "</a>"; | ||||
|                     if (current) current.text += out; | ||||
|                     else response += out; | ||||
|                 } else { | ||||
|  | @ -136,33 +70,39 @@ export default class Post { | |||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (Instance.get_instance().type !== Instance.types.MASTODON) { | ||||
|                 // handle links
 | ||||
|                 if (allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) { | ||||
|                     // get length of link
 | ||||
|                     let length = text.substring(index).search(/\s|$/g); | ||||
|                     let url = text.substring(index, index + length); | ||||
|                     let out = `<a href="${url}">${url}</a>`; | ||||
|                     if (current) current.text += out; | ||||
|                     else response += out; | ||||
|                     index += length; | ||||
|                     continue; | ||||
|                 } | ||||
|             // handle links
 | ||||
|             if (Client.get().instance.type !== server_types.MASTODON && allow_new && sample.match(/^[a-z]{3,6}:\/\/[^\s]+/g)) { | ||||
|                 // get length of link
 | ||||
|                 let length = text.substring(index).search(/\s|$/g); | ||||
|                 let url = text.substring(index, index + length); | ||||
|                 let out = `<a href="${url}">${url}</a>`; | ||||
|                 if (current) current.text += out; | ||||
|                 else response += out; | ||||
|                 index += length; | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // handle emojis
 | ||||
|             if (allow_new && sample.startsWith(':')) { | ||||
|                 // lookahead to next invalid emoji character
 | ||||
|                 let look = sample.substring(1).search(/[^a-zA-Z0-9-_.]/g) + 1; | ||||
|                 // if it's ':', we can parse it
 | ||||
|                 if (look !== 0 && sample[look] === ':') { | ||||
|                     let emoji_code = sample.substring(0, look + 1); | ||||
|                     let out = parse_emoji(emoji_code, this.emojis); | ||||
|             if (allow_new && sample.match(/^:[\w\-.]{0,32}:/g)) { | ||||
|                 // find the emoji name
 | ||||
|                 let length = text.substring(index + 1).search(':'); | ||||
|                 if (length <= 0) return text; | ||||
|                 let emoji_name = text.substring(index + 1, index + length + 1); | ||||
|                 let emoji = Client.get().getEmoji(emoji_name + '@' + this.user.host); | ||||
| 
 | ||||
|                 index += length + 2; | ||||
| 
 | ||||
|                 if (!emoji) { | ||||
|                     let out = ':' + emoji_name + ':'; | ||||
|                     if (current) current.text += out; | ||||
|                     else response += out; | ||||
|                     index += emoji_code.length; | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 let out = emoji.html; | ||||
|                 if (current) current.text += out; | ||||
|                 else response += out; | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // handle markdown
 | ||||
|  |  | |||
|  | @ -1,7 +1,4 @@ | |||
| import Instance from '../instance.js'; | ||||
| import Emoji from '../emoji.js'; | ||||
| 
 | ||||
| let user_cache = Object; | ||||
| import { parseText as parseEmojis } from '../emoji.js'; | ||||
| 
 | ||||
| export default class User { | ||||
|     id; | ||||
|  | @ -11,66 +8,6 @@ export default class User { | |||
|     avatar_url; | ||||
|     emojis; | ||||
| 
 | ||||
|     static resolve_id(id) { | ||||
|         return user_cache[id]; | ||||
|     } | ||||
| 
 | ||||
|     static resolve_mention(mention) { | ||||
|         for (let i = 0; i < Object.keys(user_cache).length; i++) { | ||||
|             let user = user_cache[Object.keys(user_cache)[i]]; | ||||
|             if (user.mention === mention) return user; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static parse(data) { | ||||
|         const instance = Instance.get_instance(); | ||||
|         let user = null; | ||||
|         switch (instance.type) { | ||||
|             case Instance.types.ICESHRIMP: | ||||
|                 user = User.#parse_iceshrimp(data); | ||||
|                 break; | ||||
|             case Instance.types.MASTODON: | ||||
|                 user = User.#parse_mastodon(data); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         if (!user) { | ||||
|             console.error("Error while parsing user data"); | ||||
|             return null; | ||||
|         } | ||||
|         user_cache[user.id] = user; | ||||
|         return user; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_iceshrimp(data) { | ||||
|         let user = new User(); | ||||
|         user.id = data.id; | ||||
|         user.nickname = data.name; | ||||
|         user.username = data.username; | ||||
|         user.host = data.host || Instance.get_instance().host; | ||||
|         user.avatar_url = data.avatarUrl; | ||||
|         user.emojis = []; | ||||
|         data.emojis.forEach(emoji => { | ||||
|             user.emojis.push(Emoji.parse(emoji, user.host)); | ||||
|         }); | ||||
|         return user; | ||||
|     } | ||||
| 
 | ||||
|     static #parse_mastodon(data) { | ||||
|         let user = new User(); | ||||
|         user.id = data.id; | ||||
|         user.nickname = data.display_name; | ||||
|         user.username = data.username; | ||||
|         user.host = data.acct.search('@') ? data.acct.substring(data.acct.search('@') + 1) : instance.host; | ||||
|         user.avatar_url = data.avatar; | ||||
|         user.emojis = []; | ||||
|         data.emojis.forEach(emoji => { | ||||
|             user.emojis.push(Emoji.parse(emoji, user.host)); | ||||
|         }); | ||||
|         return user; | ||||
|     } | ||||
| 
 | ||||
|     get name() { | ||||
|         return this.nickname || this.username; | ||||
|     } | ||||
|  | @ -80,4 +17,9 @@ export default class User { | |||
|         if (this.host) res += "@" + this.host; | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     get rich_name() { | ||||
|         if (!this.nickname) return this.username; | ||||
|         return parseEmojis(this.nickname, this.host); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue