fork
Configure Feed
Select the types of activity you want to include in your feed.
Thread viewer for Bluesky
fork
Configure Feed
Select the types of activity you want to include in your feed.
1<script lang="ts">
2 import UserAutocomplete, { type AutocompleteUser } from '../components/UserAutocomplete.svelte';
3 import PostingStatsTable, { type TableOptions } from '../components/PostingStatsTable.svelte';
4 import { accountAPI } from '../api.js';
5 import { PostingStats, type PostingStatsResult } from '../services/posting_stats.js';
6 import { numberOfDays } from '../utils.js';
7
8 const tabs = [
9 { id: 'home', title: 'Home timeline' },
10 { id: 'list', title: 'List feed' },
11 { id: 'users', title: 'Selected users' },
12 { id: 'you', title: 'Your profile' }
13 ] as const;
14
15 let lists: json[] = $state([]);
16
17 let timeRangeDays = $state(7);
18 let selectedTab: typeof tabs[number]['id'] = $state(tabs[0].id);
19 let selectedUsers: AutocompleteUser[] = $state([]);
20 let selectedList: string | undefined = $state();
21
22 let scanInProgress = $state(false);
23 let requestedDays: number | undefined = $state();
24 let progress: number | undefined = $state();
25 let scanInfo = $state();
26
27 let tableOptions: TableOptions = $state({});
28 let results: PostingStatsResult | null = $state(null);
29
30 let scanner = new PostingStats((p) => { progress = Math.max(progress || 0, p) });
31
32 $effect(() => {
33 fetchLists();
34 })
35
36 function onTabChange() {
37 results = null;
38 }
39
40 async function fetchLists() {
41 let result = await accountAPI.loadUserLists();
42
43 lists = result.sort((a, b) => {
44 let aName = a.name.toLocaleLowerCase();
45 let bName = b.name.toLocaleLowerCase();
46
47 return aName.localeCompare(bName);
48 });
49
50 selectedList = lists[0]?.uri;
51 }
52
53 async function onsubmit(e: Event) {
54 e.preventDefault();
55
56 try {
57 if (!scanInProgress) {
58 await runScan();
59 } else {
60 scanInProgress = false;
61 scanner.abortScan();
62 }
63 } catch (error) {
64 if (error.name !== 'AbortError') {
65 throw error;
66 }
67 }
68 }
69
70 async function runScan() {
71 if ((selectedTab == 'list' && !selectedList) || (selectedTab == 'users' && selectedUsers.length == 0)) {
72 return;
73 }
74
75 scanInfo = undefined;
76 results = null;
77 requestedDays = timeRangeDays;
78 progress = 0;
79 scanInProgress = true;
80
81 let startTime = new Date().getTime();
82 let data: PostingStatsResult | null;
83 let options: TableOptions;
84
85 if (selectedTab == 'home') {
86 options = {};
87 data = await scanner.scanHomeTimeline(requestedDays);
88 } else if (selectedTab == 'list') {
89 options = { showReposts: false };
90 data = await scanner.scanListTimeline(selectedList!, requestedDays);
91 } else if (selectedTab == 'users') {
92 options = { showTotal: false, showPercentages: false };
93 data = await scanner.scanUserTimelines(selectedUsers, requestedDays);
94 } else { // selectedTab == 'you'
95 options = { showTotal: false, showPercentages: false };
96 data = await scanner.scanYourTimeline(requestedDays);
97 }
98
99 let now = new Date().getTime();
100
101 if (now - startTime < 150) {
102 // artificial UI delay in case scan finishes immediately
103 await new Promise(resolve => setTimeout(resolve, 150));
104 }
105
106 tableOptions = options;
107 results = data;
108 scanInProgress = false;
109 }
110</script>
111
112<main>
113<h2>Bluesky posting statistics</h2>
114
115<form {onsubmit}>
116 <p>
117 Scan posts from:
118
119 {#each tabs as tab}
120 <input type="radio" name="scan_type" id="scan_type_{tab.id}" value="{tab.id}" bind:group={selectedTab} onclick={onTabChange}>
121 <label for="scan_type_{tab.id}">{tab.title}</label>
122 {/each}
123 </p>
124
125 <p>
126 Time range: <input id="posting_stats_range" type="range" min="1" max="60" bind:value={timeRangeDays}>
127 <label for="posting_stats_range">{numberOfDays(timeRangeDays)}</label>
128 </p>
129
130 {#if selectedTab == 'list'}
131 <p class="list-choice">
132 <label for="posting_stats_list">Select list:</label>
133 <select id="posting_stats_list" name="scan_list" bind:value={selectedList}>
134 {#each lists as list}
135 <option value={list.uri}>{list.name} </option>
136 {/each}
137 </select>
138 </p>
139 {/if}
140
141 {#if selectedTab == 'users'}
142 <UserAutocomplete bind:selectedUsers />
143 {/if}
144
145 <p>
146 <input type="submit" value="{!scanInProgress ? 'Start scan' : 'Cancel'}">
147
148 {#if scanInProgress}
149 <progress max={requestedDays} value={progress}></progress>
150 {/if}
151 </p>
152</form>
153
154{#if scanInfo}
155 <p class="scan-info">{scanInfo}</p>
156{/if}
157
158{#if results}
159 <PostingStatsTable {...tableOptions} {...results} />
160{/if}
161</main>
162
163<style>
164 input[type="radio"] {
165 position: relative;
166 top: -1px;
167 margin-left: 5px;
168 }
169
170 input[type="radio"] + label {
171 user-select: none;
172 -webkit-user-select: none;
173 margin-right: 4px;
174 }
175
176 input[type="range"] {
177 width: 250px;
178 vertical-align: middle;
179 }
180
181 input[type="submit"] {
182 font-size: 12pt;
183 margin: 5px 0px;
184 padding: 5px 10px;
185 }
186
187 select {
188 font-size: 12pt;
189 margin-left: 5px;
190 }
191
192 progress {
193 width: 300px;
194 margin-left: 10px;
195 vertical-align: middle;
196 }
197
198 .scan-info {
199 font-weight: 600;
200 line-height: 125%;
201 margin: 20px 0px;
202 }
203</style>