Thread viewer for Bluesky

moved all settings to a separate settings module

+75 -46
+5 -8
src/components/AccountMenu.svelte
··· 1 <script lang="ts"> 2 import { showLoginDialog } from '../skythread.js'; 3 import { account } from '../models/account.svelte.js'; 4 import { getBaseLocation } from '../router.js'; 5 import AccountMenuButton from './AccountMenuButton.svelte'; 6 import LoadableImage from './LoadableImage.svelte'; ··· 28 function toggleBiohazard(e: Event) { 29 e.preventDefault(); 30 31 - let hazards = document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post'); 32 - 33 - if (account.biohazardEnabled === false) { 34 - account.biohazardEnabled = true; 35 - Array.from(hazards).forEach(p => { (p as HTMLElement).style.display = 'block' }); 36 } else { 37 - account.biohazardEnabled = false; 38 - Array.from(hazards).forEach(p => { (p as HTMLElement).style.display = 'none' }); 39 } 40 } 41 ··· 94 onclick={toggleBiohazard} 95 label="Show infohazards" 96 title="Show links to blocked and hidden comments" 97 - showCheckmark={account.biohazardEnabled !== false} 98 /> 99 100 {#if !account.loggedIn}
··· 1 <script lang="ts"> 2 import { showLoginDialog } from '../skythread.js'; 3 import { account } from '../models/account.svelte.js'; 4 + import { settings } from '../models/settings.svelte.js'; 5 import { getBaseLocation } from '../router.js'; 6 import AccountMenuButton from './AccountMenuButton.svelte'; 7 import LoadableImage from './LoadableImage.svelte'; ··· 29 function toggleBiohazard(e: Event) { 30 e.preventDefault(); 31 32 + if (settings.biohazardsEnabled === false) { 33 + settings.biohazardsEnabled = true; 34 } else { 35 + settings.biohazardsEnabled = false; 36 } 37 } 38 ··· 91 onclick={toggleBiohazard} 92 label="Show infohazards" 93 title="Show links to blocked and hidden comments" 94 + showCheckmark={settings.biohazardsEnabled !== false} 95 /> 96 97 {#if !account.loggedIn}
+3 -3
src/components/BiohazardDialog.svelte
··· 1 <script lang="ts"> 2 - import { account } from '../models/account.svelte.js'; 3 4 type Props = { onConfirm?: (() => void) | undefined, onClose?: (() => void) | undefined }; 5 let { onConfirm = undefined, onClose = undefined }: Props = $props(); 6 7 function showBiohazard(e: Event) { 8 e.preventDefault(); 9 - account.biohazardEnabled = true; 10 11 onConfirm?.() 12 onClose?.(); ··· 14 15 function hideBiohazard(e: Event) { 16 e.preventDefault(); 17 - account.biohazardEnabled = false; 18 19 for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) { 20 (p as HTMLElement).style.display = 'none';
··· 1 <script lang="ts"> 2 + import { settings } from '../models/settings.svelte.js'; 3 4 type Props = { onConfirm?: (() => void) | undefined, onClose?: (() => void) | undefined }; 5 let { onConfirm = undefined, onClose = undefined }: Props = $props(); 6 7 function showBiohazard(e: Event) { 8 e.preventDefault(); 9 + settings.biohazardsEnabled = true; 10 11 onConfirm?.() 12 onClose?.(); ··· 14 15 function hideBiohazard(e: Event) { 16 e.preventDefault(); 17 + settings.biohazardsEnabled = false; 18 19 for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) { 20 (p as HTMLElement).style.display = 'none';
+2 -2
src/components/posts/BlockedPostView.svelte
··· 1 <script lang="ts"> 2 - import { account } from '../../models/account.svelte.js'; 3 import { BlockedPost, DetachedQuotePost, MissingPost, Post } from '../../models/posts.js'; 4 5 import BlockedPostContent from './BlockedPostContent.svelte'; 6 import MissingPostView from './MissingPostView.svelte'; ··· 15 16 let { reason, post, placement }: Props = $props(); 17 18 - let biohazardEnabled = $derived(account.biohazardEnabled !== false); 19 let loading = $state(false); 20 let postNotFound = $state(false); 21 let reloadedPost: Post | undefined = $state();
··· 1 <script lang="ts"> 2 import { BlockedPost, DetachedQuotePost, MissingPost, Post } from '../../models/posts.js'; 3 + import { settings } from '../../models/settings.svelte.js'; 4 5 import BlockedPostContent from './BlockedPostContent.svelte'; 6 import MissingPostView from './MissingPostView.svelte'; ··· 15 16 let { reason, post, placement }: Props = $props(); 17 18 + let biohazardEnabled = $derived(settings.biohazardsEnabled !== false); 19 let loading = $state(false); 20 let postNotFound = $state(false); 21 let reloadedPost: Post | undefined = $state();
+2 -2
src/components/posts/HiddenRepliesLink.svelte
··· 1 <script lang="ts"> 2 import { showBiohazardDialog } from '../../skythread.js'; 3 - import { account } from '../../models/account.svelte.js'; 4 import { parseThreadPost } from '../../models/posts.js'; 5 import { linkToPostThread } from '../../router.js'; 6 import { getPostContext } from './PostComponent.svelte'; ··· 17 function onLinkClick(e: Event) { 18 e.preventDefault(); 19 20 - if (account.biohazardEnabled === true) { 21 loadHiddenReplies(); 22 } else { 23 showBiohazardDialog(() => {
··· 1 <script lang="ts"> 2 import { showBiohazardDialog } from '../../skythread.js'; 3 + import { settings } from '../../models/settings.svelte.js'; 4 import { parseThreadPost } from '../../models/posts.js'; 5 import { linkToPostThread } from '../../router.js'; 6 import { getPostContext } from './PostComponent.svelte'; ··· 17 function onLinkClick(e: Event) { 18 e.preventDefault(); 19 20 + if (settings.biohazardsEnabled === true) { 21 loadHiddenReplies(); 22 } else { 23 showBiohazardDialog(() => {
+3 -3
src/components/posts/PostComponent.svelte
··· 5 <script lang="ts"> 6 import { createContext } from 'svelte'; 7 import { HiddenRepliesError } from '../../api/bluesky_api.js'; 8 - import { account } from '../../models/account.svelte.js'; 9 import { Post, BlockedPost } from '../../models/posts.js'; 10 import { Embed, InlineLinkEmbed } from '../../models/embeds.js'; 11 import { isValidURL, showError } from '../../utils.js'; ··· 60 if (reply instanceof Post) { 61 return true; 62 } else if (reply instanceof BlockedPost) { 63 - return (account.biohazardEnabled !== false); 64 } else { 65 return false; 66 } ··· 157 {#if placement == 'thread' && !repliesLoaded} 158 {#if post.hasMoreReplies} 159 <LoadMoreLink onLoad={onMoreRepliesLoaded} onError={onRepliesLoadingError} /> 160 - {:else if post.hasHiddenReplies && account.biohazardEnabled !== false} 161 <HiddenRepliesLink onLoad={onHiddenRepliesLoaded} onError={onRepliesLoadingError} /> 162 {/if} 163 {/if}
··· 5 <script lang="ts"> 6 import { createContext } from 'svelte'; 7 import { HiddenRepliesError } from '../../api/bluesky_api.js'; 8 + import { settings } from '../../models/settings.svelte.js'; 9 import { Post, BlockedPost } from '../../models/posts.js'; 10 import { Embed, InlineLinkEmbed } from '../../models/embeds.js'; 11 import { isValidURL, showError } from '../../utils.js'; ··· 60 if (reply instanceof Post) { 61 return true; 62 } else if (reply instanceof BlockedPost) { 63 + return (settings.biohazardsEnabled !== false); 64 } else { 65 return false; 66 } ··· 157 {#if placement == 'thread' && !repliesLoaded} 158 {#if post.hasMoreReplies} 159 <LoadMoreLink onLoad={onMoreRepliesLoaded} onError={onRepliesLoadingError} /> 160 + {:else if post.hasHiddenReplies && settings.biohazardsEnabled !== false} 161 <HiddenRepliesLink onLoad={onHiddenRepliesLoaded} onError={onRepliesLoadingError} /> 162 {/if} 163 {/if}
+4 -24
src/models/account.svelte.ts
··· 1 import { AuthenticatedAPI } from '../api/authenticated_api.js'; 2 import { pdsEndpointForIdentifier } from '../api/identity.js'; 3 4 class Account { 5 - #isIncognito: boolean; 6 - #biohazardEnabled: boolean | undefined; 7 #loggedIn: boolean; 8 #avatarURL: string | undefined; 9 #avatarIsLoading: boolean; 10 11 constructor() { 12 - let incognito = localStorage.getItem('incognito'); 13 - let biohazard = localStorage.getItem('biohazard'); 14 - let biohazardEnabled = biohazard ? !!JSON.parse(biohazard) : undefined; 15 let accountAPI = new AuthenticatedAPI(); 16 17 - this.#isIncognito = $state(accountAPI.isLoggedIn && !!incognito); 18 - this.#biohazardEnabled = $state(biohazardEnabled); 19 this.#loggedIn = $state(accountAPI.isLoggedIn); 20 this.#avatarURL = $state(accountAPI.isLoggedIn ? accountAPI.user.avatar : undefined); 21 this.#avatarIsLoading = $state(false); 22 } 23 24 get isIncognito(): boolean { 25 - return this.#isIncognito; 26 } 27 28 toggleIncognitoMode() { 29 - if (!this.#isIncognito) { 30 - localStorage.setItem('incognito', '1'); 31 - } else { 32 - localStorage.removeItem('incognito'); 33 - } 34 - 35 location.reload(); 36 - } 37 - 38 - get biohazardEnabled(): boolean | undefined { 39 - return this.#biohazardEnabled; 40 - } 41 - 42 - set biohazardEnabled(value: boolean) { 43 - this.#biohazardEnabled = value; 44 - localStorage.setItem('biohazard', JSON.stringify(value)); 45 } 46 47 get loggedIn(): boolean { ··· 78 79 logOut() { 80 window.accountAPI.resetTokens(); 81 - localStorage.removeItem('incognito'); 82 location.reload(); 83 } 84 }
··· 1 import { AuthenticatedAPI } from '../api/authenticated_api.js'; 2 import { pdsEndpointForIdentifier } from '../api/identity.js'; 3 + import { settings } from './settings.svelte.js'; 4 5 class Account { 6 #loggedIn: boolean; 7 #avatarURL: string | undefined; 8 #avatarIsLoading: boolean; 9 10 constructor() { 11 let accountAPI = new AuthenticatedAPI(); 12 13 this.#loggedIn = $state(accountAPI.isLoggedIn); 14 this.#avatarURL = $state(accountAPI.isLoggedIn ? accountAPI.user.avatar : undefined); 15 this.#avatarIsLoading = $state(false); 16 } 17 18 get isIncognito(): boolean { 19 + return !!settings.incognitoMode; 20 } 21 22 toggleIncognitoMode() { 23 + settings.incognitoMode = !this.isIncognito; 24 location.reload(); 25 } 26 27 get loggedIn(): boolean { ··· 58 59 logOut() { 60 window.accountAPI.resetTokens(); 61 + settings.logOut(); 62 location.reload(); 63 } 64 }
+52
src/models/settings.svelte.ts
···
··· 1 + interface SettingsData { 2 + dateLocale?: string; 3 + incognito?: boolean; 4 + biohazard?: boolean; 5 + } 6 + 7 + class Settings { 8 + data: SettingsData; 9 + 10 + constructor() { 11 + let savedData = localStorage.getItem('settings'); 12 + this.data = $state(savedData ? JSON.parse(savedData) : {}); 13 + } 14 + 15 + save() { 16 + localStorage.setItem('settings', JSON.stringify(this.data)); 17 + } 18 + 19 + logOut() { 20 + delete this.data.incognito; 21 + this.save(); 22 + } 23 + 24 + get dateLocale(): string | undefined { 25 + return this.data.dateLocale; 26 + } 27 + 28 + set dateLocale(value: string) { 29 + this.data.dateLocale = value; 30 + this.save(); 31 + } 32 + 33 + get incognitoMode(): boolean | undefined { 34 + return this.data.incognito; 35 + } 36 + 37 + set incognitoMode(value: boolean) { 38 + this.data.incognito = value; 39 + this.save(); 40 + } 41 + 42 + get biohazardsEnabled(): boolean | undefined { 43 + return this.data.biohazard; 44 + } 45 + 46 + set biohazardsEnabled(value: boolean) { 47 + this.data.biohazard = value; 48 + this.save(); 49 + } 50 + } 51 + 52 + export const settings = new Settings();
+2 -1
src/pages/LycanSearchPage.svelte
··· 1 <script lang="ts"> 2 import { Post } from '../models/posts'; 3 import { Lycan } from '../services/lycan'; 4 import PostComponent from '../components/posts/PostComponent.svelte'; 5 ··· 119 if (info.progress == 1.0) { 120 importStatusLabel = `Import complete ✓`; 121 } else if (info.position) { 122 - let date = new Date(info.position).toLocaleString(window.dateLocale, { day: 'numeric', month: 'short', year: 'numeric' }); 123 importStatusLabel = `Downloaded data until: ${date}`; 124 } else if (info.status == 'requested') { 125 importStatusLabel = 'Requesting import…';
··· 1 <script lang="ts"> 2 import { Post } from '../models/posts'; 3 + import { settings } from '../models/settings.svelte'; 4 import { Lycan } from '../services/lycan'; 5 import PostComponent from '../components/posts/PostComponent.svelte'; 6 ··· 120 if (info.progress == 1.0) { 121 importStatusLabel = `Import complete ✓`; 122 } else if (info.position) { 123 + let date = new Date(info.position).toLocaleString(settings.dateLocale, { day: 'numeric', month: 'short', year: 'numeric' }); 124 importStatusLabel = `Downloaded data until: ${date}`; 125 } else if (info.status == 'requested') { 126 importStatusLabel = 'Requesting import…';
-1
src/skythread.js
··· 26 27 28 function init() { 29 - window.dateLocale = localStorage.getItem('locale') || undefined; 30 window.avatarPreloader = buildAvatarPreloader(); 31 32 svelte.mount(AccountMenu, { target: $id('account_menu_wrap') });
··· 26 27 28 function init() { 29 window.avatarPreloader = buildAvatarPreloader(); 30 31 svelte.mount(AccountMenu, { target: $id('account_menu_wrap') });
-1
src/types.d.ts
··· 1 interface Window { 2 - dateLocale: string | undefined; 3 root: AnyPost; 4 subtreeRoot: AnyPost; 5 init: () => void;
··· 1 interface Window { 2 root: AnyPost; 3 subtreeRoot: AnyPost; 4 init: () => void;
+2 -1
src/utils/post_presenter.ts
··· 1 import { sameDay } from '../utils.js'; 2 import { Post } from '../models/posts.js'; 3 4 export class PostPresenter { 5 ··· 34 35 get formattedTimestamp() { 36 let timeFormat = this.timeFormatForTimestamp; 37 - return this.post.createdAt.toLocaleString(window.dateLocale, timeFormat); 38 } 39 }
··· 1 import { sameDay } from '../utils.js'; 2 import { Post } from '../models/posts.js'; 3 + import { settings } from '../models/settings.svelte.js'; 4 5 export class PostPresenter { 6 ··· 35 36 get formattedTimestamp() { 37 let timeFormat = this.timeFormatForTimestamp; 38 + return this.post.createdAt.toLocaleString(settings.dateLocale, timeFormat); 39 } 40 }