appview-less bluesky client
24
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 131 lines 3.8 kB view raw
1<script lang="ts"> 2 import { settings, type SavedFeed } from '$lib/settings'; 3 import { fetchFeedGenerator, type FeedGenerator } from '$lib/at/feeds'; 4 import { router, viewClient } from '$lib/state.svelte'; 5 import Dropdown from './Dropdown.svelte'; 6 import Icon from '@iconify/svelte'; 7 8 interface Props { 9 selectedFeed: string | null; 10 onSelect: (feedUri: string | null) => void; 11 } 12 13 let { selectedFeed = $bindable(), onSelect }: Props = $props(); 14 15 let isOpen = $state(false); 16 17 const savedFeeds = $derived($settings.feeds); 18 const sortedFeeds = $derived( 19 [...savedFeeds].sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)) 20 ); 21 22 let feedMeta = $state<Map<string, FeedGenerator>>(new Map()); 23 24 $effect(() => { 25 for (const savedFeed of savedFeeds) { 26 if (!feedMeta.has(savedFeed.feed.uri)) { 27 fetchFeedGenerator(viewClient, savedFeed.feed.uri).then((meta) => { 28 if (meta) feedMeta.set(savedFeed.feed.uri, meta); 29 }); 30 } 31 } 32 }); 33 34 const getDisplayName = (uri: string) => { 35 const meta = feedMeta.get(uri); 36 return meta?.displayName ?? uri.split('/').pop() ?? 'Feed'; 37 }; 38 39 const selectedName = $derived(selectedFeed === null ? 'replies' : getDisplayName(selectedFeed)); 40</script> 41 42{#snippet feedIcon({ 43 avatar, 44 replies, 45 following 46}: { 47 avatar?: string; 48 replies?: boolean; 49 following?: boolean; 50})} 51 {#if replies} 52 <Icon icon="heroicons:chat-bubble-left-ellipsis-16-solid" width="20" /> 53 {:else if following} 54 <Icon icon="heroicons:users-solid" width="20" /> 55 {:else if avatar} 56 <img src={avatar} alt="" class="h-5 w-5 shrink-0 rounded-sm object-cover" /> 57 {:else} 58 <Icon icon="heroicons:rss" width="20" /> 59 {/if} 60{/snippet} 61 62<Dropdown 63 class="min-w-48 rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl" 64 bind:isOpen 65 placement="bottom-start" 66> 67 {#snippet trigger()} 68 <button 69 onclick={() => (isOpen = !isOpen)} 70 class="flex action-button items-center gap-1.5 p-2 text-sm hover:scale-102!" 71 > 72 {@render feedIcon({ 73 replies: selectedFeed === null, 74 following: selectedFeed === 'following', 75 avatar: selectedFeed ? feedMeta.get(selectedFeed)?.avatar : undefined 76 })} 77 <span>{selectedName}</span> 78 <span class="opacity-50"></span> 79 </button> 80 {/snippet} 81 <div class="flex flex-col p-1"> 82 <button 83 onclick={() => { 84 onSelect(null); 85 isOpen = false; 86 }} 87 class="my-0.5 flex items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-(--nucleus-fg)/10 {selectedFeed === 88 null 89 ? 'bg-(--nucleus-accent)/20' 90 : ''}" 91 > 92 {@render feedIcon({ replies: true })} 93 <span>replies</span> 94 </button> 95 <button 96 onclick={() => { 97 onSelect('following'); 98 isOpen = false; 99 }} 100 class="my-0.5 flex items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-(--nucleus-fg)/10 {selectedFeed === 101 'following' 102 ? 'bg-(--nucleus-accent)/20' 103 : ''}" 104 > 105 {@render feedIcon({ following: true })} 106 <span>following</span> 107 </button> 108 {#each sortedFeeds as savedFeed (savedFeed.feed.uri)} 109 <button 110 onclick={() => { 111 onSelect(savedFeed.feed.uri); 112 isOpen = false; 113 }} 114 class="my-0.5 flex items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-(--nucleus-fg)/10 {selectedFeed === 115 savedFeed.feed.uri 116 ? 'bg-(--nucleus-accent)/20' 117 : ''}" 118 > 119 {@render feedIcon({ avatar: savedFeed.feed.avatar })} 120 <span class="truncate">{savedFeed.feed.displayName}</span> 121 </button> 122 {/each} 123 {#if sortedFeeds.length === 0} 124 <a 125 href="/settings/feeds" 126 onclick={(e) => (e.preventDefault(), (isOpen = false), router.navigate('/settings/feeds'))} 127 class="px-3 py-2 text-sm opacity-70">add feeds in settings</a 128 > 129 {/if} 130 </div> 131</Dropdown>