huge refactor. addresses #21 w/ inifinite scrolling notifications
This commit is contained in:
		
							parent
							
								
									f883b61659
								
							
						
					
					
						commit
						2e64f63caa
					
				
					 29 changed files with 887 additions and 1030 deletions
				
			
		
							
								
								
									
										52
									
								
								src/lib/account.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/lib/account.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | |||
| import { server } from '$lib/client/server.js'; | ||||
| import { parseEmoji, renderEmoji } from '$lib/emoji.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| 
 | ||||
| const cache = writable({}); | ||||
| 
 | ||||
| /** | ||||
|  * Parses an account using API data, and returns a writable store object. | ||||
|  * @param {Object} data | ||||
|  * @param {number} ancestor_count | ||||
|  */ | ||||
| export function parseAccount(data) { | ||||
|     if (!data) { | ||||
|         console.error("Attempted to parse account data but no data was provided"); | ||||
|         return null; | ||||
|     } | ||||
|     let account = get(cache)[data.id]; | ||||
|     if (account) return account; | ||||
|     // cache miss!
 | ||||
| 
 | ||||
|     account = {}; | ||||
|     account.id = data.id; | ||||
|     account.nickname = data.display_name.trim(); | ||||
|     account.username = data.username; | ||||
|     account.name = account.nickname || account.username; | ||||
|     account.avatar_url = data.avatar; | ||||
|     account.url = data.url; | ||||
| 
 | ||||
|     if (data.acct.includes('@')) | ||||
|         account.host = data.acct.split('@')[1]; | ||||
|     else | ||||
|         account.host = get(server).host; | ||||
| 
 | ||||
|     account.mention = "@" + account.username; | ||||
|     if (account.host != get(server).host) | ||||
|         account.mention += "@" + account.host; | ||||
|      | ||||
|     account.emojis = {}; | ||||
|     data.emojis.forEach(emoji => { | ||||
|         account.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); | ||||
|     }); | ||||
| 
 | ||||
|     account.rich_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username; | ||||
| 
 | ||||
|     cache.update(cache => { | ||||
|         cache[account.id] = account; | ||||
|         return cache; | ||||
|     }); | ||||
| 
 | ||||
|     return account; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										339
									
								
								src/lib/api.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								src/lib/api.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,339 @@ | |||
| /** | ||||
|  * GET /api/v1/instance | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  */ | ||||
| export async function getInstance(host) { | ||||
|     const data = await fetch(`https://${host}/api/v1/instance`) | ||||
|         .then(res => res.json()) | ||||
|         .catch(error => console.error(error)); | ||||
|     return data ? data : false; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/apps | ||||
|  * Attempts to create an application for a given server host. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  */ | ||||
| export async function createApp(host) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_name", "Campfire"); | ||||
|     form.append("redirect_uris", `${location.origin}/callback`); | ||||
|     form.append("scopes", "read write push"); | ||||
|     form.append("website", "https://campfire.bliss.town"); | ||||
| 
 | ||||
|     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, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns the OAuth authorization url for the target server. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} app_id - The application id for the target server. | ||||
|  */ | ||||
| export function getOAuthUrl(host, app_id) { | ||||
|     return `https://${host}/oauth/authorize` + | ||||
|         `?client_id=${app_id}` + | ||||
|         "&scope=read+write+push" + | ||||
|         `&redirect_uri=${location.origin}/callback` + | ||||
|         "&response_type=code"; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /oauth/token | ||||
|  * Attempts to generate an OAuth token. | ||||
|  * Returns false on failure. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} client_id - The application id. | ||||
|  * @param {string} secret - The application secret. | ||||
|  * @param {string} code - The authorization code provided by OAuth. | ||||
|  */ | ||||
| export async function getToken(host, client_id, secret, code) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_id", client_id); | ||||
|     form.append("client_secret", 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://${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; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /oauth/revoke | ||||
|  * Attempts to revoke an OAuth token. | ||||
|  * Returns false on failure. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} client_id - The application id. | ||||
|  * @param {string} secret - The application secret. | ||||
|  * @param {string} token - The application token. | ||||
|  */ | ||||
| export async function revokeToken(host, client_id, secret, token) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_id", client_id); | ||||
|     form.append("client_secret", secret); | ||||
|     form.append("token", token); | ||||
| 
 | ||||
|     const res = await fetch(`https://${host}/oauth/revoke`, { | ||||
|         method: "POST", | ||||
|         body: form, | ||||
|     }) | ||||
|     .catch(error => { | ||||
|         console.error(error); | ||||
|         return false; | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) return false; | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/accounts/verify_credentials | ||||
|  * This endpoint returns information about the client account, | ||||
|  * and other useful data. | ||||
|  * Returns false on failure. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  */ | ||||
| export async function verifyCredentials(host, token) { | ||||
|     let url = `https://${host}/api/v1/accounts/verify_credentials`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/notifications | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} max_id - If provided, only shows notifications after this ID. | ||||
|  * @param {string} limit - The maximum number of notifications to retrieve (default 40). | ||||
|  * @param {string} types - A list of notification types to filter to. | ||||
|  */ | ||||
| export async function getNotifications(host, token, max_id, limit, types) { | ||||
|     let url = `https://${host}/api/v1/notifications`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (max_id) params.append("max_id", max_id); | ||||
|     if (limit) params.append("limit", limit); | ||||
|     if (types) params.append("types", types.join(',')); | ||||
|     const params_string = params.toString(); | ||||
|     if (params_string) url += '?' + params_string; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/timelines/{timeline} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} timeline - The name of the timeline to pull (default "home"). | ||||
|  * @param {string} max_id - If provided, only shows posts after this ID. | ||||
|  */ | ||||
| export async function getTimeline(host, token, timeline, max_id) { | ||||
|     let url = `https://${host}/api/v1/timelines/${timeline || "home"}`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (max_id) params.append("max_id", max_id); | ||||
|     const params_string = params.toString(); | ||||
|     if (params_string) url += '?' + params_string; | ||||
|      | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/statuses/{post_id}. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to fetch. | ||||
|  */ | ||||
| export async function getPost(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/statuses/{post_id}/context. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to fetch. | ||||
|  */ | ||||
| export async function getPostContext(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/context`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/reblog. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to boost. | ||||
|  */ | ||||
| export async function boostPost(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/reblog`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/unreblog. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to unboost. | ||||
|  */ | ||||
| export async function unboostPost(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/unreblog`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/favourite. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to favourite. | ||||
|  */ | ||||
| export async function favouritePost(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/favourite`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/unfavourite. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to unfavourite. | ||||
|  */ | ||||
| export async function unfavouritePost(host, token, post_id) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/unfavourite`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/react/{shortcode} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to favourite. | ||||
|  * @param {string} shortcode - The shortcode of the emote to react with. | ||||
|  */ | ||||
| export async function reactPost(host, token, post_id, shortcode) { | ||||
|     // note: reacting with foreign emotes is unsupported on most servers
 | ||||
|     // chuckya appears to allow this, but other servers tested have
 | ||||
|     // not demonstrated this.
 | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * POST /api/v1/statuses/{post_id}/unreact/{shortcode} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} post_id - The ID of the post to favourite. | ||||
|  * @param {string} shortcode - The shortcode of the reaction emote to remove. | ||||
|  */ | ||||
| export async function unreactPost(host, token, post_id, shortcode) { | ||||
|     let url = `https://${host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": `Bearer ${token}` } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /api/v1/accounts/{user_id} | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  * @param {string} token - The application token. | ||||
|  * @param {string} user_id - The ID of the user to fetch. | ||||
|  */ | ||||
| export async function getUser(host, token, user_id) { | ||||
|     let url = `https://${host}/api/v1/accounts/${user_id}`; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
|  | @ -1,339 +0,0 @@ | |||
| import { client } from '$lib/client/client.js'; | ||||
| import { user } from '$lib/stores/user.js'; | ||||
| import { capabilities } from '../client/instance.js'; | ||||
| import Post from '$lib/post.js'; | ||||
| import User from '$lib/user/user.js'; | ||||
| import Emoji from '$lib/emoji.js'; | ||||
| import { get } from 'svelte/store'; | ||||
| 
 | ||||
| export async function createApp(host) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_name", "Campfire"); | ||||
|     form.append("redirect_uris", `${location.origin}/callback`); | ||||
|     form.append("scopes", "read write push"); | ||||
|     form.append("website", "https://campfire.bliss.town"); | ||||
| 
 | ||||
|     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() { | ||||
|     return `https://${get(client).instance.host}/oauth/authorize` + | ||||
|         `?client_id=${get(client).app.id}` + | ||||
|         "&scope=read+write+push" + | ||||
|         `&redirect_uri=${location.origin}/callback` + | ||||
|         "&response_type=code"; | ||||
| } | ||||
| 
 | ||||
| export async function getToken(code) { | ||||
|     let form = new FormData(); | ||||
|     form.append("client_id", get(client).app.id); | ||||
|     form.append("client_secret", get(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://${get(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 form = new FormData(); | ||||
|     form.append("client_id", get(client).app.id); | ||||
|     form.append("client_secret", get(client).app.secret); | ||||
|     form.append("token", get(client).app.token); | ||||
| 
 | ||||
|     const res = await fetch(`https://${get(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 verifyCredentials() { | ||||
|     let url = `https://${get(client).instance.host}/api/v1/accounts/verify_credentials`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function getNotifications(since_id, limit, types) { | ||||
|     if (!get(user)) return false; | ||||
| 
 | ||||
|     let url = `https://${get(client).instance.host}/api/v1/notifications`; | ||||
| 
 | ||||
|     let params = new URLSearchParams(); | ||||
|     if (since_id) params.append("since_id", since_id); | ||||
|     if (limit) params.append("limit", limit); | ||||
|     if (types) params.append("types", types.join(',')); | ||||
|     const params_string = params.toString(); | ||||
|     if (params_string) url += '?' + params_string; | ||||
| 
 | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function getTimeline(last_post_id) { | ||||
|     if (!get(user)) return false; | ||||
|     let url = `https://${get(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 " + get(client).app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function getPost(post_id, ancestor_count) { | ||||
|     let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => { return res.ok ? res.json() : false }); | ||||
| 
 | ||||
|     if (data === false) return false; | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function getPostContext(post_id) { | ||||
|     let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/context`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => { return res.ok ? res.json() : false }); | ||||
| 
 | ||||
|     if (data === false) return false; | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function boostPost(post_id) { | ||||
|     let url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/reblog`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(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 url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreblog`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(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 url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/favourite`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(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 url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unfavourite`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(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 url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/react/${encodeURIComponent(shortcode)}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(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 url = `https://${get(client).instance.host}/api/v1/statuses/${post_id}/unreact/${encodeURIComponent(shortcode)}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => { return res.ok ? res.json() : false }); | ||||
| 
 | ||||
|     if (data === false) return false; | ||||
|     return data; | ||||
| } | ||||
| 
 | ||||
| export async function parsePost(data, ancestor_count) { | ||||
|     let post = new Post(); | ||||
| 
 | ||||
|     post.text = data.content; | ||||
|     post.html = data.content; | ||||
| 
 | ||||
|     post.reply = null; | ||||
|     if ((data.in_reply_to_id || data.reply) && | ||||
|         ancestor_count !== 0 | ||||
|     ) { | ||||
|         const reply_data = data.reply || await getPost(data.in_reply_to_id, ancestor_count - 1); | ||||
|         // if the post returns false, we probably don't have permission to read it.
 | ||||
|         // we'll respect the thread's privacy, and leave it alone :)
 | ||||
|         if (!reply_data) return false; | ||||
|         post.reply = await parsePost(reply_data, ancestor_count - 1, false); | ||||
|     } | ||||
| 
 | ||||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; | ||||
| 
 | ||||
|     post.id = data.id; | ||||
|     post.created_at = new Date(data.created_at); | ||||
|     post.user = await parseUser(data.account); | ||||
|     post.warning = data.spoiler_text; | ||||
|     post.boost_count = data.reblogs_count; | ||||
|     post.reply_count = data.replies_count; | ||||
|     post.favourite_count = data.favourites_count; | ||||
|     post.favourited = data.favourited; | ||||
|     post.boosted = data.reblogged; | ||||
|     post.mentions = data.mentions; | ||||
|     post.files = data.media_attachments; | ||||
|     post.url = data.url; | ||||
|     post.visibility = data.visibility; | ||||
| 
 | ||||
|     post.emojis = []; | ||||
|     if (data.emojis) { | ||||
|         data.emojis.forEach(emoji_data => { | ||||
|             let name = emoji_data.shortcode.split('@')[0]; | ||||
|             post.emojis.push(parseEmoji({ | ||||
|                 id: name + '@' + post.user.host, | ||||
|                 name: name, | ||||
|                 host: post.user.host, | ||||
|                 url: emoji_data.url, | ||||
|             })); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     if (data.reactions && get(client).instance.capabilities.includes(capabilities.REACTIONS)) { | ||||
|         post.reactions = parseReactions(data.reactions); | ||||
|     } | ||||
|     return post; | ||||
| } | ||||
| 
 | ||||
| export async function parseUser(data) { | ||||
|     if (!data) { | ||||
|         console.error("Attempted to parse user data but no data was provided"); | ||||
|         return null; | ||||
|     } | ||||
|     let user = await get(client).getCacheUser(data.id); | ||||
| 
 | ||||
|     if (user) return user; | ||||
|     // cache miss!
 | ||||
| 
 | ||||
|     user = new User(); | ||||
|     user.id = data.id; | ||||
|     user.nickname = data.display_name.trim(); | ||||
|     user.username = data.username; | ||||
|     user.avatar_url = data.avatar; | ||||
|     user.url = data.url; | ||||
| 
 | ||||
|     if (data.acct.includes('@')) | ||||
|         user.host = data.acct.split('@')[1]; | ||||
|     else | ||||
|         user.host = get(client).instance.host; | ||||
| 
 | ||||
|     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(parseEmoji(emoji_data)); | ||||
|     }); | ||||
| 
 | ||||
|     get(client).putCacheUser(user); | ||||
|     return user; | ||||
| } | ||||
| 
 | ||||
| export function parseReactions(data) { | ||||
|     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) { | ||||
|     let emoji = new Emoji( | ||||
|         data.id, | ||||
|         data.name, | ||||
|         data.host, | ||||
|         data.url, | ||||
|     ); | ||||
|     get(client).putCacheEmoji(emoji); | ||||
|     return emoji; | ||||
| } | ||||
| 
 | ||||
| export async function getUser(user_id) { | ||||
|     let url = `https://${get(client).instance.host}/api/v1/accounts/${user_id}`; | ||||
|     const data = await fetch(url, { | ||||
|         method: 'GET', | ||||
|         headers: { "Authorization": "Bearer " + get(client).app.token } | ||||
|     }).then(res => res.json()); | ||||
| 
 | ||||
|     return data; | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/lib/client/app.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/lib/client/app.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| import { writable } from 'svelte/store'; | ||||
| import { app_name } from '$lib/config.js'; | ||||
| import { browser } from "$app/environment"; | ||||
| 
 | ||||
| // if app is falsy, assume user has not begun the login process.
 | ||||
| // if app.token is falsy, assume user has not logged in.
 | ||||
| export const app = writable(loadApp()); | ||||
| 
 | ||||
| // write to localStorage on each update
 | ||||
| app.subscribe(app => { | ||||
|     saveApp(app); | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * Saves the provided app to localStorage. | ||||
|  * If `app` is falsy, data is removed from localStorage. | ||||
|  * @param {Object} app | ||||
|  */ | ||||
| function saveApp(app) { | ||||
|     if (!browser) return; | ||||
|     if (!app) localStorage.removeItem(app_name + "_app"); | ||||
|     localStorage.setItem(app_name + "_app", JSON.stringify(app)); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns application data loaded from localStorage, if it exists. | ||||
|  * Otherwise, returns false. | ||||
|  */ | ||||
| function loadApp() { | ||||
|     if (!browser) return; | ||||
|     let data = localStorage.getItem(app_name + "_app"); | ||||
|     if (!data) return false; | ||||
|     return JSON.parse(data); | ||||
| } | ||||
|  | @ -1,192 +0,0 @@ | |||
| import { Instance, server_types } from './instance.js'; | ||||
| import * as api from './api.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import { last_read_notif_id } from '$lib/notifications.js'; | ||||
| import { user, logged_in } from '$lib/stores/user.js'; | ||||
| 
 | ||||
| export const client = writable(false); | ||||
| 
 | ||||
| const save_name = "campfire"; | ||||
| 
 | ||||
| export class Client { | ||||
|     instance; | ||||
|     app; | ||||
|     #cache; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.instance = null; | ||||
|         this.app = null; | ||||
|         this.cache = { | ||||
|             users: {}, | ||||
|             emojis: {}, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     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()).catch(error => { console.error(error) }); | ||||
|         if (!data) { | ||||
|             console.error(`Failed to connect to ${host}`); | ||||
|             return `Failed to connect to ${host}!`; | ||||
|         } | ||||
|          | ||||
|         this.instance = new Instance(host, data.version); | ||||
|         if (this.instance.type == server_types.UNSUPPORTED) { | ||||
|             console.warn(`Server ${host} is unsupported - ${data.version}`); | ||||
|             if (!confirm( | ||||
|                 `This app does not officially support ${host}. ` + | ||||
|                 `Things may break, or otherwise not work as epxected! ` + | ||||
|                 `Are you sure you wish to continue?` | ||||
|             )) return false; | ||||
|         } else { | ||||
|             console.log(`Server is "${this.instance.type}" (or compatible) with capabilities: [${this.instance.capabilities}].`); | ||||
|         } | ||||
| 
 | ||||
|         this.app = await api.createApp(host); | ||||
| 
 | ||||
|         if (!this.app || !this.instance) { | ||||
|             console.error("Failed to create app. Check the network logs for details."); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         this.save(); | ||||
| 
 | ||||
|         client.set(this); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     getOAuthUrl() { | ||||
|         return api.getOAuthUrl(this.app.secret); | ||||
|     } | ||||
| 
 | ||||
|     async getToken(code) { | ||||
|         const token = await api.getToken(code); | ||||
|         if (!token) { | ||||
|             console.error("Failed to obtain access token"); | ||||
|             return false; | ||||
|         } | ||||
|         return token; | ||||
|     } | ||||
| 
 | ||||
|     async revokeToken() { | ||||
|         return await api.revokeToken(); | ||||
|     } | ||||
| 
 | ||||
|     async getNotifications(since_id, limit, types) { | ||||
|         return await api.getNotifications(since_id, limit, types); | ||||
|     } | ||||
| 
 | ||||
|     async getTimeline(last_post_id) { | ||||
|         return await api.getTimeline(last_post_id); | ||||
|     } | ||||
| 
 | ||||
|     async getPost(post_id, parent_replies, child_replies) { | ||||
|         return await api.getPost(post_id, parent_replies, child_replies); | ||||
|     } | ||||
| 
 | ||||
|     async getPostContext(post_id) { | ||||
|         return await api.getPostContext(post_id); | ||||
|     } | ||||
| 
 | ||||
|     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) { | ||||
|         this.cache.users[user.id] = user; | ||||
|         client.set(this); | ||||
|     } | ||||
| 
 | ||||
|     async getCacheUser(user_id) { | ||||
|         let user = this.cache.users[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; | ||||
|         client.set(this); | ||||
|     } | ||||
| 
 | ||||
|     getEmoji(emoji_id) { | ||||
|         let emoji = this.cache.emojis[emoji_id]; | ||||
|         if (!emoji) return false; | ||||
|         return emoji; | ||||
|     } | ||||
| 
 | ||||
|     async getUser(user_id) { | ||||
|         return await api.getUser(user_id); | ||||
|     } | ||||
| 
 | ||||
|     save() { | ||||
|         if (typeof localStorage === typeof undefined) return; | ||||
|         localStorage.setItem(save_name, JSON.stringify({ | ||||
|             version: APP_VERSION, | ||||
|             instance: { | ||||
|                 host: this.instance.host, | ||||
|                 version: this.instance.version, | ||||
|             }, | ||||
|             last_read_notif_id: get(last_read_notif_id), | ||||
|             app: this.app, | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     load() { | ||||
|         if (typeof localStorage === typeof undefined) return; | ||||
|         let json = localStorage.getItem(save_name); | ||||
|         if (!json) return false; | ||||
|         let saved = JSON.parse(json); | ||||
|         if (!saved.version || saved.version !== APP_VERSION) { | ||||
|             localStorage.removeItem(save_name); | ||||
|             return false; | ||||
|         } | ||||
|         this.instance = new Instance(saved.instance.host, saved.instance.version); | ||||
|         last_read_notif_id.set(saved.last_read_notif_id || 0); | ||||
|         this.app = saved.app; | ||||
|         client.set(this); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     async logout() { | ||||
|         if (!this.instance || !this.app) return; | ||||
|         if (!await this.revokeToken()) { | ||||
|             console.warn("Failed to log out correctly; ditching the old tokens anyways."); | ||||
|         } | ||||
|         localStorage.removeItem(save_name); | ||||
|         logged_in.set(false); | ||||
|         client.set(new Client()); | ||||
|         console.log("Logged out successfully."); | ||||
|     } | ||||
| } | ||||
|  | @ -1,70 +0,0 @@ | |||
| export const server_types = { | ||||
|     UNSUPPORTED: "unsupported", | ||||
|     MASTODON: "mastodon", | ||||
|     GLITCHSOC: "glitchsoc", | ||||
|     CHUCKYA: "chuckya", | ||||
|     FIREFISH: "firefish", | ||||
|     ICESHRIMP: "iceshrimp", | ||||
|     SHARKEY: "sharkey", | ||||
| }; | ||||
| 
 | ||||
| export const capabilities = { | ||||
|     MARKDOWN_CONTENT: "mdcontent", | ||||
|     REACTIONS: "reactions", | ||||
| }; | ||||
| 
 | ||||
| export class Instance { | ||||
|     host; | ||||
|     version; | ||||
|     capabilities; | ||||
|     type = server_types.UNSUPPORTED; | ||||
| 
 | ||||
|     constructor(host, version) { | ||||
|         this.host = host; | ||||
|         this.version = version; | ||||
|         this.#setType(version); | ||||
|         this.capabilities = this.#getCapabilities(this.type); | ||||
|     } | ||||
| 
 | ||||
|     #setType(version) { | ||||
| 	this.type = server_types.UNSUPPORTED; | ||||
|         if (version.constructor !== String) return; | ||||
|         let version_lower = version.toLowerCase(); | ||||
|         for (let i = 1; i < Object.keys(server_types).length; i++) { | ||||
|             const check_type = Object.values(server_types)[i]; | ||||
|             if (version_lower.includes(check_type)) { | ||||
|                 this.type = check_type; | ||||
| 		return; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #getCapabilities(type) { | ||||
|         let c = []; | ||||
|         switch (type) { | ||||
|             case server_types.MASTODON: | ||||
|                 break; | ||||
|             case server_types.GLITCHSOC: | ||||
|                 c.push(capabilities.REACTIONS); | ||||
|                 break; | ||||
|             case server_types.CHUCKYA: | ||||
|                 c.push(capabilities.REACTIONS); | ||||
|                 break; | ||||
|             case server_types.FIREFISH: | ||||
|                 c.push(capabilities.REACTIONS); | ||||
|                 break; | ||||
|             case server_types.ICESHRIMP: | ||||
|                 // more trouble than it's worth atm
 | ||||
|                 // the server already hands this to us ;p
 | ||||
|                 //c.push(capabilities.MARKDOWN_CONTENT);
 | ||||
|                 c.push(capabilities.REACTIONS); | ||||
|                 break; | ||||
|             case server_types.SHARKEY: | ||||
|                 c.push(capabilities.REACTIONS); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|         return c; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										138
									
								
								src/lib/client/server.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/lib/client/server.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,138 @@ | |||
| import * as api from '$lib/api.js'; | ||||
| import { writable } from 'svelte/store'; | ||||
| import { app_name } from '$lib/config.js'; | ||||
| import { browser } from "$app/environment"; | ||||
| 
 | ||||
| const server_types = { | ||||
|     UNSUPPORTED: "unsupported", | ||||
|     MASTODON: "mastodon", | ||||
|     GLITCHSOC: "glitchsoc", | ||||
|     CHUCKYA: "chuckya", | ||||
|     FIREFISH: "firefish", | ||||
|     ICESHRIMP: "iceshrimp", | ||||
|     SHARKEY: "sharkey", | ||||
|     AKKOMA: "akkoma", // TODO: verify
 | ||||
|     PLEROMA: "pleroma", // TODO: verify
 | ||||
| }; | ||||
| 
 | ||||
| export const capabilities = { | ||||
|     MARKDOWN_CONTENT: "mdcontent", | ||||
|     REACTIONS: "reactions", | ||||
| }; | ||||
| 
 | ||||
| // if server is falsy, assume user has not begun the login process.
 | ||||
| export let server = writable(loadServer()); | ||||
| 
 | ||||
| // write to localStorage on each update
 | ||||
| server.subscribe(server => { | ||||
|     saveServer(server); | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * Attempts to create an server object using a given hostname. | ||||
|  * @param {string} host - The domain of the target server. | ||||
|  */ | ||||
| export async function createServer(host) { | ||||
|     if (!host) { | ||||
|         console.error("Attempted to create server without providing a hostname"); | ||||
|         return false; | ||||
|     } | ||||
|     if (host.startsWith("http://")) { | ||||
|         console.error("Cowardly refusing to connect to an insecure server"); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     let server = {}; | ||||
|     server.host = host; | ||||
| 
 | ||||
|     if (host.startsWith("https://")) host = host.substring(8); | ||||
|     const data = await api.getInstance(host); | ||||
|     if (!data) { | ||||
|         console.error(`Failed to connect to ${host}`); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     server.version = data.version; | ||||
|     server.type = getType(server.version); | ||||
|     server.capabilities = getCapabilities(server.type); | ||||
| 
 | ||||
|     if (server.type === server_types.UNSUPPORTED) { | ||||
|         console.warn(`Server ${host} is unsupported (${server.version}). Things may break, or not work as expected`); | ||||
|     } else { | ||||
|         console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`); | ||||
|     } | ||||
| 
 | ||||
|     return server; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Saves the provided server to localStorage. | ||||
|  * If `server` is falsy, data is removed from localStorage. | ||||
|  * @param {Object} server | ||||
|  */ | ||||
| function saveServer(server) { | ||||
|     if (!browser) return; | ||||
|     if (!server) localStorage.removeItem(app_name + "_server"); | ||||
|     localStorage.setItem(app_name + "_server", JSON.stringify(server)); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns server data loaded from localStorage, if it exists. | ||||
|  * Otherwise, returns false. | ||||
|  */ | ||||
| function loadServer() { | ||||
|     if (!browser) return; | ||||
|     let data = localStorage.getItem(app_name + "_server"); | ||||
|     if (!data) return false; | ||||
|     return JSON.parse(data); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns the type of an server, inferred from its version string. | ||||
|  * @param {string} version | ||||
|  * @returns the inferred server_type | ||||
|  */ | ||||
| function getType(version) { | ||||
|     if (version.constructor !== String) return; | ||||
|     let version_lower = version.toLowerCase(); | ||||
|     for (let i = 1; i < Object.keys(server_types).length; i++) { | ||||
|         const type = Object.values(server_types)[i]; | ||||
|         if (version_lower.includes(type)) { | ||||
|             return type; | ||||
|         } | ||||
|     } | ||||
|     return server_types.UNSUPPORTED; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns a list of capabilities for a given server_type. | ||||
|  * @param {string} type | ||||
|  */ | ||||
| function getCapabilities(type) { | ||||
|     let c = []; | ||||
|     switch (type) { | ||||
|         case server_types.MASTODON: | ||||
|             break; | ||||
|         case server_types.GLITCHSOC: | ||||
|             c.push(capabilities.REACTIONS); | ||||
|             break; | ||||
|         case server_types.CHUCKYA: | ||||
|             c.push(capabilities.REACTIONS); | ||||
|             break; | ||||
|         case server_types.FIREFISH: | ||||
|             c.push(capabilities.REACTIONS); | ||||
|             break; | ||||
|         case server_types.ICESHRIMP: | ||||
|             // more trouble than it's worth atm
 | ||||
|             // mastodon API already hands html to us
 | ||||
|             //c.push(capabilities.MARKDOWN_CONTENT);
 | ||||
|             c.push(capabilities.REACTIONS); | ||||
|             break; | ||||
|         case server_types.SHARKEY: | ||||
|             c.push(capabilities.REACTIONS); | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|     } | ||||
|     return c; | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/lib/config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| export const app_name = "campfire"; | ||||
|  | @ -1,52 +1,27 @@ | |||
| import { client } from './client/client.js'; | ||||
| import { get } from 'svelte/store'; | ||||
| export const EMOJI_REGEX = /:[\w\-.]{0,32}:/g; | ||||
| 
 | ||||
| export const EMOJI_REGEX = /:[\w\-.]{0,32}@[\w\-.]{0,32}:/g; | ||||
| export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; | ||||
| 
 | ||||
| export default class Emoji { | ||||
|     name; | ||||
|     url; | ||||
| 
 | ||||
|     constructor(id, name, host, url) { | ||||
|         this.id = id; | ||||
|         this.name = name; | ||||
|         this.host = host; | ||||
|         this.url = url; | ||||
|     } | ||||
| 
 | ||||
|     get html() { | ||||
|         if (this.url) | ||||
|             return `<img src="${this.url}" class="emoji" height="20" title="${this.name}" alt="${this.name}">`; | ||||
|         else | ||||
|             return `${this.name}`; | ||||
|     } | ||||
| export function parseEmoji(shortcode, url) { | ||||
|     let emoji = { shortcode, url }; | ||||
|     if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode
 | ||||
|     emoji.html = `<img src="${emoji.url}" class="emoji" height="20" title="${emoji.shortcode}" alt="${emoji.shortcode}"/>`; | ||||
|     return emoji; | ||||
| } | ||||
| 
 | ||||
| export function parseText(text, host) { | ||||
| export function renderEmoji(text, emoji_list) { | ||||
|     if (!text) return text; | ||||
|      | ||||
|     let index = text.search(EMOJI_NAME_REGEX); | ||||
|     let index = text.search(EMOJI_REGEX); | ||||
|     if (index === -1) return text; | ||||
| 
 | ||||
|     // find the emoji name
 | ||||
|     // find the closing comma
 | ||||
|     let length = text.substring(index + 1).search(':'); | ||||
|     if (length <= 0) return text; | ||||
|     let emoji_name = text.substring(index + 1, index + length + 1); | ||||
|     let emoji = get(client).getEmoji(emoji_name + '@' + host); | ||||
| 
 | ||||
|     if (emoji) { | ||||
|         return text.substring(0, index) + emoji.html + | ||||
|             parseText(text.substring(index + length + 2), host); | ||||
|     } | ||||
|     return text.substring(0, index + length + 1) + | ||||
|         parseText(text.substring(index + length + 1), host); | ||||
| } | ||||
|     // see if emoji is valid
 | ||||
|     let shortcode = text.substring(index + 1, index + length + 1); | ||||
|     let emoji = emoji_list[shortcode]; | ||||
|     let replace = emoji ? emoji.html : shortcode; | ||||
| 
 | ||||
| export function parseOne(emoji_id) { | ||||
|     if (emoji_id == '❤') return '❤️'; // stupid heart unicode
 | ||||
|     if (EMOJI_REGEX.exec(':' + emoji_id + ':')) return emoji_id; | ||||
|     let cached_emoji = get(client).getEmoji(emoji_id); | ||||
|     if (!cached_emoji) return emoji_id; | ||||
|     return cached_emoji.html; | ||||
|     return text.substring(0, index) + replace + renderEmoji(text.substring(index + length + 2), emoji_list); | ||||
| } | ||||
|  |  | |||
|  | @ -1,40 +1,88 @@ | |||
| import { client } from '$lib/client/client.js'; | ||||
| import * as api from '$lib/client/api.js'; | ||||
| import * as api from '$lib/api.js'; | ||||
| import { server } from '$lib/client/server.js'; | ||||
| import { app } from '$lib/client/app.js'; | ||||
| import { app_name } from '$lib/config.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import { browser } from '$app/environment'; | ||||
| import { parsePost } from '$lib/post.js'; | ||||
| import { parseAccount } from '$lib/account.js'; | ||||
| 
 | ||||
| export let notifications = writable([]); | ||||
| export let unread_notif_count = writable(0); | ||||
| export let last_read_notif_id = writable(0); | ||||
| const prefix = app_name + '_notif_'; | ||||
| 
 | ||||
| export const notifications = writable([]); | ||||
| export const unread_notif_count = writable(load("unread_count")); | ||||
| export const last_read_notif_id = writable(load("last_read")); | ||||
| 
 | ||||
| unread_notif_count.subscribe(count => save("unread_count", count)); | ||||
| last_read_notif_id.subscribe(id => save("last_read", id)); | ||||
| 
 | ||||
| /** | ||||
|  * Saves the provided data to localStorage. | ||||
|  * If `data` is falsy, the record is removed from localStorage. | ||||
|  * @param {Object} name | ||||
|  * @param {any} data | ||||
|  */ | ||||
| function save(name, data) { | ||||
|     if (!browser) return; | ||||
|     if (data) { | ||||
|         localStorage.setItem(prefix + name, data); | ||||
|     } else { | ||||
|         localStorage.removeItem(prefix + name); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Returns named data loaded from localStorage, if it exists. | ||||
|  * Otherwise, returns false. | ||||
|  */ | ||||
| function load(name) { | ||||
|     if (!browser) return; | ||||
|     let data = localStorage.getItem(prefix + name); | ||||
|     return data ? data : false; | ||||
| } | ||||
| 
 | ||||
| let loading; | ||||
| export async function getNotifications() { | ||||
| export async function getNotifications(clean) { | ||||
|     if (loading) return; // no spamming!!
 | ||||
|     loading = true; | ||||
| 
 | ||||
|     api.getNotifications().then(async data => { | ||||
|         if (!data || data.length <= 0) return; | ||||
|         notifications.set([]); | ||||
|         for (let i in data) { | ||||
|             let notif = data[i]; | ||||
|             notif.accounts = [ await api.parseUser(notif.account) ]; | ||||
|             if (get(notifications).length > 0) { | ||||
|                 let prev = get(notifications)[get(notifications).length - 1]; | ||||
|                 if (notif.type === prev.type) { | ||||
|                     if (prev.status && notif.status && prev.status.id === notif.status.id) { | ||||
|                         notifications.update(notifications => { | ||||
|                             notifications[notifications.length - 1].accounts.push(notif.accounts[0]); | ||||
|                             return notifications; | ||||
|                         }); | ||||
|                         continue; | ||||
|                     } | ||||
|     let last_id = false; | ||||
|     if (!clean && get(notifications).length > 0) | ||||
|         last_id = get(notifications)[get(notifications).length - 1].id; | ||||
| 
 | ||||
|     const notif_data = await api.getNotifications( | ||||
|         get(server).host, | ||||
|         get(app).token, | ||||
|         last_id | ||||
|     ); | ||||
| 
 | ||||
|     if (!notif_data) { | ||||
|         console.error(`Failed to retrieve notifications.`); | ||||
|         loading = false; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (clean) notifications.set([]); | ||||
| 
 | ||||
|     for (let i in notif_data) { | ||||
|         let notif = notif_data[i]; | ||||
|         notif.accounts = [ await parseAccount(notif.account) ]; | ||||
|         if (get(notifications).length > 0) { | ||||
|             let prev = get(notifications)[get(notifications).length - 1]; | ||||
|             if (notif.type === prev.type) { | ||||
|                 if (prev.status && notif.status && prev.status.id === notif.status.id) { | ||||
|                     notifications.update(notifications => { | ||||
|                         notifications[notifications.length - 1].accounts.push(notif.accounts[0]); | ||||
|                         return notifications; | ||||
|                     }); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             notif.status = notif.status ? await api.parsePost(notif.status, 0, false) : null; | ||||
|             notifications.update(notifications => [...notifications, notif]); | ||||
|         } | ||||
|         last_read_notif_id.set(data[0].id); | ||||
|         unread_notif_count.set(0); | ||||
|         get(client).save(); | ||||
|         loading = false; | ||||
|     }); | ||||
|         notif.status = notif.status ? await parsePost(notif.status, 0, false) : null; | ||||
|         notifications.update(notifications => [...notifications, notif]); | ||||
|     } | ||||
|     if (!last_id) last_read_notif_id.set(notif_data[0].id); | ||||
|     if (!last_id) unread_notif_count.set(0); | ||||
|     loading = false; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										235
									
								
								src/lib/post.js
									
										
									
									
									
								
							
							
						
						
									
										235
									
								
								src/lib/post.js
									
										
									
									
									
								
							|  | @ -1,177 +1,82 @@ | |||
| import { parseText as parseEmoji } from './emoji.js'; | ||||
| import * as api from '$lib/api.js'; | ||||
| import { server } from '$lib/client/server.js'; | ||||
| import { app } from '$lib/client/app.js'; | ||||
| import { parseAccount } from '$lib/account.js'; | ||||
| import { parseEmoji, renderEmoji } from '$lib/emoji.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| 
 | ||||
| export default class Post { | ||||
|     id; | ||||
|     created_at; | ||||
|     user; | ||||
|     text; | ||||
|     warning; | ||||
|     boost_count; | ||||
|     reply_count; | ||||
|     favourite_count; | ||||
|     favourited; | ||||
|     boosted; | ||||
|     mentions; | ||||
|     reactions; | ||||
|     emojis; | ||||
|     files; | ||||
|     url; | ||||
|     reply; | ||||
|     reply_id; | ||||
|     replies; | ||||
|     boost; | ||||
|     visibility; | ||||
| const cache = writable({}); | ||||
| 
 | ||||
|     async rich_text() { | ||||
|         return parseEmoji(this.text, this.user.host); | ||||
| /** | ||||
|  * Parses a post using API data, and returns a writable store object. | ||||
|  * @param {Object} data | ||||
|  * @param {number} ancestor_count | ||||
|  */ | ||||
| export async function parsePost(data, ancestor_count) { | ||||
|     let post = {}; | ||||
|     if (!ancestor_count) ancestor_count = 0; | ||||
| 
 | ||||
|     post.html = data.content; | ||||
| 
 | ||||
|     post.reply = null; | ||||
|     if ((data.in_reply_to_id || data.reply) && ancestor_count !== 0) { | ||||
|         const reply_data = data.reply || await api.getPost(get(server).host, get(app).token, data.in_reply_to_id); | ||||
|         // if the post returns false, we probably don't have permission to read it.
 | ||||
|         // we'll respect the thread's privacy, and leave it alone :)
 | ||||
|         if (!reply_data) return false; | ||||
|         post.reply = await parsePost(reply_data, ancestor_count - 1, false); | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|     async rich_text() { | ||||
|         let text = this.text; | ||||
|         if (!text) return text; | ||||
|         let client = Client.get(); | ||||
|     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; | ||||
| 
 | ||||
|         const markdown_tokens = [ | ||||
|             { tag: "pre", token: "```" }, | ||||
|             { tag: "code", token: "`" }, | ||||
|             { tag: "strong", token: "**" }, | ||||
|             { tag: "strong", token: "__" }, | ||||
|             { tag: "em", token: "*" }, | ||||
|             { tag: "em", token: "_" }, | ||||
|         ]; | ||||
|     post.id = data.id; | ||||
|     post.created_at = new Date(data.created_at); | ||||
|     post.account = await parseAccount(data.account); | ||||
|     post.warning = data.spoiler_text; | ||||
|     post.reply_count = data.replies_count; | ||||
|     post.boost_count = data.reblogs_count; | ||||
|     post.boosted = data.reblogged; | ||||
|     post.favourite_count = data.favourites_count; | ||||
|     post.favourited = data.favourited; | ||||
|     post.mentions = data.mentions; | ||||
|     post.media = data.media_attachments; | ||||
|     post.url = data.url; | ||||
|     post.visibility = data.visibility; | ||||
| 
 | ||||
|         let response = ""; | ||||
|         let md_layer; | ||||
|         let index = 0; | ||||
|         while (index < text.length) { | ||||
|             let sample = text.substring(index); | ||||
|             let md_nostack = !(md_layer && md_layer.nostack); | ||||
|     post.emojis = []; | ||||
|     data.emojis.forEach(emoji => { | ||||
|         post.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); | ||||
|     }); | ||||
| 
 | ||||
|             // handle newlines
 | ||||
|             if (md_nostack && sample.startsWith('\n')) { | ||||
|                 response += "<br>"; | ||||
|                 index++; | ||||
|                 continue; | ||||
|             } | ||||
|     if (data.reactions) post.reactions = parseReactions(data.reactions); | ||||
| 
 | ||||
|             // handle mentions
 | ||||
|             if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) | ||||
|                 && md_nostack | ||||
|                 && 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++; | ||||
|                 length++; // skim the middle @
 | ||||
|                 while (index + length < text.length && /[a-z0-9-_.]/.test(text[index + length])) length++; | ||||
|     post.rich_text = renderEmoji(post.html, post.emojis); | ||||
| 
 | ||||
|                 let mention = text.substring(index, index + length); | ||||
|     return post; | ||||
| 
 | ||||
|                 // attempt to resolve mention to a user
 | ||||
|                 let user = await client.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.username + '@' + user.host + "</a>"; | ||||
|                     if (md_layer) md_layer.text += out; | ||||
|                     else response += out; | ||||
|                 } else { | ||||
|                     response += mention; | ||||
|                 } | ||||
|                 index += mention.length; | ||||
|                 continue; | ||||
|             } | ||||
|     // let cache_post = get(cache)[post.id];
 | ||||
|     // if (cache_post) {
 | ||||
|     //     cache_post.set(post);
 | ||||
|     // } else {
 | ||||
|     //     cache.update(cache => {
 | ||||
|     //         cache[post.id] = writable(post);
 | ||||
|     //         return cache;
 | ||||
|     //     });
 | ||||
|     // }
 | ||||
| 
 | ||||
|             // handle links
 | ||||
|             if (client.instance.capabilities.includes(capabilities.MARKDOWN_CONTENT) | ||||
|                 && md_nostack | ||||
|                 && 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 (md_layer) md_layer.text += out; | ||||
|                 else response += out; | ||||
|                 index += length; | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // handle emojis
 | ||||
|             if (md_nostack && 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.getEmoji(emoji_name + '@' + this.user.host); | ||||
| 
 | ||||
|                 index += length + 2; | ||||
| 
 | ||||
|                 if (!emoji) { | ||||
|                     let out = ':' + emoji_name + ':'; | ||||
|                     if (md_layer) md_layer.text += out; | ||||
|                     else response += out; | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 let out = emoji.html; | ||||
|                 if (md_layer) md_layer.text += out; | ||||
|                 else response += out; | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // handle markdown
 | ||||
|             // TODO: handle misskey-flavoured markdown(?)
 | ||||
|             if (md_layer) { | ||||
|                 // try to pop layer
 | ||||
|                 if (sample.startsWith(md_layer.token)) { | ||||
|                     index += md_layer.token.length; | ||||
|                     let out = `<${md_layer.tag}>${md_layer.text}</${md_layer.tag}>`; | ||||
|                     if (md_layer.token === '```') | ||||
|                         out = `<code><pre>${md_layer.text}</pre></code>`; | ||||
|                     if (md_layer.parent) md_layer.parent.text += out; | ||||
|                     else response += out; | ||||
|                     md_layer = md_layer.parent; | ||||
|                 } else { | ||||
|                     md_layer.text += sample[0]; | ||||
|                     index++; | ||||
|                 } | ||||
|             } else if (md_nostack) { | ||||
|                 // should we add a layer?
 | ||||
|                 let pushed = false; | ||||
|                 for (let i = 0; i < markdown_tokens.length; i++) { | ||||
|                     let item = markdown_tokens[i]; | ||||
|                     if (sample.startsWith(item.token)) { | ||||
|                         let new_md_layer = { | ||||
|                             token: item.token, | ||||
|                             tag: item.tag, | ||||
|                             text: "", | ||||
|                             parent: md_layer, | ||||
|                         }; | ||||
|                         if (item.token === '```' || item.token === '`') new_md_layer.nostack = true; | ||||
|                         md_layer = new_md_layer; | ||||
|                         pushed = true; | ||||
|                         index += md_layer.token.length; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!pushed) { | ||||
|                     response += sample[0]; | ||||
|                     index++; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // destroy the remaining stack
 | ||||
|         while (md_layer) { | ||||
|             let out = md_layer.token + md_layer.text; | ||||
|             if (md_layer.parent) md_layer.parent.text += out; | ||||
|             else response += out; | ||||
|             md_layer = md_layer.parent; | ||||
|         } | ||||
| 
 | ||||
|         return response; | ||||
|     } | ||||
|     */ | ||||
|     // return get(cache)[post.id];
 | ||||
| } | ||||
| 
 | ||||
| export function parseReactions(data) { | ||||
|     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; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										4
									
								
								src/lib/stores/account.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/lib/stores/account.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| import { writable } from 'svelte/store'; | ||||
| 
 | ||||
| export let account = writable(false); | ||||
| export let logged_in = writable(false); | ||||
|  | @ -1,22 +0,0 @@ | |||
| import { client } from '$lib/client/client.js'; | ||||
| import * as api from '$lib/client/api.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| 
 | ||||
| export let user = writable(0); | ||||
| export let logged_in = writable(false); | ||||
| 
 | ||||
| export async function getUser() { | ||||
|     // already known
 | ||||
|     if (get(user)) return get(user); | ||||
| 
 | ||||
|     // cannot provide- not logged in
 | ||||
|     if (!get(client).app || !get(client).app.token) return false; | ||||
| 
 | ||||
|     // logged in- attempt to retrieve using token
 | ||||
|     const data = await api.verifyCredentials(); | ||||
|     if (!data) return false; | ||||
| 
 | ||||
|     user.set(await api.parseUser(data)); | ||||
|     console.log(`Logged in as @${get(user).username}@${get(user).host}`); | ||||
|     return get(user); | ||||
| } | ||||
|  | @ -1,8 +1,10 @@ | |||
| import { client } from '$lib/client/client.js'; | ||||
| import * as api from '$lib/api.js'; | ||||
| import { server } from '$lib/client/server.js'; | ||||
| import { app } from '$lib/client/app.js'; | ||||
| import { get, writable } from 'svelte/store'; | ||||
| import { parsePost } from '$lib/client/api.js'; | ||||
| import { parsePost } from '$lib/post.js'; | ||||
| 
 | ||||
| export let timeline = writable([]); | ||||
| export const timeline = writable([]); | ||||
| 
 | ||||
| let loading = false; | ||||
| 
 | ||||
|  | @ -10,9 +12,16 @@ export async function getTimeline(clean) { | |||
|     if (loading) return; // no spamming!!
 | ||||
|     loading = true; | ||||
| 
 | ||||
|     let timeline_data; | ||||
|     if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() | ||||
|     else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); | ||||
|     let last_post = false; | ||||
|     if (!clean && get(timeline).length > 0) | ||||
|         last_post = get(timeline)[get(timeline).length - 1].id; | ||||
| 
 | ||||
|     const timeline_data = await api.getTimeline( | ||||
|         get(server).host, | ||||
|         get(app).token, | ||||
|         "home", | ||||
|         last_post | ||||
|     ); | ||||
| 
 | ||||
|     if (!timeline_data) { | ||||
|         console.error(`Failed to retrieve timeline.`); | ||||
|  | @ -24,7 +33,7 @@ export async function getTimeline(clean) { | |||
| 
 | ||||
|     for (let i in timeline_data) { | ||||
|         const post_data = timeline_data[i]; | ||||
|         const post = await parsePost(post_data, 1, false); | ||||
|         const post = await parsePost(post_data, 1); | ||||
|         if (!post) { | ||||
|             if (post === null || post === undefined) { | ||||
|                 if (post_data.id) { | ||||
|  |  | |||
|  | @ -1,37 +1,42 @@ | |||
| <script> | ||||
|     import { client } from '$lib/client/client.js'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { server, createServer } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     import Logo from '$lib/../img/campfire-logo.svg'; | ||||
| 
 | ||||
|     let instance_url_error = false; | ||||
|     let display_error = false; | ||||
|     let logging_in = false; | ||||
| 
 | ||||
|     function log_in(event) { | ||||
|     async function log_in(event) { | ||||
|         event.preventDefault(); | ||||
|         instance_url_error = false; | ||||
|         display_error = false; | ||||
| 
 | ||||
|         logging_in = true; | ||||
|         const host = event.target.host.value; | ||||
| 
 | ||||
|         if (!host || host === "") { | ||||
|             instance_url_error = "Please enter an instance domain."; | ||||
|             display_error = "Please enter an server domain."; | ||||
|             logging_in = false; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         console.log(client); | ||||
| 
 | ||||
|         get(client).init(host).then(res => { | ||||
|         server.set(await createServer(host)); | ||||
|         if (!get(server)) { | ||||
|             display_error = "Failed to connect to the server.\nCheck the browser console for details!" | ||||
|             logging_in = false; | ||||
|             if (!res) return; | ||||
|             if (res.constructor === String) { | ||||
|                 instance_url_error = res; | ||||
|                 return; | ||||
|             }; | ||||
|             let oauth_url = get(client).getOAuthUrl(); | ||||
|             location = oauth_url; | ||||
|         }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         app.set(await api.createApp(get(server).host)); | ||||
|         if (!get(app)) { | ||||
|             display_error = "Failed to create an application for this server." | ||||
|             logging_in = false; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         location = api.getOAuthUrl(get(server).host, get(app).id); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -40,11 +45,11 @@ | |||
|         <Logo /> | ||||
|     </div> | ||||
|     <p>Welcome, fediverse user!</p> | ||||
|     <p>Please enter your instance domain to log in.</p> | ||||
|     <p>Please enter your server domain to log in.</p> | ||||
|     <div class="input-wrapper"> | ||||
|         <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> | ||||
|         {#if instance_url_error} | ||||
|             <p class="error">{instance_url_error}</p> | ||||
|         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> | ||||
|         {#if display_error} | ||||
|             <p class="error">{display_error}</p> | ||||
|         {/if} | ||||
|     </div> | ||||
|     <br> | ||||
|  |  | |||
|  | @ -1,18 +1,17 @@ | |||
| <script> | ||||
|     import Logo from '$lib/../img/campfire-logo.svg'; | ||||
|     import Button from './Button.svelte'; | ||||
|     import Feed from './Feed.svelte'; | ||||
|     import { client } from '$lib/client/client.js'; | ||||
|     import { user } from '$lib/stores/user.js'; | ||||
|     import { account, logged_in } from '$lib/stores/account.js'; | ||||
|     import { play_sound } from '$lib/sound.js'; | ||||
|     import { getTimeline } from '$lib/timeline.js'; | ||||
|     import { getNotifications } from '$lib/notifications.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { page } from '$app/stores'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
| 
 | ||||
|     import Logo from '$lib/../img/campfire-logo.svg'; | ||||
|     import Button from './Button.svelte'; | ||||
|     import Feed from './Feed.svelte'; | ||||
| 
 | ||||
|     import TimelineIcon from '../../img/icons/timeline.svg'; | ||||
|     import NotificationsIcon from '../../img/icons/notifications.svg'; | ||||
|     import ExploreIcon from '../../img/icons/explore.svg'; | ||||
|  | @ -37,7 +36,7 @@ | |||
|                 break; | ||||
|             case "notifications": | ||||
|                 route = "/notifications"; | ||||
|                 getNotifications(); | ||||
|                 getNotifications(true); | ||||
|                 break; | ||||
|             case "explore": | ||||
|             case "lists": | ||||
|  | @ -63,7 +62,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <div id="navigation"> | ||||
|     <header class="instance-header"> | ||||
|     <header class="server-header"> | ||||
|         <div class="app-logo"> | ||||
|             <Logo /> | ||||
|         </div> | ||||
|  | @ -151,11 +150,11 @@ | |||
|         </div> | ||||
| 
 | ||||
|         <div id="account-button"> | ||||
|             <img src={$user.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => play_sound()}> | ||||
|             <img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => play_sound()}> | ||||
|             <div class="account-name" aria-hidden="true"> | ||||
|                 <a href={$user.url} class="nickname" title={$user.nickname}>{@html $user.rich_name}</a> | ||||
|                 <span class="username" title={`@${$user.username}@${$user.host}`}> | ||||
|                     {`@${$user.username}@${$user.host}`} | ||||
|                 <a href={$account.url} class="nickname" title={$account.nickname}>{@html $account.rich_name}</a> | ||||
|                 <span class="username" title={`@${$account.username}@${$account.host}`}> | ||||
|                     {`@${$account.username}@${$account.host}`} | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
|  | @ -183,7 +182,7 @@ | |||
|         background-color: var(--bg-800); | ||||
|     } | ||||
| 
 | ||||
|     .instance-header { | ||||
|     .server-header { | ||||
|         width: 100%; | ||||
|         height: 172px; | ||||
|         display: flex; | ||||
|  | @ -196,7 +195,7 @@ | |||
|         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); | ||||
|     } | ||||
| 
 | ||||
|     .instance-icon { | ||||
|     .server-icon { | ||||
|         height: 50%; | ||||
|         border-radius: 8px; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
|     import * as api from '$lib/client/api.js'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
| 
 | ||||
|     import ReplyIcon from '$lib/../img/icons/reply.svg'; | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| <script> | ||||
|     import { client } from '../../client/client.js'; | ||||
|     import * as api from '../../client/api.js'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { parseReactions } from '$lib/post.js'; | ||||
| 
 | ||||
|     import ActionButton from './ActionButton.svelte'; | ||||
| 
 | ||||
|  | @ -18,9 +20,9 @@ | |||
|     async function toggleBoost() { | ||||
|         let data; | ||||
|         if (post.boosted) | ||||
|             data = await get(client).unboostPost(post.id); | ||||
|             data = await api.unboostPost(get(server).host, get(app).token, post.id); | ||||
|         else | ||||
|             data = await get(client).boostPost(post.id); | ||||
|             data = await api.boostPost(get(server).host, get(app).token, post.id); | ||||
|         if (!data) { | ||||
|             console.error(`Failed to boost post ${post.id}`); | ||||
|             return; | ||||
|  | @ -32,16 +34,16 @@ | |||
|     async function toggleFavourite() { | ||||
|         let data; | ||||
|         if (post.favourited) | ||||
|             data = await get(client).unfavouritePost(post.id); | ||||
|             data = await api.unfavouritePost(get(server).host, get(app).token, post.id); | ||||
|         else | ||||
|             data = await get(client).favouritePost(post.id); | ||||
|             data = await api.favouritePost(get(server).host, get(app).token, 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); | ||||
|         if (data.reactions) post.reactions = parseReactions(data.reactions); | ||||
|     } | ||||
| 
 | ||||
|     async function toggleReaction(reaction) { | ||||
|  | @ -49,16 +51,16 @@ | |||
| 
 | ||||
|         let data; | ||||
|         if (reaction.me) | ||||
|             data = await get(client).unreactPost(post.id, reaction.name); | ||||
|             data = await api.unreactPost(get(server).host, get(app).token, post.id, reaction.name); | ||||
|         else | ||||
|             data = await get(client).reactPost(post.id, reaction.name); | ||||
|             data = await api.reactPost(get(server).host, get(app).token, 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); | ||||
|         if (data.reactions) post.reactions = parseReactions(data.reactions); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| <script> | ||||
|     export let post; | ||||
| 
 | ||||
|     let rich_text; | ||||
|     post.rich_text().then(res => {rich_text = res}); | ||||
|     let open_warned = false; | ||||
| </script> | ||||
| 
 | ||||
|  | @ -22,22 +20,22 @@ | |||
|         </button> | ||||
|     {/if} | ||||
|     {#if !post.warning || open_warned} | ||||
|         {#if post.text} | ||||
|             <span class="post-text">{@html rich_text}</span> | ||||
|         {#if post.html} | ||||
|             <span class="post-text">{@html post.html}</span> | ||||
|         {/if} | ||||
|         {#if post.files && post.files.length > 0} | ||||
|             <div class="post-media-container" data-count={post.files.length}> | ||||
|                 {#each post.files as file} | ||||
|                     <div class="post-media {file.type}" on:click|stopPropagation on:mouseup|stopPropagation> | ||||
|                         {#if ["image", "gifv", "gif"].includes(file.type)} | ||||
|                             <a href={file.url} target="_blank"> | ||||
|                                 <img src={file.url} alt={file.description} title={file.description} height="200" loading="lazy" decoding="async"> | ||||
|         {#if post.media && post.media.length > 0} | ||||
|             <div class="post-media-container" data-count={post.media.length}> | ||||
|                 {#each post.media as media} | ||||
|                     <div class="post-media {media.type}" on:click|stopPropagation on:mouseup|stopPropagation> | ||||
|                         {#if ["image", "gifv", "gif"].includes(media.type)} | ||||
|                             <a href={media.url} target="_blank"> | ||||
|                                 <img src={media.url} alt={media.description} title={media.description} height="200" loading="lazy" decoding="async"> | ||||
|                             </a> | ||||
|                         {:else if file.type === "video"} | ||||
|                         {:else if media.type === "video"} | ||||
|                             <video controls height="200"> | ||||
|                                 <source src={file.url} alt={file.description} title={file.description} type={file.url.endsWith('.mp4') ? 'video/mp4' : 'video/webm'}> | ||||
|                                 <p>{file.description}   <a href={file.url}>[link]</a></p> | ||||
|                                 <!-- <media src={file.url} alt={file.description} loading="lazy" decoding="async"> --> | ||||
|                                 <source src={media.url} alt={media.description} title={media.description} type={media.url.endsWith('.mp4') ? 'video/mp4' : 'video/webm'}> | ||||
|                                 <p>{media.description}   <a href={media.url}>[link]</a></p> | ||||
|                                 <!-- <media src={media.url} alt={media.description} loading="lazy" decoding="async"> --> | ||||
|                             </video> | ||||
|                         {/if} | ||||
|                     </div> | ||||
|  |  | |||
|  | @ -1,17 +1,16 @@ | |||
| <script> | ||||
|     import { parseText as parseEmojis } from '../../emoji.js'; | ||||
|     import { shorthand as short_time } from '../../time.js'; | ||||
|     import { shorthand as short_time } from '$lib/time.js'; | ||||
| 
 | ||||
|     export let post; | ||||
| 
 | ||||
|     let time_string = post.created_at.toLocaleString(); | ||||
|     const time_string = post.created_at.toLocaleString(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-context"> | ||||
|     <span class="post-context-icon">🔁</span> | ||||
|     <span class="post-context-action"> | ||||
|         <a href={post.user.url} target="_blank"><span class="name"> | ||||
|                 {@html parseEmojis(post.user.rich_name)}</span> | ||||
|         <a href={post.account.url} target="_blank"><span class="name"> | ||||
|                 {@html post.account.rich_name}</span> | ||||
|         </a> | ||||
|         boosted this post. | ||||
|     </span> | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| <script> | ||||
|     import { parseOne as parseEmoji } from '../../emoji.js'; | ||||
|     import { play_sound } from '../../sound.js'; | ||||
|     import { onMount } from 'svelte'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|  | @ -51,7 +50,7 @@ | |||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||
|     let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at; | ||||
| </script> | ||||
| 
 | ||||
| <div class="post-container"> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| <script> | ||||
|     import { parseText as parseEmojis } from '../../emoji.js'; | ||||
|     import { shorthand as short_time } from '../../time.js'; | ||||
|     import { shorthand as short_time } from '$lib/time.js'; | ||||
| 
 | ||||
|     export let post; | ||||
|     export let reply = undefined; | ||||
|  | @ -9,13 +8,13 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class={"post-header-container" + (reply ? " reply" : "")}> | ||||
|     <a href={post.user.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation> | ||||
|         <img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||
|     <a href={post.account.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation> | ||||
|         <img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||
|     </a> | ||||
|     <header class="post-header"> | ||||
|         <div class="post-user-info" on:mouseup|stopPropagation> | ||||
|             <a href={post.user.url} target="_blank" class="name">{@html post.user.rich_name}</a> | ||||
|             <span class="username">{post.user.mention}</span> | ||||
|             <a href={post.account.url} target="_blank" class="name">{@html post.account.rich_name}</a> | ||||
|             <span class="username">{post.account.mention}</span> | ||||
|         </div> | ||||
|         <div class="post-info" on:mouseup|stopPropagation> | ||||
|             <a href={post.url} target="_blank" class="created-at"> | ||||
|  |  | |||
|  | @ -1,7 +1,4 @@ | |||
| <script> | ||||
|     import { parseText as parseEmojis, parseOne as parseEmoji } from '../../emoji.js'; | ||||
|     import { shorthand as short_time } from '../../time.js'; | ||||
|     import * as api from '../../client/api.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
| 
 | ||||
|     import PostHeader from './PostHeader.svelte'; | ||||
|  | @ -12,7 +9,7 @@ | |||
| 
 | ||||
|     export let post; | ||||
|     let time_string = post.created_at.toLocaleString(); | ||||
|     let aria_label = post.user.username + '; ' + post.text + '; ' + post.created_at; | ||||
|     let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at; | ||||
| 
 | ||||
|     let mouse_pos = { top: 0, left: 0 }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,29 +0,0 @@ | |||
| import { client } from '../client/client.js'; | ||||
| import { parseText as parseEmojis } from '../emoji.js'; | ||||
| import { get } from 'svelte/store'; | ||||
| 
 | ||||
| export default class User { | ||||
|     id; | ||||
|     nickname; | ||||
|     username; | ||||
|     host; | ||||
|     avatar_url; | ||||
|     emojis; | ||||
|     url; | ||||
| 
 | ||||
|     get name() { | ||||
|         return this.nickname || this.username; | ||||
|     } | ||||
| 
 | ||||
|     get mention() { | ||||
|         let res = "@" + this.username; | ||||
|         if (this.host != get(client).instance.host) | ||||
|             res += "@" + this.host; | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     get rich_name() { | ||||
|         if (!this.nickname) return this.username; | ||||
|         return parseEmojis(this.nickname, this.host); | ||||
|     } | ||||
| } | ||||
|  | @ -1,39 +1,42 @@ | |||
| <script> | ||||
|     import '$lib/app.css'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { account, logged_in } from '$lib/stores/account.js'; | ||||
|     import { parseAccount } from '$lib/account.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     import Navigation from '$lib/ui/Navigation.svelte'; | ||||
|     import Widgets from '$lib/ui/Widgets.svelte'; | ||||
|     import { client, Client } from '$lib/client/client.js'; | ||||
|     import { user, getUser } from '$lib/stores/user.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
| 
 | ||||
|     let ready = new Promise(resolve => { | ||||
|         if (get(client)) { | ||||
|             if (get(user)) logged_in.set(true); | ||||
|             return resolve(); | ||||
|     async function init() { | ||||
|         if (!get(app) || !get(app).token) { | ||||
|             account.set(false); | ||||
|             logged_in.set(false); | ||||
|             return; | ||||
|         } | ||||
|         let new_client = new Client(); | ||||
|         new_client.load(); | ||||
|         client.set(new_client); | ||||
| 
 | ||||
|         return getUser().then(new_user => { | ||||
|             if (!new_user) return resolve(); | ||||
|         // logged in- attempt to retrieve using token | ||||
|         const data = await api.verifyCredentials(get(server).host, get(app).token); | ||||
|         if (!data) return; | ||||
| 
 | ||||
|             logged_in.set(true); | ||||
|             user.set(new_user); | ||||
|         account.set(parseAccount(data)); | ||||
|         logged_in.set(true); | ||||
|         console.log(`Logged in as @${get(account).username}@${get(account).host}`); | ||||
| 
 | ||||
|             // spin up async task to fetch notifications | ||||
|             get(client).getNotifications( | ||||
|                 get(last_read_notif_id) | ||||
|             ).then(notif_data => { | ||||
|                 if (!notif_data) return; | ||||
|                 unread_notif_count.set(notif_data.length); | ||||
|             }); | ||||
|         // spin up async task to fetch notifications | ||||
|         const notif_data = await api.getNotifications( | ||||
|             get(server).host, | ||||
|             get(app).token, | ||||
|             get(last_read_notif_id) | ||||
|         ); | ||||
| 
 | ||||
|             return resolve(); | ||||
|         }); | ||||
|     }); | ||||
|         if (!notif_data) return; | ||||
| 
 | ||||
|         unread_notif_count.set(notif_data.length); | ||||
|     }; | ||||
| </script> | ||||
| 
 | ||||
| <div id="app"> | ||||
|  | @ -43,7 +46,7 @@ | |||
|     </header> | ||||
| 
 | ||||
|     <main> | ||||
|         {#await ready} | ||||
|         {#await init()} | ||||
|             <div class="loading throb"> | ||||
|                 <span>just a moment...</span> | ||||
|             </div> | ||||
|  |  | |||
|  | @ -1,13 +1,11 @@ | |||
| <script> | ||||
|     import { page } from '$app/stores'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import { logged_in } from '$lib/stores/account.js'; | ||||
|     import { timeline, getTimeline } from '$lib/timeline.js'; | ||||
| 
 | ||||
|     import LoginForm from '$lib/ui/LoginForm.svelte'; | ||||
|     import Feed from '$lib/ui/Feed.svelte'; | ||||
|     import User from '$lib/user/user.js'; | ||||
|     import Button from '$lib/ui/Button.svelte'; | ||||
| 
 | ||||
|     logged_in.subscribe(logged_in => { | ||||
|         if (logged_in) getTimeline(); | ||||
|  |  | |||
|  | @ -1,42 +1,48 @@ | |||
| <script> | ||||
|     import { client } from '$lib/client/client.js'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { parseAccount } from '$lib/account.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { error } from '@sveltejs/kit'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { last_read_notif_id } from '$lib/notifications.js'; | ||||
|     import { logged_in, user, getUser } from '$lib/stores/user.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
|     import { logged_in, account } from '$lib/stores/account.js'; | ||||
| 
 | ||||
|     export let data; | ||||
| 
 | ||||
|     let auth_code = data.code; | ||||
| 
 | ||||
|     if (!auth_code) { | ||||
|     if (!auth_code || !get(server) || !get(app)) { | ||||
|         error(400, { message: "Bad request" }); | ||||
|     } else { | ||||
|         get(client).getToken(auth_code).then(token => { | ||||
|         api.getToken(get(server).host, get(app).id, get(app).secret, auth_code).then(token => { | ||||
|             if (!token) { | ||||
|                 error(400, { message: "Invalid auth code provided" }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             client.update(c => { | ||||
|                 c.app.token = token; | ||||
|                 c.save(); | ||||
|                 return c; | ||||
|             app.update(app => { | ||||
|                 app.token = token; | ||||
|                 return app; | ||||
|             }); | ||||
| 
 | ||||
|             getUser().then(new_user => { | ||||
|                 if (!new_user) return; | ||||
|             api.verifyCredentials(get(server).host, get(app).token).then(data => { | ||||
|                 if (!data) return goto("/"); | ||||
| 
 | ||||
|                 account.set(parseAccount(data)); | ||||
|                 logged_in.set(true); | ||||
|                 user.set(new_user); | ||||
|                 console.log(`Logged in as @${get(account).username}@${get(account).host}`); | ||||
| 
 | ||||
|                 return get(client).getNotifications( | ||||
|                 // spin up async task to fetch notifications | ||||
|                 return api.getNotifications( | ||||
|                     get(server).host, | ||||
|                     get(app).token, | ||||
|                     get(last_read_notif_id) | ||||
|                 ).then(notif_data => { | ||||
|                     unread_notif_count.set(0); | ||||
|                     if (notif_data.constructor === Array && notif_data.length > 0) | ||||
|                         last_read_notif_id.set(notif_data[0].id); | ||||
|                     get(client).save(); | ||||
|                     goto("/"); | ||||
|                 }); | ||||
|             }); | ||||
|  |  | |||
|  | @ -1,21 +1,20 @@ | |||
| <script> | ||||
|     import { notifications, getNotifications } from '$lib/notifications.js'; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import { logged_in } from '$lib/stores/account.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { page } from '$app/stores'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import Notification from '$lib/ui/Notification.svelte'; | ||||
| 
 | ||||
|     if (!get(logged_in)) goto("/"); | ||||
| 
 | ||||
|     getNotifications(); | ||||
|     /* | ||||
|     document.addEventListener("scroll", event => { | ||||
|         if (get(logged_in) && get(page).url.pathname !== "/") return; | ||||
|         if (get(logged_in) && get(page).url.pathname !== "/notifications") return; | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getNotifications(); | ||||
|         } | ||||
|     }); | ||||
|     */ | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| <script> | ||||
|     import { client } from '$lib/client/client.js'; | ||||
|     import * as api from '$lib/client/api.js'; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import * as api from '$lib/api.js'; | ||||
|     import { logged_in } from '$lib/stores/account.js'; | ||||
|     import { server } from '$lib/client/server.js'; | ||||
|     import { app } from '$lib/client/app.js'; | ||||
|     import { parsePost } from '$lib/post.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { goto, afterNavigate } from '$app/navigation'; | ||||
|     import { base } from '$app/paths' | ||||
|  | @ -21,15 +23,15 @@ | |||
|     }) | ||||
| 
 | ||||
|     $: post = (async resolve => { | ||||
|         const post_data = await get(client).getPost(data.post_id, 0, false); | ||||
|         const post_data = await api.getPost(get(server).host, get(app).token, data.post_id); | ||||
|         if (!post_data) { | ||||
|             error = `Failed to retrieve post <code>${data.post_id}</code>.`; | ||||
|             console.error(`Failed to retrieve post ${data.post_id}.`); | ||||
|             return; | ||||
|         } | ||||
|         let post = await api.parsePost(post_data, 0, false); | ||||
|         let post = await parsePost(post_data, 0); | ||||
| 
 | ||||
|         const post_context = await get(client).getPostContext(data.post_id); | ||||
|         const post_context = await api.getPostContext(get(server).host, get(app).token, data.post_id); | ||||
| 
 | ||||
|         if (!post_context || !post_context.ancestors || !post_context.descendants) | ||||
|             return post; | ||||
|  | @ -37,16 +39,14 @@ | |||
|         // handle ancestors (above post) | ||||
|         let thread_top = post; | ||||
|         while (post_context.ancestors.length > 0) { | ||||
|             thread_top.reply = await api.parsePost(post_context.ancestors.pop(), 0, false); | ||||
|             thread_top.reply = await parsePost(post_context.ancestors.pop(), 0); | ||||
|             thread_top = thread_top.reply; | ||||
|         } | ||||
| 
 | ||||
|         // handle descendants (below post) | ||||
|         post.replies = []; | ||||
|         for (let i in post_context.descendants) { | ||||
|             post.replies.push( | ||||
|                 api.parsePost(post_context.descendants[i], 0, false) | ||||
|             ); | ||||
|             post.replies.push(parsePost(post_context.descendants[i], 0)); | ||||
|         } | ||||
| 
 | ||||
|         return post; | ||||
|  | @ -59,9 +59,9 @@ | |||
|         <nav> | ||||
|             <Button centered on:click={() => {goto(previous_page)}}>Back</Button> | ||||
|         </nav> | ||||
|         <img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async"> | ||||
|         <img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async"> | ||||
|         <h1> | ||||
|             Post by {@html post.user.rich_name} | ||||
|             Post by {@html post.account.rich_name} | ||||
|         </h1> | ||||
|     {/await} | ||||
| </header> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue