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'; | 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 function parseEmoji(shortcode, url) { | ||||||
| export const EMOJI_NAME_REGEX = /:[\w\-.]{0,32}:/g; |     let emoji = { shortcode, url }; | ||||||
| 
 |     if (emoji.shortcode == '❤') emoji.shortcode = '❤️'; // stupid heart unicode
 | ||||||
| export default class Emoji { |     emoji.html = `<img src="${emoji.url}" class="emoji" height="20" title="${emoji.shortcode}" alt="${emoji.shortcode}"/>`; | ||||||
|     name; |     return emoji; | ||||||
|     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 parseText(text, host) { | export function renderEmoji(text, emoji_list) { | ||||||
|     if (!text) return text; |     if (!text) return text; | ||||||
|      |      | ||||||
|     let index = text.search(EMOJI_NAME_REGEX); |     let index = text.search(EMOJI_REGEX); | ||||||
|     if (index === -1) return text; |     if (index === -1) return text; | ||||||
| 
 | 
 | ||||||
|     // find the emoji name
 |     // find the closing comma
 | ||||||
|     let length = text.substring(index + 1).search(':'); |     let length = text.substring(index + 1).search(':'); | ||||||
|     if (length <= 0) return text; |     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) { |     // see if emoji is valid
 | ||||||
|         return text.substring(0, index) + emoji.html + |     let shortcode = text.substring(index + 1, index + length + 1); | ||||||
|             parseText(text.substring(index + length + 2), host); |     let emoji = emoji_list[shortcode]; | ||||||
|     } |     let replace = emoji ? emoji.html : shortcode; | ||||||
|     return text.substring(0, index + length + 1) + |  | ||||||
|         parseText(text.substring(index + length + 1), host); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export function parseOne(emoji_id) { |     return text.substring(0, index) + replace + renderEmoji(text.substring(index + length + 2), emoji_list); | ||||||
|     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; |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,22 +1,72 @@ | ||||||
| import { client } from '$lib/client/client.js'; | import * as api from '$lib/api.js'; | ||||||
| import * as api from '$lib/client/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 { 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([]); | const prefix = app_name + '_notif_'; | ||||||
| export let unread_notif_count = writable(0); | 
 | ||||||
| export let last_read_notif_id = writable(0); | 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; | let loading; | ||||||
| export async function getNotifications() { | export async function getNotifications(clean) { | ||||||
|     if (loading) return; // no spamming!!
 |     if (loading) return; // no spamming!!
 | ||||||
|     loading = true; |     loading = true; | ||||||
| 
 | 
 | ||||||
|     api.getNotifications().then(async data => { |     let last_id = false; | ||||||
|         if (!data || data.length <= 0) return; |     if (!clean && get(notifications).length > 0) | ||||||
|         notifications.set([]); |         last_id = get(notifications)[get(notifications).length - 1].id; | ||||||
|         for (let i in data) { | 
 | ||||||
|             let notif = data[i]; |     const notif_data = await api.getNotifications( | ||||||
|             notif.accounts = [ await api.parseUser(notif.account) ]; |         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) { |         if (get(notifications).length > 0) { | ||||||
|             let prev = get(notifications)[get(notifications).length - 1]; |             let prev = get(notifications)[get(notifications).length - 1]; | ||||||
|             if (notif.type === prev.type) { |             if (notif.type === prev.type) { | ||||||
|  | @ -29,12 +79,10 @@ export async function getNotifications() { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|             notif.status = notif.status ? await api.parsePost(notif.status, 0, false) : null; |         notif.status = notif.status ? await parsePost(notif.status, 0, false) : null; | ||||||
|         notifications.update(notifications => [...notifications, notif]); |         notifications.update(notifications => [...notifications, notif]); | ||||||
|     } |     } | ||||||
|         last_read_notif_id.set(data[0].id); |     if (!last_id) last_read_notif_id.set(notif_data[0].id); | ||||||
|         unread_notif_count.set(0); |     if (!last_id) unread_notif_count.set(0); | ||||||
|         get(client).save(); |  | ||||||
|     loading = false; |     loading = false; | ||||||
|     }); |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										251
									
								
								src/lib/post.js
									
										
									
									
									
								
							
							
						
						
									
										251
									
								
								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 { | const cache = writable({}); | ||||||
|     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; |  | ||||||
| 
 | 
 | ||||||
|     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 | ||||||
|     /* |  | ||||||
|     async rich_text() { |  | ||||||
|         let text = this.text; |  | ||||||
|         if (!text) return text; |  | ||||||
|         let client = Client.get(); |  | ||||||
| 
 |  | ||||||
|         const markdown_tokens = [ |  | ||||||
|             { tag: "pre", token: "```" }, |  | ||||||
|             { tag: "code", token: "`" }, |  | ||||||
|             { tag: "strong", token: "**" }, |  | ||||||
|             { tag: "strong", token: "__" }, |  | ||||||
|             { tag: "em", token: "*" }, |  | ||||||
|             { tag: "em", token: "_" }, |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         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); |  | ||||||
| 
 |  | ||||||
|             // handle newlines
 |  | ||||||
|             if (md_nostack && sample.startsWith('\n')) { |  | ||||||
|                 response += "<br>"; |  | ||||||
|                 index++; |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // 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++; |  | ||||||
| 
 |  | ||||||
|                 let mention = text.substring(index, index + length); |  | ||||||
| 
 |  | ||||||
|                 // 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; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // 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; |  | ||||||
|     } |  | ||||||
|  */ |  */ | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     post.boost = data.reblog ? await parsePost(data.reblog, 1, false) : null; | ||||||
|  | 
 | ||||||
|  |     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; | ||||||
|  | 
 | ||||||
|  |     post.emojis = []; | ||||||
|  |     data.emojis.forEach(emoji => { | ||||||
|  |         post.emojis[emoji.shortcode] = parseEmoji(emoji.shortcode, emoji.url); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (data.reactions) post.reactions = parseReactions(data.reactions); | ||||||
|  | 
 | ||||||
|  |     post.rich_text = renderEmoji(post.html, post.emojis); | ||||||
|  | 
 | ||||||
|  |     return post; | ||||||
|  | 
 | ||||||
|  |     // 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;
 | ||||||
|  |     //     });
 | ||||||
|  |     // }
 | ||||||
|  | 
 | ||||||
|  |     // 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 { 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; | let loading = false; | ||||||
| 
 | 
 | ||||||
|  | @ -10,9 +12,16 @@ export async function getTimeline(clean) { | ||||||
|     if (loading) return; // no spamming!!
 |     if (loading) return; // no spamming!!
 | ||||||
|     loading = true; |     loading = true; | ||||||
| 
 | 
 | ||||||
|     let timeline_data; |     let last_post = false; | ||||||
|     if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() |     if (!clean && get(timeline).length > 0) | ||||||
|     else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); |         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) { |     if (!timeline_data) { | ||||||
|         console.error(`Failed to retrieve timeline.`); |         console.error(`Failed to retrieve timeline.`); | ||||||
|  | @ -24,7 +33,7 @@ export async function getTimeline(clean) { | ||||||
| 
 | 
 | ||||||
|     for (let i in timeline_data) { |     for (let i in timeline_data) { | ||||||
|         const post_data = timeline_data[i]; |         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) { | ||||||
|             if (post === null || post === undefined) { |             if (post === null || post === undefined) { | ||||||
|                 if (post_data.id) { |                 if (post_data.id) { | ||||||
|  |  | ||||||
|  | @ -1,37 +1,42 @@ | ||||||
| <script> | <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 { get } from 'svelte/store'; | ||||||
| 
 | 
 | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import Logo from '$lib/../img/campfire-logo.svg'; | ||||||
| 
 | 
 | ||||||
|     let instance_url_error = false; |     let display_error = false; | ||||||
|     let logging_in = false; |     let logging_in = false; | ||||||
| 
 | 
 | ||||||
|     function log_in(event) { |     async function log_in(event) { | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|         instance_url_error = false; |         display_error = false; | ||||||
| 
 | 
 | ||||||
|         logging_in = true; |         logging_in = true; | ||||||
|         const host = event.target.host.value; |         const host = event.target.host.value; | ||||||
| 
 | 
 | ||||||
|         if (!host || host === "") { |         if (!host || host === "") { | ||||||
|             instance_url_error = "Please enter an instance domain."; |             display_error = "Please enter an server domain."; | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         console.log(client); |         server.set(await createServer(host)); | ||||||
| 
 |         if (!get(server)) { | ||||||
|         get(client).init(host).then(res => { |             display_error = "Failed to connect to the server.\nCheck the browser console for details!" | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             if (!res) return; |  | ||||||
|             if (res.constructor === String) { |  | ||||||
|                 instance_url_error = res; |  | ||||||
|             return; |             return; | ||||||
|             }; |         } | ||||||
|             let oauth_url = get(client).getOAuthUrl(); | 
 | ||||||
|             location = oauth_url; |         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> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -40,11 +45,11 @@ | ||||||
|         <Logo /> |         <Logo /> | ||||||
|     </div> |     </div> | ||||||
|     <p>Welcome, fediverse user!</p> |     <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"> |     <div class="input-wrapper"> | ||||||
|         <input type="text" id="host" aria-label="instance domain" class={logging_in ? "throb" : ""}> |         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> | ||||||
|         {#if instance_url_error} |         {#if display_error} | ||||||
|             <p class="error">{instance_url_error}</p> |             <p class="error">{display_error}</p> | ||||||
|         {/if} |         {/if} | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
|  |  | ||||||
|  | @ -1,18 +1,17 @@ | ||||||
| <script> | <script> | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import { account, logged_in } from '$lib/stores/account.js'; | ||||||
|     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 { play_sound } from '$lib/sound.js'; |     import { play_sound } from '$lib/sound.js'; | ||||||
|     import { getTimeline } from '$lib/timeline.js'; |     import { getTimeline } from '$lib/timeline.js'; | ||||||
|     import { getNotifications } from '$lib/notifications.js'; |     import { getNotifications } from '$lib/notifications.js'; | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { get } from 'svelte/store'; |     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 { 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 TimelineIcon from '../../img/icons/timeline.svg'; | ||||||
|     import NotificationsIcon from '../../img/icons/notifications.svg'; |     import NotificationsIcon from '../../img/icons/notifications.svg'; | ||||||
|     import ExploreIcon from '../../img/icons/explore.svg'; |     import ExploreIcon from '../../img/icons/explore.svg'; | ||||||
|  | @ -37,7 +36,7 @@ | ||||||
|                 break; |                 break; | ||||||
|             case "notifications": |             case "notifications": | ||||||
|                 route = "/notifications"; |                 route = "/notifications"; | ||||||
|                 getNotifications(); |                 getNotifications(true); | ||||||
|                 break; |                 break; | ||||||
|             case "explore": |             case "explore": | ||||||
|             case "lists": |             case "lists": | ||||||
|  | @ -63,7 +62,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div id="navigation"> | <div id="navigation"> | ||||||
|     <header class="instance-header"> |     <header class="server-header"> | ||||||
|         <div class="app-logo"> |         <div class="app-logo"> | ||||||
|             <Logo /> |             <Logo /> | ||||||
|         </div> |         </div> | ||||||
|  | @ -151,11 +150,11 @@ | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div id="account-button"> |         <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"> |             <div class="account-name" aria-hidden="true"> | ||||||
|                 <a href={$user.url} class="nickname" title={$user.nickname}>{@html $user.rich_name}</a> |                 <a href={$account.url} class="nickname" title={$account.nickname}>{@html $account.rich_name}</a> | ||||||
|                 <span class="username" title={`@${$user.username}@${$user.host}`}> |                 <span class="username" title={`@${$account.username}@${$account.host}`}> | ||||||
|                     {`@${$user.username}@${$user.host}`} |                     {`@${$account.username}@${$account.host}`} | ||||||
|                 </span> |                 </span> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  | @ -183,7 +182,7 @@ | ||||||
|         background-color: var(--bg-800); |         background-color: var(--bg-800); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .instance-header { |     .server-header { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 172px; |         height: 172px; | ||||||
|         display: flex; |         display: flex; | ||||||
|  | @ -196,7 +195,7 @@ | ||||||
|         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); |         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .instance-icon { |     .server-icon { | ||||||
|         height: 50%; |         height: 50%; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <script> | <script> | ||||||
|     import * as api from '$lib/client/api.js'; |     import * as api from '$lib/api.js'; | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
| 
 | 
 | ||||||
|     import ReplyIcon from '$lib/../img/icons/reply.svg'; |     import ReplyIcon from '$lib/../img/icons/reply.svg'; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| <script> | <script> | ||||||
|     import { client } from '../../client/client.js'; |     import * as api from '$lib/api.js'; | ||||||
|     import * as api from '../../client/api.js'; |  | ||||||
|     import { get } from 'svelte/store'; |     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'; |     import ActionButton from './ActionButton.svelte'; | ||||||
| 
 | 
 | ||||||
|  | @ -18,9 +20,9 @@ | ||||||
|     async function toggleBoost() { |     async function toggleBoost() { | ||||||
|         let data; |         let data; | ||||||
|         if (post.boosted) |         if (post.boosted) | ||||||
|             data = await get(client).unboostPost(post.id); |             data = await api.unboostPost(get(server).host, get(app).token, post.id); | ||||||
|         else |         else | ||||||
|             data = await get(client).boostPost(post.id); |             data = await api.boostPost(get(server).host, get(app).token, post.id); | ||||||
|         if (!data) { |         if (!data) { | ||||||
|             console.error(`Failed to boost post ${post.id}`); |             console.error(`Failed to boost post ${post.id}`); | ||||||
|             return; |             return; | ||||||
|  | @ -32,16 +34,16 @@ | ||||||
|     async function toggleFavourite() { |     async function toggleFavourite() { | ||||||
|         let data; |         let data; | ||||||
|         if (post.favourited) |         if (post.favourited) | ||||||
|             data = await get(client).unfavouritePost(post.id); |             data = await api.unfavouritePost(get(server).host, get(app).token, post.id); | ||||||
|         else |         else | ||||||
|             data = await get(client).favouritePost(post.id); |             data = await api.favouritePost(get(server).host, get(app).token, post.id); | ||||||
|         if (!data) { |         if (!data) { | ||||||
|             console.error(`Failed to favourite post ${post.id}`); |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         post.favourited = data.favourited; |         post.favourited = data.favourited; | ||||||
|         post.favourite_count = data.favourites_count; |         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) { |     async function toggleReaction(reaction) { | ||||||
|  | @ -49,16 +51,16 @@ | ||||||
| 
 | 
 | ||||||
|         let data; |         let data; | ||||||
|         if (reaction.me) |         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 |         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) { |         if (!data) { | ||||||
|             console.error(`Failed to favourite post ${post.id}`); |             console.error(`Failed to favourite post ${post.id}`); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         post.favourited = data.favourited; |         post.favourited = data.favourited; | ||||||
|         post.favourite_count = data.favourites_count; |         post.favourite_count = data.favourites_count; | ||||||
|         if (data.reactions) post.reactions = api.parseReactions(data.reactions); |         if (data.reactions) post.reactions = parseReactions(data.reactions); | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| <script> | <script> | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|     let rich_text; |  | ||||||
|     post.rich_text().then(res => {rich_text = res}); |  | ||||||
|     let open_warned = false; |     let open_warned = false; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -22,22 +20,22 @@ | ||||||
|         </button> |         </button> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if !post.warning || open_warned} |     {#if !post.warning || open_warned} | ||||||
|         {#if post.text} |         {#if post.html} | ||||||
|             <span class="post-text">{@html rich_text}</span> |             <span class="post-text">{@html post.html}</span> | ||||||
|         {/if} |         {/if} | ||||||
|         {#if post.files && post.files.length > 0} |         {#if post.media && post.media.length > 0} | ||||||
|             <div class="post-media-container" data-count={post.files.length}> |             <div class="post-media-container" data-count={post.media.length}> | ||||||
|                 {#each post.files as file} |                 {#each post.media as media} | ||||||
|                     <div class="post-media {file.type}" on:click|stopPropagation on:mouseup|stopPropagation> |                     <div class="post-media {media.type}" on:click|stopPropagation on:mouseup|stopPropagation> | ||||||
|                         {#if ["image", "gifv", "gif"].includes(file.type)} |                         {#if ["image", "gifv", "gif"].includes(media.type)} | ||||||
|                             <a href={file.url} target="_blank"> |                             <a href={media.url} target="_blank"> | ||||||
|                                 <img src={file.url} alt={file.description} title={file.description} height="200" loading="lazy" decoding="async"> |                                 <img src={media.url} alt={media.description} title={media.description} height="200" loading="lazy" decoding="async"> | ||||||
|                             </a> |                             </a> | ||||||
|                         {:else if file.type === "video"} |                         {:else if media.type === "video"} | ||||||
|                             <video controls height="200"> |                             <video controls height="200"> | ||||||
|                                 <source src={file.url} alt={file.description} title={file.description} type={file.url.endsWith('.mp4') ? 'video/mp4' : 'video/webm'}> |                                 <source src={media.url} alt={media.description} title={media.description} type={media.url.endsWith('.mp4') ? 'video/mp4' : 'video/webm'}> | ||||||
|                                 <p>{file.description}   <a href={file.url}>[link]</a></p> |                                 <p>{media.description}   <a href={media.url}>[link]</a></p> | ||||||
|                                 <!-- <media src={file.url} alt={file.description} loading="lazy" decoding="async"> --> |                                 <!-- <media src={media.url} alt={media.description} loading="lazy" decoding="async"> --> | ||||||
|                             </video> |                             </video> | ||||||
|                         {/if} |                         {/if} | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
|  | @ -1,17 +1,16 @@ | ||||||
| <script> | <script> | ||||||
|     import { parseText as parseEmojis } from '../../emoji.js'; |     import { shorthand as short_time } from '$lib/time.js'; | ||||||
|     import { shorthand as short_time } from '../../time.js'; |  | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|     let time_string = post.created_at.toLocaleString(); |     const time_string = post.created_at.toLocaleString(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-context"> | <div class="post-context"> | ||||||
|     <span class="post-context-icon">🔁</span> |     <span class="post-context-icon">🔁</span> | ||||||
|     <span class="post-context-action"> |     <span class="post-context-action"> | ||||||
|         <a href={post.user.url} target="_blank"><span class="name"> |         <a href={post.account.url} target="_blank"><span class="name"> | ||||||
|                 {@html parseEmojis(post.user.rich_name)}</span> |                 {@html post.account.rich_name}</span> | ||||||
|         </a> |         </a> | ||||||
|         boosted this post. |         boosted this post. | ||||||
|     </span> |     </span> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| <script> | <script> | ||||||
|     import { parseOne as parseEmoji } from '../../emoji.js'; |  | ||||||
|     import { play_sound } from '../../sound.js'; |     import { play_sound } from '../../sound.js'; | ||||||
|     import { onMount } from 'svelte'; |     import { onMount } from 'svelte'; | ||||||
|     import { goto } from '$app/navigation'; |     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> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-container"> | <div class="post-container"> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| <script> | <script> | ||||||
|     import { parseText as parseEmojis } from '../../emoji.js'; |     import { shorthand as short_time } from '$lib/time.js'; | ||||||
|     import { shorthand as short_time } from '../../time.js'; |  | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|     export let reply = undefined; |     export let reply = undefined; | ||||||
|  | @ -9,13 +8,13 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class={"post-header-container" + (reply ? " reply" : "")}> | <div class={"post-header-container" + (reply ? " reply" : "")}> | ||||||
|     <a href={post.user.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation> |     <a href={post.account.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"> |         <img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||||
|     </a> |     </a> | ||||||
|     <header class="post-header"> |     <header class="post-header"> | ||||||
|         <div class="post-user-info" on:mouseup|stopPropagation> |         <div class="post-user-info" on:mouseup|stopPropagation> | ||||||
|             <a href={post.user.url} target="_blank" class="name">{@html post.user.rich_name}</a> |             <a href={post.account.url} target="_blank" class="name">{@html post.account.rich_name}</a> | ||||||
|             <span class="username">{post.user.mention}</span> |             <span class="username">{post.account.mention}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="post-info" on:mouseup|stopPropagation> |         <div class="post-info" on:mouseup|stopPropagation> | ||||||
|             <a href={post.url} target="_blank" class="created-at"> |             <a href={post.url} target="_blank" class="created-at"> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,4 @@ | ||||||
| <script> | <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 { goto } from '$app/navigation'; | ||||||
| 
 | 
 | ||||||
|     import PostHeader from './PostHeader.svelte'; |     import PostHeader from './PostHeader.svelte'; | ||||||
|  | @ -12,7 +9,7 @@ | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
|     let time_string = post.created_at.toLocaleString(); |     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 }; |     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> | <script> | ||||||
|     import '$lib/app.css'; |     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 Navigation from '$lib/ui/Navigation.svelte'; | ||||||
|     import Widgets from '$lib/ui/Widgets.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 => { |     async function init() { | ||||||
|         if (get(client)) { |         if (!get(app) || !get(app).token) { | ||||||
|             if (get(user)) logged_in.set(true); |             account.set(false); | ||||||
|             return resolve(); |             logged_in.set(false); | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
|         let new_client = new Client(); |  | ||||||
|         new_client.load(); |  | ||||||
|         client.set(new_client); |  | ||||||
| 
 | 
 | ||||||
|         return getUser().then(new_user => { |         // logged in- attempt to retrieve using token | ||||||
|             if (!new_user) return resolve(); |         const data = await api.verifyCredentials(get(server).host, get(app).token); | ||||||
|  |         if (!data) return; | ||||||
| 
 | 
 | ||||||
|  |         account.set(parseAccount(data)); | ||||||
|         logged_in.set(true); |         logged_in.set(true); | ||||||
|             user.set(new_user); |         console.log(`Logged in as @${get(account).username}@${get(account).host}`); | ||||||
| 
 | 
 | ||||||
|         // spin up async task to fetch notifications |         // spin up async task to fetch notifications | ||||||
|             get(client).getNotifications( |         const notif_data = await api.getNotifications( | ||||||
|  |             get(server).host, | ||||||
|  |             get(app).token, | ||||||
|             get(last_read_notif_id) |             get(last_read_notif_id) | ||||||
|             ).then(notif_data => { |         ); | ||||||
|                 if (!notif_data) return; |  | ||||||
|                 unread_notif_count.set(notif_data.length); |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             return resolve(); |         if (!notif_data) return; | ||||||
|         }); | 
 | ||||||
|     }); |         unread_notif_count.set(notif_data.length); | ||||||
|  |     }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div id="app"> | <div id="app"> | ||||||
|  | @ -43,7 +46,7 @@ | ||||||
|     </header> |     </header> | ||||||
| 
 | 
 | ||||||
|     <main> |     <main> | ||||||
|         {#await ready} |         {#await init()} | ||||||
|             <div class="loading throb"> |             <div class="loading throb"> | ||||||
|                 <span>just a moment...</span> |                 <span>just a moment...</span> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  | @ -1,13 +1,11 @@ | ||||||
| <script> | <script> | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { get } from 'svelte/store'; |     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 { timeline, getTimeline } from '$lib/timeline.js'; | ||||||
| 
 | 
 | ||||||
|     import LoginForm from '$lib/ui/LoginForm.svelte'; |     import LoginForm from '$lib/ui/LoginForm.svelte'; | ||||||
|     import Feed from '$lib/ui/Feed.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 => { |     logged_in.subscribe(logged_in => { | ||||||
|         if (logged_in) getTimeline(); |         if (logged_in) getTimeline(); | ||||||
|  |  | ||||||
|  | @ -1,42 +1,48 @@ | ||||||
| <script> | <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 { goto } from '$app/navigation'; | ||||||
|     import { error } from '@sveltejs/kit'; |     import { error } from '@sveltejs/kit'; | ||||||
|     import { get } from 'svelte/store'; |     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||||
|     import { last_read_notif_id } from '$lib/notifications.js'; |     import { logged_in, account } from '$lib/stores/account.js'; | ||||||
|     import { logged_in, user, getUser } from '$lib/stores/user.js'; |  | ||||||
| 
 | 
 | ||||||
|     export let data; |     export let data; | ||||||
| 
 | 
 | ||||||
|     let auth_code = data.code; |     let auth_code = data.code; | ||||||
| 
 | 
 | ||||||
|     if (!auth_code) { |     if (!auth_code || !get(server) || !get(app)) { | ||||||
|         error(400, { message: "Bad request" }); |         error(400, { message: "Bad request" }); | ||||||
|     } else { |     } 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) { |             if (!token) { | ||||||
|                 error(400, { message: "Invalid auth code provided" }); |                 error(400, { message: "Invalid auth code provided" }); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             client.update(c => { |             app.update(app => { | ||||||
|                 c.app.token = token; |                 app.token = token; | ||||||
|                 c.save(); |                 return app; | ||||||
|                 return c; |  | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             getUser().then(new_user => { |             api.verifyCredentials(get(server).host, get(app).token).then(data => { | ||||||
|                 if (!new_user) return; |                 if (!data) return goto("/"); | ||||||
| 
 | 
 | ||||||
|  |                 account.set(parseAccount(data)); | ||||||
|                 logged_in.set(true); |                 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) |                     get(last_read_notif_id) | ||||||
|                 ).then(notif_data => { |                 ).then(notif_data => { | ||||||
|  |                     unread_notif_count.set(0); | ||||||
|                     if (notif_data.constructor === Array && notif_data.length > 0) |                     if (notif_data.constructor === Array && notif_data.length > 0) | ||||||
|                         last_read_notif_id.set(notif_data[0].id); |                         last_read_notif_id.set(notif_data[0].id); | ||||||
|                     get(client).save(); |  | ||||||
|                     goto("/"); |                     goto("/"); | ||||||
|                 }); |                 }); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|  | @ -1,21 +1,20 @@ | ||||||
| <script> | <script> | ||||||
|     import { notifications, getNotifications } from '$lib/notifications.js'; |     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 { goto } from '$app/navigation'; | ||||||
|  |     import { page } from '$app/stores'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|     import Notification from '$lib/ui/Notification.svelte'; |     import Notification from '$lib/ui/Notification.svelte'; | ||||||
| 
 | 
 | ||||||
|     if (!get(logged_in)) goto("/"); |     if (!get(logged_in)) goto("/"); | ||||||
| 
 | 
 | ||||||
|     getNotifications(); |     getNotifications(); | ||||||
|     /* |  | ||||||
|     document.addEventListener("scroll", event => { |     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) { |         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|             getNotifications(); |             getNotifications(); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|     */ |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| <script> | <script> | ||||||
|     import { client } from '$lib/client/client.js'; |     import * as api from '$lib/api.js'; | ||||||
|     import * as api from '$lib/client/api.js'; |     import { logged_in } from '$lib/stores/account.js'; | ||||||
|     import { logged_in } from '$lib/stores/user.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 { get } from 'svelte/store'; | ||||||
|     import { goto, afterNavigate } from '$app/navigation'; |     import { goto, afterNavigate } from '$app/navigation'; | ||||||
|     import { base } from '$app/paths' |     import { base } from '$app/paths' | ||||||
|  | @ -21,15 +23,15 @@ | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     $: post = (async resolve => { |     $: 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) { |         if (!post_data) { | ||||||
|             error = `Failed to retrieve post <code>${data.post_id}</code>.`; |             error = `Failed to retrieve post <code>${data.post_id}</code>.`; | ||||||
|             console.error(`Failed to retrieve post ${data.post_id}.`); |             console.error(`Failed to retrieve post ${data.post_id}.`); | ||||||
|             return; |             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) |         if (!post_context || !post_context.ancestors || !post_context.descendants) | ||||||
|             return post; |             return post; | ||||||
|  | @ -37,16 +39,14 @@ | ||||||
|         // handle ancestors (above post) |         // handle ancestors (above post) | ||||||
|         let thread_top = post; |         let thread_top = post; | ||||||
|         while (post_context.ancestors.length > 0) { |         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; |             thread_top = thread_top.reply; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // handle descendants (below post) |         // handle descendants (below post) | ||||||
|         post.replies = []; |         post.replies = []; | ||||||
|         for (let i in post_context.descendants) { |         for (let i in post_context.descendants) { | ||||||
|             post.replies.push( |             post.replies.push(parsePost(post_context.descendants[i], 0)); | ||||||
|                 api.parsePost(post_context.descendants[i], 0, false) |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return post; |         return post; | ||||||
|  | @ -59,9 +59,9 @@ | ||||||
|         <nav> |         <nav> | ||||||
|             <Button centered on:click={() => {goto(previous_page)}}>Back</Button> |             <Button centered on:click={() => {goto(previous_page)}}>Back</Button> | ||||||
|         </nav> |         </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> |         <h1> | ||||||
|             Post by {@html post.user.rich_name} |             Post by {@html post.account.rich_name} | ||||||
|         </h1> |         </h1> | ||||||
|     {/await} |     {/await} | ||||||
| </header> | </header> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue