Thread viewer for Bluesky
1<script lang="ts">
2 import PostComponent from '../components/posts/PostComponent.svelte';
3 import SearchPage from './SearchPage.svelte';
4 import { Post } from '../models/posts';
5 import { TimelineSearch } from '../services/timeline_search.js';
6 import { numberOfDays } from '../utils.js';
7
8 let timeRangeDays = $state(7);
9 let progressMax: number | undefined = $state();
10 let progress: number | undefined = $state();
11 let fetchInProgress = $derived(progress !== undefined);
12 let daysFetched: number | undefined = $state();
13
14 let query = $state('');
15 let results: Post[] = $state([]);
16
17 let timelineSearch = new TimelineSearch();
18
19 async function startScan(e: Event) {
20 e.preventDefault();
21
22 try {
23 if (!fetchInProgress) {
24 progressMax = timeRangeDays;
25 progress = 0;
26
27 await timelineSearch.fetchTimeline(timeRangeDays, (p) => { progress = p });
28
29 daysFetched = progress;
30 progress = undefined;
31 } else {
32 progress = undefined;
33 timelineSearch.abortFetch();
34 }
35 } catch (error) {
36 if (error.name !== 'AbortError') {
37 throw error;
38 }
39 }
40 }
41
42 function onKeyPress(e: KeyboardEvent) {
43 if (e.key == 'Enter') {
44 e.preventDefault();
45
46 let q = query.trim().toLowerCase();
47 results = timelineSearch.searchPosts(q);
48 }
49 }
50</script>
51
52<SearchPage>
53 <h2>Timeline search</h2>
54
55 <div class="timeline-search">
56 <form onsubmit={startScan}>
57 <p>
58 Fetch timeline posts: <input id="timeline_search_range" type="range" min="1" max="60" bind:value={timeRangeDays}>
59 <label for="timeline_search_range">{numberOfDays(timeRangeDays)}</label>
60 </p>
61
62 <p>
63 <input type="submit" value="{fetchInProgress ? 'Cancel' : 'Fetch timeline'}">
64
65 {#if fetchInProgress}
66 <progress max={progressMax} value={progress}></progress>
67 {/if}
68 </p>
69 </form>
70
71 {#if daysFetched}
72 <p class="archive-status">
73 Timeline archive fetched: {numberOfDays(Math.round(daysFetched))}
74 </p>
75 {/if}
76
77 <hr>
78 </div>
79
80 {#if daysFetched}
81 <form class="search-form">
82 <p class="search">
83 Search:
84 <input type="text" class="search-query" autocomplete="off" onkeydown={onKeyPress} bind:value={query}>
85 </p>
86 </form>
87
88 <div class="results">
89 {#each results as post (post.uri)}
90 <PostComponent {post} placement="feed" />
91 {/each}
92 </div>
93 {/if}
94</SearchPage>
95
96<style>
97 input[type="range"] {
98 width: 250px;
99 vertical-align: middle;
100 }
101</style>