another huge commit but we have notifs now yay
This commit is contained in:
		
							parent
							
								
									015a3e65e1
								
							
						
					
					
						commit
						998e8f2517
					
				
					 17 changed files with 442 additions and 52 deletions
				
			
		|  | @ -219,6 +219,7 @@ export async function parsePost(data, ancestor_count) { | ||||||
|     let post = new Post(); |     let post = new Post(); | ||||||
| 
 | 
 | ||||||
|     post.text = data.content; |     post.text = data.content; | ||||||
|  |     post.html = data.content; | ||||||
| 
 | 
 | ||||||
|     post.reply = null; |     post.reply = null; | ||||||
|     if ((data.in_reply_to_id || data.reply) && |     if ((data.in_reply_to_id || data.reply) && | ||||||
|  | @ -278,7 +279,7 @@ export async function parseUser(data) { | ||||||
| 
 | 
 | ||||||
|     user = new User(); |     user = new User(); | ||||||
|     user.id = data.id; |     user.id = data.id; | ||||||
|     user.nickname = data.display_name; |     user.nickname = data.display_name.trim(); | ||||||
|     user.username = data.username; |     user.username = data.username; | ||||||
|     user.avatar_url = data.avatar; |     user.avatar_url = data.avatar; | ||||||
|     user.url = data.url; |     user.url = data.url; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import { Instance, server_types } from './instance.js'; | import { Instance, server_types } from './instance.js'; | ||||||
| import * as api from './api.js'; | import * as api from './api.js'; | ||||||
| import { get, writable } from 'svelte/store'; | import { get, writable } from 'svelte/store'; | ||||||
|  | import { last_read_notif_id } from '$lib/notifications.js'; | ||||||
|  | import { user } from '$lib/stores/user.js'; | ||||||
| 
 | 
 | ||||||
| export const client = writable(false); | export const client = writable(false); | ||||||
| 
 | 
 | ||||||
|  | @ -177,6 +179,7 @@ export class Client { | ||||||
|                 host: this.instance.host, |                 host: this.instance.host, | ||||||
|                 version: this.instance.version, |                 version: this.instance.version, | ||||||
|             }, |             }, | ||||||
|  |             last_read_notif_id: get(last_read_notif_id), | ||||||
|             app: this.app, |             app: this.app, | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
|  | @ -191,6 +194,7 @@ export class Client { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         this.instance = new Instance(saved.instance.host, saved.instance.version); |         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; |         this.app = saved.app; | ||||||
|         client.set(this); |         client.set(this); | ||||||
|         return true; |         return true; | ||||||
|  |  | ||||||
							
								
								
									
										40
									
								
								src/lib/notifications.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/lib/notifications.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | ||||||
|  | import { client } from '$lib/client/client.js'; | ||||||
|  | import * as api from '$lib/client/api.js'; | ||||||
|  | import { get, writable } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  | export let notifications = writable([]); | ||||||
|  | export let unread_notif_count = writable(0); | ||||||
|  | export let last_read_notif_id = writable(0); | ||||||
|  | 
 | ||||||
|  | let loading; | ||||||
|  | export async function getNotifications() { | ||||||
|  |     if (loading) return; // no spamming!!
 | ||||||
|  |     loading = true; | ||||||
|  | 
 | ||||||
|  |     api.getNotifications().then(async data => { | ||||||
|  |         if (!data || data.length <= 0) return; | ||||||
|  |         notifications.set([]); | ||||||
|  |         for (let i in data) { | ||||||
|  |             let notif = data[i]; | ||||||
|  |             notif.accounts = [ await api.parseUser(notif.account) ]; | ||||||
|  |             if (get(notifications).length > 0) { | ||||||
|  |                 let prev = get(notifications)[get(notifications).length - 1]; | ||||||
|  |                 if (notif.type === prev.type) { | ||||||
|  |                     if (prev.status && notif.status && prev.status.id === notif.status.id) { | ||||||
|  |                         notifications.update(notifications => { | ||||||
|  |                             notifications[notifications.length - 1].accounts.push(notif.accounts[0]); | ||||||
|  |                             return notifications; | ||||||
|  |                         }); | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             notif.status = await api.parsePost(notif.status, 0, false); | ||||||
|  |             notifications.update(notifications => [...notifications, notif]); | ||||||
|  |         } | ||||||
|  |         last_read_notif_id.set(data[0].id); | ||||||
|  |         unread_notif_count.set(0); | ||||||
|  |         get(client).save(); | ||||||
|  |         loading = false; | ||||||
|  |     }); | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/lib/stores/user.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/lib/stores/user.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | import { writable } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  | export let user = writable(0); | ||||||
|  | export let logged_in = writable(false); | ||||||
|  | @ -2,7 +2,7 @@ import { client } from '$lib/client/client.js'; | ||||||
| import { get, writable } from 'svelte/store'; | import { get, writable } from 'svelte/store'; | ||||||
| import { parsePost } from '$lib/client/api.js'; | import { parsePost } from '$lib/client/api.js'; | ||||||
| 
 | 
 | ||||||
| export let posts = writable([]); | export let timeline = writable([]); | ||||||
| 
 | 
 | ||||||
| let loading = false; | let loading = false; | ||||||
| 
 | 
 | ||||||
|  | @ -11,8 +11,8 @@ export async function getTimeline(clean) { | ||||||
|     loading = true; |     loading = true; | ||||||
| 
 | 
 | ||||||
|     let timeline_data; |     let timeline_data; | ||||||
|     if (clean || get(posts).length === 0) timeline_data = await get(client).getTimeline() |     if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() | ||||||
|     else timeline_data = await get(client).getTimeline(get(posts)[get(posts).length - 1].id); |     else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); | ||||||
| 
 | 
 | ||||||
|     if (!timeline_data) { |     if (!timeline_data) { | ||||||
|         console.error(`Failed to retrieve timeline.`); |         console.error(`Failed to retrieve timeline.`); | ||||||
|  | @ -20,7 +20,7 @@ export async function getTimeline(clean) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (clean) posts.set([]); |     if (clean) timeline.set([]); | ||||||
| 
 | 
 | ||||||
|     for (let i in timeline_data) { |     for (let i in timeline_data) { | ||||||
|         const post_data = timeline_data[i]; |         const post_data = timeline_data[i]; | ||||||
|  | @ -36,7 +36,7 @@ export async function getTimeline(clean) { | ||||||
|             } |             } | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|         posts.update(current => [...current, post]); |         timeline.update(current => [...current, post]); | ||||||
|     } |     } | ||||||
|     loading = false; |     loading = false; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| <script> | <script> | ||||||
|     import { play_sound } from '../sound.js'; |     import { play_sound } from '../sound.js'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     import { afterUpdate } from 'svelte'; | ||||||
|  | 
 | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|     export let active = false; |     export let active = false; | ||||||
|  | @ -12,10 +14,6 @@ | ||||||
|     export let href = false; |     export let href = false; | ||||||
| 
 | 
 | ||||||
|     let classes = []; |     let classes = []; | ||||||
|     if (active) classes = ["active"]; |  | ||||||
|     if (filled) classes = ["filled"]; |  | ||||||
|     if (disabled) classes = ["disabled"]; |  | ||||||
|     if (centered) classes.push("centered"); |  | ||||||
| 
 | 
 | ||||||
|     function click() { |     function click() { | ||||||
|         if (disabled) return; |         if (disabled) return; | ||||||
|  | @ -26,6 +24,14 @@ | ||||||
|         play_sound(sound); |         play_sound(sound); | ||||||
|         dispatch('click'); |         dispatch('click'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     afterUpdate(() => { | ||||||
|  |         classes = []; | ||||||
|  |         if (active) classes = ["active"]; | ||||||
|  |         if (filled) classes = ["filled"]; | ||||||
|  |         if (disabled) classes = ["disabled"]; | ||||||
|  |         if (centered) classes.push("centered"); | ||||||
|  |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button | <button | ||||||
|  |  | ||||||
|  | @ -1,14 +1,9 @@ | ||||||
| <script> | <script> | ||||||
|     import Button from './Button.svelte'; |     import Button from './Button.svelte'; | ||||||
|     import Post from './post/Post.svelte'; |     import Post from './post/Post.svelte'; | ||||||
|     import { posts, getTimeline } from '$lib/timeline.js'; |     import { getTimeline } from '$lib/timeline.js'; | ||||||
| 
 | 
 | ||||||
|     getTimeline(); |     export let posts = []; | ||||||
|     document.addEventListener("scroll", event => { |  | ||||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { |  | ||||||
|             getTimeline(); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|  | @ -26,7 +21,7 @@ | ||||||
|             <span>getting the feed...</span> |             <span>getting the feed...</span> | ||||||
|         </div> |         </div> | ||||||
|     {/if} |     {/if} | ||||||
|     {#each $posts as post} |     {#each posts as post} | ||||||
|         <Post post_data={post} /> |         <Post post_data={post} /> | ||||||
|     {/each} |     {/each} | ||||||
| </div> | </div> | ||||||
|  | @ -34,6 +29,7 @@ | ||||||
| <style> | <style> | ||||||
|     header { |     header { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|  |         height: 64px; | ||||||
|         margin: 16px 0 8px 0; |         margin: 16px 0 8px 0; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  |  | ||||||
|  | @ -5,9 +5,12 @@ | ||||||
|     import { client } from '$lib/client/client.js'; |     import { client } from '$lib/client/client.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 { 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 { onMount } from 'svelte'; |     import { logged_in } from '$lib/stores/user.js'; | ||||||
|  |     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||||
| 
 | 
 | ||||||
|     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'; | ||||||
|  | @ -21,27 +24,26 @@ | ||||||
|     import SettingsIcon from '../../img/icons/settings.svg'; |     import SettingsIcon from '../../img/icons/settings.svg'; | ||||||
|     import LogoutIcon from '../../img/icons/logout.svg'; |     import LogoutIcon from '../../img/icons/logout.svg'; | ||||||
| 
 | 
 | ||||||
|     export let path; |  | ||||||
| 
 |  | ||||||
|     const VERSION = APP_VERSION; |     const VERSION = APP_VERSION; | ||||||
|      |  | ||||||
|     let notification_count = 0; |  | ||||||
|     if (notification_count > 99) notification_count = "99+"; |  | ||||||
| 
 | 
 | ||||||
|     function handle_btn(name) { |     function handle_btn(name) { | ||||||
|  |         if (!get(logged_in)) return; | ||||||
|         let route; |         let route; | ||||||
|         switch (name) { |         switch (name) { | ||||||
|             case "timeline": |             case "timeline": | ||||||
|                 if (!get(client).user) break; |  | ||||||
|                 route = "/"; |                 route = "/"; | ||||||
|                 getTimeline(true); |                 getTimeline(true); | ||||||
|                 break; |                 break; | ||||||
|             case "notifcations": |             case "notifications": | ||||||
|  |                 route = "/notifications"; | ||||||
|  |                 getNotifications(); | ||||||
|  |                 break; | ||||||
|             case "explore": |             case "explore": | ||||||
|             case "lists": |             case "lists": | ||||||
|             case "favourites": |             case "favourites": | ||||||
|             case "bookmarks": |             case "bookmarks": | ||||||
|             case "hashtags": |             case "hashtags": | ||||||
|  |             default: | ||||||
|                 return; |                 return; | ||||||
|         } |         } | ||||||
|         if (!route) return; |         if (!route) return; | ||||||
|  | @ -66,11 +68,11 @@ | ||||||
|         </div> |         </div> | ||||||
|     </header> |     </header> | ||||||
| 
 | 
 | ||||||
|  |     {#if $logged_in} | ||||||
|     <div id="nav-items"> |     <div id="nav-items"> | ||||||
|         <Button label="Timeline" |         <Button label="Timeline" | ||||||
|                 on:click={() => handle_btn("timeline")} |                 on:click={() => handle_btn("timeline")} | ||||||
|                 active={path == "/" && $client.user} |                 active={$page.url.pathname === "/"}> | ||||||
|                 disabled={!$client.user}> |  | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <TimelineIcon/> |                 <TimelineIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|  | @ -78,14 +80,15 @@ | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Notifications" |         <Button label="Notifications" | ||||||
|                 on:click={() => handle_btn("notifications")} |                 on:click={() => handle_btn("notifications")} | ||||||
|                 active={path == "/notifications"} |                 active={$page.url.pathname === "/notifications"}> | ||||||
|                 disabled> |  | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <NotificationsIcon/> |                 <NotificationsIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Notifications |             Notifications | ||||||
|             {#if notification_count} |             {#if $unread_notif_count} | ||||||
|             <span class="notification-count">{notification_count}</span> |                 <span class="notification-count"> | ||||||
|  |                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} | ||||||
|  |                 </span> | ||||||
|             {/if} |             {/if} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Explore" disabled> |         <Button label="Explore" disabled> | ||||||
|  | @ -127,7 +130,6 @@ | ||||||
|             </Button> |             </Button> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     {#if $client.user} |  | ||||||
|     <div id="account-items"> |     <div id="account-items"> | ||||||
|         <div class="flex-row"> |         <div class="flex-row"> | ||||||
|             <Button centered label="Profile information" disabled> |             <Button centered label="Profile information" disabled> | ||||||
|  | @ -222,6 +224,7 @@ | ||||||
|         transform: translate(22px, -16px); |         transform: translate(22px, -16px); | ||||||
|         min-width: 12px; |         min-width: 12px; | ||||||
|         height: 28px; |         height: 28px; | ||||||
|  |         margin-left: auto; | ||||||
|         padding: 0 8px; |         padding: 0 8px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|  |  | ||||||
							
								
								
									
										227
									
								
								src/lib/ui/Notification.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								src/lib/ui/Notification.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,227 @@ | ||||||
|  | <script> | ||||||
|  |     import * as api from '$lib/client/api.js'; | ||||||
|  | 
 | ||||||
|  |     import ReplyIcon from '$lib/../img/icons/reply.svg'; | ||||||
|  |     import RepostIcon from '$lib/../img/icons/repost.svg'; | ||||||
|  |     import FavouriteIcon from '$lib/../img/icons/like.svg'; | ||||||
|  |     import ReactIcon from '$lib/../img/icons/react.svg'; | ||||||
|  |     import QuoteIcon from '$lib/../img/icons/quote.svg'; | ||||||
|  |     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; | ||||||
|  |     import ActionBar from '$lib/ui/post/ActionBar.svelte'; | ||||||
|  | 
 | ||||||
|  |     let mention = (accounts) => { | ||||||
|  |         let res = `<a href=${account.url}>${account.rich_name}</a>`; | ||||||
|  |         if (accounts.length > 1) res += ` and <strong>${accounts.length - 1}</strong> others`; | ||||||
|  |         return res; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     export let data; | ||||||
|  |     let activity_text = function (type) { | ||||||
|  |         switch (type) { | ||||||
|  |             case "mention": | ||||||
|  |                 return `%1 mentioned you.`; | ||||||
|  |             case "reblog": | ||||||
|  |                 return `%1 boosted your post.`; | ||||||
|  |             case "follow": | ||||||
|  |                 return `%1 followed you.`; | ||||||
|  |             case "follow_request": | ||||||
|  |                 return `%1 requested to follow you.`; | ||||||
|  |             case "favourite": | ||||||
|  |                 return `%1 favourited your post.`; | ||||||
|  |             case "poll": | ||||||
|  |                 return `%1's poll as ended.`; | ||||||
|  |             case "update": | ||||||
|  |                 return `%1 updated their post.`; | ||||||
|  |             default: | ||||||
|  |                 return `%1 poked you!`; | ||||||
|  |         } | ||||||
|  |     }(data.type); | ||||||
|  | 
 | ||||||
|  |     let account = data.accounts[0]; | ||||||
|  |     $: accounts_short = data.accounts.slice(0, 3).reverse(); | ||||||
|  |      | ||||||
|  |     let aria_label = function () { | ||||||
|  |         if (accounts.length == 1) | ||||||
|  |             return activity_text.replace("%1", account.username) + ' ' + new Date(data.created_at); | ||||||
|  |         else | ||||||
|  |             return activity_text.replace("%1", `${account.username} and ${accounts.length - 1} others`) + ' ' + new Date(data.created_at); | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <a class="notification" href={`/post/${data.status.id}`} aria-label={aria_label}> | ||||||
|  |     <header aria-hidden> | ||||||
|  |         <span class="notif-icon"> | ||||||
|  |             {#if data.type === "favourite"} | ||||||
|  |                 <FavouriteIcon /> | ||||||
|  |             {:else if data.type === "reblog"} | ||||||
|  |                 <RepostIcon /> | ||||||
|  |             {:else if data.type === "react"} | ||||||
|  |                 <ReactIcon /> | ||||||
|  |             {:else if data.type === "mention"} | ||||||
|  |                 <ReplyIcon /> | ||||||
|  |             {:else} | ||||||
|  |                 <ReactIcon /> | ||||||
|  |             {/if} | ||||||
|  |         </span> | ||||||
|  |         <span class="notif-avatars"> | ||||||
|  |             {#if data.accounts.length == 1} | ||||||
|  |                 <a href={data.accounts[0].url} class="notif-avatar"> | ||||||
|  |                     <img src={data.accounts[0].avatar_url} alt="" width="28" height="28" /> | ||||||
|  |                 </a> | ||||||
|  |             {:else} | ||||||
|  |                 {#each accounts_short as account} | ||||||
|  |                     <img src={account.avatar_url} alt="" width="28" height="28" /> | ||||||
|  |                 {/each} | ||||||
|  |             {/if} | ||||||
|  |         </span> | ||||||
|  |         <span class="notif-activity">{@html activity_text.replace("%1", mention(data.accounts))}</span> | ||||||
|  |     </header> | ||||||
|  |     {#if data.status} | ||||||
|  |         <div class="notif-content"> | ||||||
|  |             {@html data.status.html} | ||||||
|  |         </div> | ||||||
|  |         {#if data.type === "mention"} | ||||||
|  |             {#if data.status.reactions} | ||||||
|  |                 <ReactionBar post={data.status} /> | ||||||
|  |             {/if} | ||||||
|  |             <ActionBar post={data.status} /> | ||||||
|  |         {/if} | ||||||
|  |     {/if} | ||||||
|  | </a> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     .notification { | ||||||
|  |         display: block; | ||||||
|  |         margin: 8px 0; | ||||||
|  |         padding: 16px; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         background: var(--bg-800); | ||||||
|  |         text-decoration: inherit; | ||||||
|  |         color: inherit; | ||||||
|  |         transition: background-color .1s; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notification:hover { | ||||||
|  |         background-color: color-mix(in srgb, var(--bg-800), black 5%); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header { | ||||||
|  |         width: 100%; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header .notif-icon { | ||||||
|  |         width: 28px; | ||||||
|  |         height: 28px; | ||||||
|  |         display: inline-flex; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header .notif-avatars { | ||||||
|  |         display: inline-flex; | ||||||
|  |         flex-direction: row-reverse; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header .notif-avatar { | ||||||
|  |         line-height: 0; | ||||||
|  |     } | ||||||
|  |     header .notif-avatars img { | ||||||
|  |         border-radius: 4px; | ||||||
|  |     } | ||||||
|  |     header .notif-avatars img:not(:first-child) { | ||||||
|  |         box-shadow: 4px 0 8px -2px rgba(0,0,0,.33); | ||||||
|  |     } | ||||||
|  |     header .notif-avatars img:not(:last-child) { | ||||||
|  |         margin-left: -8px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header .notif-activity { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header :global(a) { | ||||||
|  |         font-weight: bold; | ||||||
|  |         color: var(--text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header :global(.emoji) { | ||||||
|  |         margin: -.2em 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content { | ||||||
|  |         margin: 16px 0 4px 0; | ||||||
|  |         font-size: 14px; | ||||||
|  |         line-height: 1.45em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(p) { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(.emoji) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 6px; | ||||||
|  |         margin-top: -10px; | ||||||
|  |         height: 24px!important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(blockquote) { | ||||||
|  |         margin: .4em 0; | ||||||
|  |         padding: .1em 0 .1em 1em; | ||||||
|  |         border-left: 4px solid #8888; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(blockquote span) { | ||||||
|  |         opacity: .5; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(code) { | ||||||
|  |         font-size: 1.2em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(pre:has(code)) { | ||||||
|  |         margin: 8px 0; | ||||||
|  |         padding: 8px; | ||||||
|  |         display: block; | ||||||
|  |         overflow-x: scroll; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         background-color: #080808; | ||||||
|  |         color: var(--accent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(pre code) { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(a) { | ||||||
|  |         color: var(--accent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(a.mention) { | ||||||
|  |         color: inherit; | ||||||
|  |         font-weight: 600; | ||||||
|  |         padding: 3px 6px; | ||||||
|  |         background: var(--bg-700); | ||||||
|  |         border-radius: 6px; | ||||||
|  |         text-decoration: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(a.mention:hover) { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(a.hashtag) { | ||||||
|  |         background-color: transparent; | ||||||
|  |         padding: 0; | ||||||
|  |         font-style: italic; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notif-content :global(.mention-avatar) { | ||||||
|  |         position: relative; | ||||||
|  |         top: 4px; | ||||||
|  |         height: 20px; | ||||||
|  |         margin-right: 4px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -84,7 +84,8 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-text { |     .post-text { | ||||||
|         line-height: 1.2em; |         font-size: .9em; | ||||||
|  |         line-height: 1.45em; | ||||||
|         word-wrap: break-word; |         word-wrap: break-word; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,10 +34,11 @@ | ||||||
| <style> | <style> | ||||||
|     .post-reactions { |     .post-reactions { | ||||||
|         width: fit-content; |         width: fit-content; | ||||||
|         height: 32px; |         min-height: 32px; | ||||||
|         margin-top: 8px; |         margin-top: 8px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|  |         flex-wrap: wrap; | ||||||
|         gap: 2px; |         gap: 2px; | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -4,12 +4,12 @@ | ||||||
|     import Widgets from '$lib/ui/Widgets.svelte'; |     import Widgets from '$lib/ui/Widgets.svelte'; | ||||||
|     import { client, Client } from '$lib/client/client.js'; |     import { client, Client } from '$lib/client/client.js'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
| 
 |     import { logged_in } from '$lib/stores/user.js'; | ||||||
|     export let data; |     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||||
|     $: path = data.path || "/"; |  | ||||||
| 
 | 
 | ||||||
|     let ready = new Promise(resolve => { |     let ready = new Promise(resolve => { | ||||||
|         if (get(client)) { |         if (get(client)) { | ||||||
|  |             if (get(client).user) logged_in.set(true); | ||||||
|             return resolve(); |             return resolve(); | ||||||
|         } |         } | ||||||
|         let new_client = new Client(); |         let new_client = new Client(); | ||||||
|  | @ -21,8 +21,18 @@ | ||||||
|                 client.set(new_client); |                 client.set(new_client); | ||||||
|                 return resolve(); |                 return resolve(); | ||||||
|             } |             } | ||||||
|  |             if (user) logged_in.set(true); | ||||||
|             new_client.user = user; |             new_client.user = user; | ||||||
|             window.peekie = new_client; |             window.peekie = new_client; | ||||||
|  | 
 | ||||||
|  |             // spin up async task to fetch notifications | ||||||
|  |             get(client).getNotifications( | ||||||
|  |                 get(last_read_notif_id) | ||||||
|  |             ).then(notif_data => { | ||||||
|  |                 if (!notif_data) return; | ||||||
|  |                 unread_notif_count.set(notif_data.length); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|             client.update(client => { |             client.update(client => { | ||||||
|                 client.user = user; |                 client.user = user; | ||||||
|                 return client; |                 return client; | ||||||
|  | @ -35,7 +45,7 @@ | ||||||
| <div id="app"> | <div id="app"> | ||||||
| 
 | 
 | ||||||
|     <header> |     <header> | ||||||
|         <Navigation path={path} /> |         <Navigation /> | ||||||
|     </header> |     </header> | ||||||
| 
 | 
 | ||||||
|     <main> |     <main> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,2 @@ | ||||||
| export const prerender = true; | export const prerender = true; | ||||||
| export const ssr = false; | export const ssr = false; | ||||||
| 
 |  | ||||||
| export async function load({ url }) { |  | ||||||
|     return { path: url.pathname }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,14 +1,25 @@ | ||||||
| <script> | <script> | ||||||
|  |     import { page } from '$app/stores'; | ||||||
|  |     import { get } from 'svelte/store'; | ||||||
|     import { client } from '$lib/client/client.js'; |     import { client } from '$lib/client/client.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 User from '$lib/user/user.js'; | ||||||
|     import Button from '$lib/ui/Button.svelte'; |     import Button from '$lib/ui/Button.svelte'; | ||||||
|  | 
 | ||||||
|  |     getTimeline(); | ||||||
|  |     document.addEventListener("scroll", event => { | ||||||
|  |         if (get(page).url.pathname !== "/") return; | ||||||
|  |         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|  |             getTimeline(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $client.user} | {#if $client.user} | ||||||
|     <Feed /> |     <Feed posts={$timeline} /> | ||||||
| {:else} | {:else} | ||||||
|     <LoginForm /> |     <LoginForm /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -28,7 +28,21 @@ | ||||||
|                     client.user = user |                     client.user = user | ||||||
|                     return client; |                     return client; | ||||||
|                 }); |                 }); | ||||||
|                 goto("/"); | 
 | ||||||
|  |                 return get(client).getNotifications( | ||||||
|  |                     get(last_read_notification_id) | ||||||
|  |                 ).then(notif_data => { | ||||||
|  |                     client.update(client => { | ||||||
|  |                         // we've just logged in, so assume all past notifications are read. | ||||||
|  |                         // i *would* just use the mastodon marker API to get the last read | ||||||
|  |                         // notification, but this does not appear to be widely supported. | ||||||
|  |                         if (notif_data.constructor === Array && notif_data.length > 0) | ||||||
|  |                             last_read_notification_id.set(notif_data[0].id); | ||||||
|  |                         client.save(); | ||||||
|  |                         return client; | ||||||
|  |                     }); | ||||||
|  |                     goto("/"); | ||||||
|  |                 }); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										57
									
								
								src/routes/notifications/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/routes/notifications/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | <script> | ||||||
|  |     import { notifications, getNotifications } from '$lib/notifications.js'; | ||||||
|  |     import Notification from '$lib/ui/Notification.svelte'; | ||||||
|  | 
 | ||||||
|  |     getNotifications(); | ||||||
|  |     /* | ||||||
|  |     document.addEventListener("scroll", event => { | ||||||
|  |         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|  |             getNotifications(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     */ | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <header> | ||||||
|  |     <h1>Notifications</h1> | ||||||
|  | </header> | ||||||
|  | 
 | ||||||
|  | <div class="notifications"> | ||||||
|  |     {#if $notifications.length === 0} | ||||||
|  |         <div class="loading throb"> | ||||||
|  |             <span>fetching notifications...</span> | ||||||
|  |         </div> | ||||||
|  |     {:else} | ||||||
|  |         {#each $notifications as notif} | ||||||
|  |             <Notification data={notif} /> | ||||||
|  |         {/each} | ||||||
|  |     {/if} | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     header { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 64px; | ||||||
|  |         margin: 16px 0 8px 0; | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     h1 { | ||||||
|  |         font-size: 1.5em; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .notifications { | ||||||
|  |         margin: 16px 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .loading { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 80vh; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -2,6 +2,8 @@ | ||||||
|     import { client } from '$lib/client/client.js'; |     import { client } from '$lib/client/client.js'; | ||||||
|     import * as api from '$lib/client/api.js'; |     import * as api from '$lib/client/api.js'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|  |     import { goto, afterNavigate } from '$app/navigation'; | ||||||
|  |     import { base } from '$app/paths' | ||||||
| 
 | 
 | ||||||
|     import Post from '$lib/ui/post/Post.svelte'; |     import Post from '$lib/ui/post/Post.svelte'; | ||||||
|     import Button from '$lib/ui/Button.svelte'; |     import Button from '$lib/ui/Button.svelte'; | ||||||
|  | @ -13,6 +15,12 @@ | ||||||
|         goto("/"); |         goto("/"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     let previous_page = base; | ||||||
|  | 
 | ||||||
|  |     afterNavigate(({from}) => { | ||||||
|  |         previous_page = from?.url.pathname || previous_page | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|     $: post = (async resolve => { |     $: post = (async resolve => { | ||||||
|         const post_data = await get(client).getPost(data.post_id, 0, false); |         const post_data = await get(client).getPost(data.post_id, 0, false); | ||||||
|         if (!post_data) { |         if (!post_data) { | ||||||
|  | @ -49,16 +57,19 @@ | ||||||
| {#if !error} | {#if !error} | ||||||
| <header> | <header> | ||||||
|     {#await post then post} |     {#await post then post} | ||||||
|         <h1>Post by {@html post.user.rich_name}</h1> |         <nav> | ||||||
|  |             <Button centered on:click={() => {goto(previous_page)}}>Back</Button> | ||||||
|  |         </nav> | ||||||
|  |         <img src={post.user.avatar_url} type={post.user.avatar_type} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async"> | ||||||
|  |         <h1> | ||||||
|  |             Post by {@html post.user.rich_name} | ||||||
|  |         </h1> | ||||||
|     {/await} |     {/await} | ||||||
|     <nav> |  | ||||||
|         <Button centered>Back</Button> |  | ||||||
|     </nav> |  | ||||||
| </header> | </header> | ||||||
| 
 | 
 | ||||||
| <div id="feed" role="feed"> | <div id="feed" role="feed"> | ||||||
|     {#await post} |     {#await post} | ||||||
|         <div class="throb"> |         <div class="loading throb"> | ||||||
|             <span>loading post...</span> |             <span>loading post...</span> | ||||||
|         </div> |         </div> | ||||||
|     {:then post} |     {:then post} | ||||||
|  | @ -78,11 +89,19 @@ | ||||||
| <style> | <style> | ||||||
|     header { |     header { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|  |         height: 64px; | ||||||
|         margin: 16px 0 8px 0; |         margin: 16px 0 8px 0; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     header .header-avatar { | ||||||
|  |         width: 40px; | ||||||
|  |         height: 40px; | ||||||
|  |         margin: auto 0; | ||||||
|  |         border-radius: 4px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     header h1 { |     header h1 { | ||||||
|         margin: auto auto auto 8px; |         margin: auto auto auto 8px; | ||||||
|         font-size: 1.5em; |         font-size: 1.5em; | ||||||
|  | @ -92,7 +111,7 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     header nav { |     header nav { | ||||||
|         margin-left: auto; |         margin-right: 8px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         gap: 8px; |         gap: 8px; | ||||||
|  | @ -102,7 +121,7 @@ | ||||||
|         margin-bottom: 20vh; |         margin-bottom: 20vh; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .throb { |     .loading { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 80vh; |         height: 80vh; | ||||||
|         display: flex; |         display: flex; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue