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(); | ||||
| 
 | ||||
|     post.text = data.content; | ||||
|     post.html = data.content; | ||||
| 
 | ||||
|     post.reply = null; | ||||
|     if ((data.in_reply_to_id || data.reply) && | ||||
|  | @ -278,7 +279,7 @@ export async function parseUser(data) { | |||
| 
 | ||||
|     user = new User(); | ||||
|     user.id = data.id; | ||||
|     user.nickname = data.display_name; | ||||
|     user.nickname = data.display_name.trim(); | ||||
|     user.username = data.username; | ||||
|     user.avatar_url = data.avatar; | ||||
|     user.url = data.url; | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| 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 } from '$lib/stores/user.js'; | ||||
| 
 | ||||
| export const client = writable(false); | ||||
| 
 | ||||
|  | @ -177,6 +179,7 @@ export class Client { | |||
|                 host: this.instance.host, | ||||
|                 version: this.instance.version, | ||||
|             }, | ||||
|             last_read_notif_id: get(last_read_notif_id), | ||||
|             app: this.app, | ||||
|         })); | ||||
|     } | ||||
|  | @ -191,6 +194,7 @@ export class Client { | |||
|             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; | ||||
|  |  | |||
							
								
								
									
										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 { parsePost } from '$lib/client/api.js'; | ||||
| 
 | ||||
| export let posts = writable([]); | ||||
| export let timeline = writable([]); | ||||
| 
 | ||||
| let loading = false; | ||||
| 
 | ||||
|  | @ -11,8 +11,8 @@ export async function getTimeline(clean) { | |||
|     loading = true; | ||||
| 
 | ||||
|     let timeline_data; | ||||
|     if (clean || get(posts).length === 0) timeline_data = await get(client).getTimeline() | ||||
|     else timeline_data = await get(client).getTimeline(get(posts)[get(posts).length - 1].id); | ||||
|     if (clean || get(timeline).length === 0) timeline_data = await get(client).getTimeline() | ||||
|     else timeline_data = await get(client).getTimeline(get(timeline)[get(timeline).length - 1].id); | ||||
| 
 | ||||
|     if (!timeline_data) { | ||||
|         console.error(`Failed to retrieve timeline.`); | ||||
|  | @ -20,7 +20,7 @@ export async function getTimeline(clean) { | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (clean) posts.set([]); | ||||
|     if (clean) timeline.set([]); | ||||
| 
 | ||||
|     for (let i in timeline_data) { | ||||
|         const post_data = timeline_data[i]; | ||||
|  | @ -36,7 +36,7 @@ export async function getTimeline(clean) { | |||
|             } | ||||
|             continue; | ||||
|         } | ||||
|         posts.update(current => [...current, post]); | ||||
|         timeline.update(current => [...current, post]); | ||||
|     } | ||||
|     loading = false; | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| <script> | ||||
|     import { play_sound } from '../sound.js'; | ||||
|     import { createEventDispatcher } from 'svelte'; | ||||
|     import { afterUpdate } from 'svelte'; | ||||
| 
 | ||||
|     const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|     export let active = false; | ||||
|  | @ -12,10 +14,6 @@ | |||
|     export let href = false; | ||||
| 
 | ||||
|     let classes = []; | ||||
|     if (active) classes = ["active"]; | ||||
|     if (filled) classes = ["filled"]; | ||||
|     if (disabled) classes = ["disabled"]; | ||||
|     if (centered) classes.push("centered"); | ||||
| 
 | ||||
|     function click() { | ||||
|         if (disabled) return; | ||||
|  | @ -26,6 +24,14 @@ | |||
|         play_sound(sound); | ||||
|         dispatch('click'); | ||||
|     } | ||||
| 
 | ||||
|     afterUpdate(() => { | ||||
|         classes = []; | ||||
|         if (active) classes = ["active"]; | ||||
|         if (filled) classes = ["filled"]; | ||||
|         if (disabled) classes = ["disabled"]; | ||||
|         if (centered) classes.push("centered"); | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| <button | ||||
|  |  | |||
|  | @ -1,14 +1,9 @@ | |||
| <script> | ||||
|     import Button from './Button.svelte'; | ||||
|     import Post from './post/Post.svelte'; | ||||
|     import { posts, getTimeline } from '$lib/timeline.js'; | ||||
|     import { getTimeline } from '$lib/timeline.js'; | ||||
| 
 | ||||
|     getTimeline(); | ||||
|     document.addEventListener("scroll", event => { | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getTimeline(); | ||||
|         } | ||||
|     }); | ||||
|     export let posts = []; | ||||
| </script> | ||||
| 
 | ||||
| <header> | ||||
|  | @ -26,7 +21,7 @@ | |||
|             <span>getting the feed...</span> | ||||
|         </div> | ||||
|     {/if} | ||||
|     {#each $posts as post} | ||||
|     {#each posts as post} | ||||
|         <Post post_data={post} /> | ||||
|     {/each} | ||||
| </div> | ||||
|  | @ -34,6 +29,7 @@ | |||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         height: 64px; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|  |  | |||
|  | @ -5,9 +5,12 @@ | |||
|     import { client } from '$lib/client/client.js'; | ||||
|     import { play_sound } from '$lib/sound.js'; | ||||
|     import { getTimeline } from '$lib/timeline.js'; | ||||
|     import { getNotifications } from '$lib/notifications.js'; | ||||
|     import { goto } from '$app/navigation'; | ||||
|     import { page } from '$app/stores'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { 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 NotificationsIcon from '../../img/icons/notifications.svg'; | ||||
|  | @ -21,27 +24,26 @@ | |||
|     import SettingsIcon from '../../img/icons/settings.svg'; | ||||
|     import LogoutIcon from '../../img/icons/logout.svg'; | ||||
| 
 | ||||
|     export let path; | ||||
| 
 | ||||
|     const VERSION = APP_VERSION; | ||||
|      | ||||
|     let notification_count = 0; | ||||
|     if (notification_count > 99) notification_count = "99+"; | ||||
| 
 | ||||
|     function handle_btn(name) { | ||||
|         if (!get(logged_in)) return; | ||||
|         let route; | ||||
|         switch (name) { | ||||
|             case "timeline": | ||||
|                 if (!get(client).user) break; | ||||
|                 route = "/"; | ||||
|                 getTimeline(true); | ||||
|                 break; | ||||
|             case "notifcations": | ||||
|             case "notifications": | ||||
|                 route = "/notifications"; | ||||
|                 getNotifications(); | ||||
|                 break; | ||||
|             case "explore": | ||||
|             case "lists": | ||||
|             case "favourites": | ||||
|             case "bookmarks": | ||||
|             case "hashtags": | ||||
|             default: | ||||
|                 return; | ||||
|         } | ||||
|         if (!route) return; | ||||
|  | @ -66,11 +68,11 @@ | |||
|         </div> | ||||
|     </header> | ||||
| 
 | ||||
|     {#if $logged_in} | ||||
|     <div id="nav-items"> | ||||
|         <Button label="Timeline" | ||||
|                 on:click={() => handle_btn("timeline")} | ||||
|                 active={path == "/" && $client.user} | ||||
|                 disabled={!$client.user}> | ||||
|                 active={$page.url.pathname === "/"}> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <TimelineIcon/> | ||||
|             </svelte:fragment> | ||||
|  | @ -78,14 +80,15 @@ | |||
|         </Button> | ||||
|         <Button label="Notifications" | ||||
|                 on:click={() => handle_btn("notifications")} | ||||
|                 active={path == "/notifications"} | ||||
|                 disabled> | ||||
|                 active={$page.url.pathname === "/notifications"}> | ||||
|             <svelte:fragment slot="icon"> | ||||
|                 <NotificationsIcon/> | ||||
|             </svelte:fragment> | ||||
|             Notifications | ||||
|             {#if notification_count} | ||||
|             <span class="notification-count">{notification_count}</span> | ||||
|             {#if $unread_notif_count} | ||||
|                 <span class="notification-count"> | ||||
|                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} | ||||
|                 </span> | ||||
|             {/if} | ||||
|         </Button> | ||||
|         <Button label="Explore" disabled> | ||||
|  | @ -127,7 +130,6 @@ | |||
|             </Button> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if $client.user} | ||||
|     <div id="account-items"> | ||||
|         <div class="flex-row"> | ||||
|             <Button centered label="Profile information" disabled> | ||||
|  | @ -222,6 +224,7 @@ | |||
|         transform: translate(22px, -16px); | ||||
|         min-width: 12px; | ||||
|         height: 28px; | ||||
|         margin-left: auto; | ||||
|         padding: 0 8px; | ||||
|         display: flex; | ||||
|         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 { | ||||
|         line-height: 1.2em; | ||||
|         font-size: .9em; | ||||
|         line-height: 1.45em; | ||||
|         word-wrap: break-word; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,10 +34,11 @@ | |||
| <style> | ||||
|     .post-reactions { | ||||
|         width: fit-content; | ||||
|         height: 32px; | ||||
|         min-height: 32px; | ||||
|         margin-top: 8px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         flex-wrap: wrap; | ||||
|         gap: 2px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -4,12 +4,12 @@ | |||
|     import Widgets from '$lib/ui/Widgets.svelte'; | ||||
|     import { client, Client } from '$lib/client/client.js'; | ||||
|     import { get } from 'svelte/store'; | ||||
| 
 | ||||
|     export let data; | ||||
|     $: path = data.path || "/"; | ||||
|     import { logged_in } from '$lib/stores/user.js'; | ||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||
| 
 | ||||
|     let ready = new Promise(resolve => { | ||||
|         if (get(client)) { | ||||
|             if (get(client).user) logged_in.set(true); | ||||
|             return resolve(); | ||||
|         } | ||||
|         let new_client = new Client(); | ||||
|  | @ -21,8 +21,18 @@ | |||
|                 client.set(new_client); | ||||
|                 return resolve(); | ||||
|             } | ||||
|             if (user) logged_in.set(true); | ||||
|             new_client.user = user; | ||||
|             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.user = user; | ||||
|                 return client; | ||||
|  | @ -35,7 +45,7 @@ | |||
| <div id="app"> | ||||
| 
 | ||||
|     <header> | ||||
|         <Navigation path={path} /> | ||||
|         <Navigation /> | ||||
|     </header> | ||||
| 
 | ||||
|     <main> | ||||
|  |  | |||
|  | @ -1,6 +1,2 @@ | |||
| export const prerender = true; | ||||
| export const ssr = false; | ||||
| 
 | ||||
| export async function load({ url }) { | ||||
|     return { path: url.pathname }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,14 +1,25 @@ | |||
| <script> | ||||
|     import { page } from '$app/stores'; | ||||
|     import { get } from 'svelte/store'; | ||||
|     import { client } from '$lib/client/client.js'; | ||||
|     import { timeline, getTimeline } from '$lib/timeline.js'; | ||||
| 
 | ||||
|     import LoginForm from '$lib/ui/LoginForm.svelte'; | ||||
|     import Feed from '$lib/ui/Feed.svelte'; | ||||
|     import User from '$lib/user/user.js'; | ||||
|     import Button from '$lib/ui/Button.svelte'; | ||||
| 
 | ||||
|     getTimeline(); | ||||
|     document.addEventListener("scroll", event => { | ||||
|         if (get(page).url.pathname !== "/") return; | ||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||
|             getTimeline(); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| {#if $client.user} | ||||
|     <Feed /> | ||||
|     <Feed posts={$timeline} /> | ||||
| {:else} | ||||
|     <LoginForm /> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -28,7 +28,21 @@ | |||
|                     client.user = user | ||||
|                     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 * as api from '$lib/client/api.js'; | ||||
|     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 Button from '$lib/ui/Button.svelte'; | ||||
|  | @ -13,6 +15,12 @@ | |||
|         goto("/"); | ||||
|     } | ||||
| 
 | ||||
|     let previous_page = base; | ||||
| 
 | ||||
|     afterNavigate(({from}) => { | ||||
|         previous_page = from?.url.pathname || previous_page | ||||
|     }) | ||||
| 
 | ||||
|     $: post = (async resolve => { | ||||
|         const post_data = await get(client).getPost(data.post_id, 0, false); | ||||
|         if (!post_data) { | ||||
|  | @ -49,16 +57,19 @@ | |||
| {#if !error} | ||||
| <header> | ||||
|     {#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} | ||||
|     <nav> | ||||
|         <Button centered>Back</Button> | ||||
|     </nav> | ||||
| </header> | ||||
| 
 | ||||
| <div id="feed" role="feed"> | ||||
|     {#await post} | ||||
|         <div class="throb"> | ||||
|         <div class="loading throb"> | ||||
|             <span>loading post...</span> | ||||
|         </div> | ||||
|     {:then post} | ||||
|  | @ -78,11 +89,19 @@ | |||
| <style> | ||||
|     header { | ||||
|         width: 100%; | ||||
|         height: 64px; | ||||
|         margin: 16px 0 8px 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|     } | ||||
| 
 | ||||
|     header .header-avatar { | ||||
|         width: 40px; | ||||
|         height: 40px; | ||||
|         margin: auto 0; | ||||
|         border-radius: 4px; | ||||
|     } | ||||
| 
 | ||||
|     header h1 { | ||||
|         margin: auto auto auto 8px; | ||||
|         font-size: 1.5em; | ||||
|  | @ -92,7 +111,7 @@ | |||
|     } | ||||
| 
 | ||||
|     header nav { | ||||
|         margin-left: auto; | ||||
|         margin-right: 8px; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 8px; | ||||
|  | @ -102,7 +121,7 @@ | |||
|         margin-bottom: 20vh; | ||||
|     } | ||||
| 
 | ||||
|     .throb { | ||||
|     .loading { | ||||
|         width: 100%; | ||||
|         height: 80vh; | ||||
|         display: flex; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue