Skip to content

Commit 1113cac

Browse files
committed
rework following
1 parent 8251c27 commit 1113cac

File tree

5 files changed

+594
-26
lines changed

5 files changed

+594
-26
lines changed

src/lib/components/Sidebar.svelte

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -177,33 +177,30 @@
177177
return subs;
178178
});
179179
180-
// Sort followed users: 1) with unread shares, 2) followed in-app, 3) others (by DID)
180+
// Sort followed users: only Skyreader (in-app) follows, sorted by unread shares then by DID
181181
let sortedFollowedUsers = $derived(() => {
182182
const counts = sharerCounts();
183-
let users = [...socialStore.followedUsers].sort((a, b) => {
184-
const countA = counts.get(a.did) || 0;
185-
const countB = counts.get(b.did) || 0;
186-
const hasUnreadA = countA > 0;
187-
const hasUnreadB = countB > 0;
188-
189-
// Tier 1: accounts with unread shares
190-
if (hasUnreadA && !hasUnreadB) return -1;
191-
if (!hasUnreadA && hasUnreadB) return 1;
192-
193-
// Within unread tier, sort by count descending
194-
if (hasUnreadA && hasUnreadB) {
195-
return countB - countA;
196-
}
197-
198-
// Tier 2: accounts followed in-app
199-
const aIsInApp = a.source === 'inapp' || a.source === 'both';
200-
const bIsInApp = b.source === 'inapp' || b.source === 'both';
201-
if (aIsInApp && !bIsInApp) return -1;
202-
if (!aIsInApp && bIsInApp) return 1;
203-
204-
// Tier 3: by DID (stable sort - profiles are fetched async in UserItem)
205-
return a.did.localeCompare(b.did);
206-
});
183+
// Filter to only Skyreader follows (source 'inapp' or 'both')
184+
let users = [...socialStore.followedUsers]
185+
.filter((u) => u.source === 'inapp' || u.source === 'both')
186+
.sort((a, b) => {
187+
const countA = counts.get(a.did) || 0;
188+
const countB = counts.get(b.did) || 0;
189+
const hasUnreadA = countA > 0;
190+
const hasUnreadB = countB > 0;
191+
192+
// Tier 1: accounts with unread shares
193+
if (hasUnreadA && !hasUnreadB) return -1;
194+
if (!hasUnreadA && hasUnreadB) return 1;
195+
196+
// Within unread tier, sort by count descending
197+
if (hasUnreadA && hasUnreadB) {
198+
return countB - countA;
199+
}
200+
201+
// Tier 2: by DID (stable sort - profiles are fetched async in UserItem)
202+
return a.did.localeCompare(b.did);
203+
});
207204
if (sidebarStore.showOnlyUnread.shared) {
208205
users = users.filter((u) => (counts.get(u.did) || 0) > 0);
209206
}
@@ -394,6 +391,9 @@
394391
onLabelClick={() => selectFilter('following')}
395392
onUnreadToggle={() => sidebarStore.toggleShowOnlyUnread('shared')}
396393
>
394+
<a href="/following" class="manage-link" onclick={() => sidebarStore.closeMobile()}>
395+
Manage
396+
</a>
397397
{@const allUsers = sortedFollowedUsers()}
398398
{@const displayedUsers = allUsers.slice(0, 10)}
399399
{#each displayedUsers as user (user.did)}
@@ -650,6 +650,21 @@
650650
color: var(--color-text-secondary);
651651
}
652652
653+
.manage-link {
654+
display: block;
655+
padding: 0.5rem 1.5rem;
656+
font-size: 0.8125rem;
657+
color: var(--color-text-secondary);
658+
text-decoration: none;
659+
transition: background-color 0.15s;
660+
border-radius: 8px;
661+
}
662+
663+
.manage-link:hover {
664+
background-color: var(--color-bg-hover, rgba(0, 0, 0, 0.05));
665+
color: var(--color-primary);
666+
}
667+
653668
.add-feed-item {
654669
display: flex;
655670
align-items: center;

src/lib/services/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
DiscoverUser,
33
FeedItem,
4+
FollowedUserDetailed,
45
GroupedShare,
56
ParsedFeed,
67
ReshareActivity,
@@ -178,6 +179,22 @@ class ApiClient {
178179
return this.fetch(`/api/social/following${query ? `?${query}` : ''}`);
179180
}
180181

182+
async getFollowingDetailed(
183+
source: 'skyreader' | 'bluesky',
184+
limit = 50,
185+
offset = 0
186+
): Promise<{
187+
users: FollowedUserDetailed[];
188+
nextOffset: number | null;
189+
}> {
190+
const params = new URLSearchParams({
191+
source,
192+
limit: limit.toString(),
193+
offset: offset.toString(),
194+
});
195+
return this.fetch(`/api/social/following-detailed?${params}`);
196+
}
197+
181198
async getDiscoverUsers(limit = 20): Promise<{ users: DiscoverUser[] }> {
182199
const params = new URLSearchParams({ limit: limit.toString() });
183200
return this.fetch(`/api/discover?${params}`);

src/lib/stores/social.svelte.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { api } from '$lib/services/api';
33
import { profileService } from '$lib/services/profiles';
44
import { syncQueue, type FollowPayload } from '$lib/services/sync-queue';
55
import { syncStore } from './sync.svelte';
6-
import type { DiscoverUser, SocialShare } from '$lib/types';
6+
import type { DiscoverUser, FollowedUserDetailed, SocialShare } from '$lib/types';
77
import { generateTid } from '$lib/utils/tid';
88

99
export interface FollowedUser {
@@ -16,8 +16,14 @@ function createSocialStore() {
1616
let popularShares = $state<(SocialShare & { shareCount: number })[]>([]);
1717
let followedUsers = $state<FollowedUser[]>([]);
1818
let discoverUsers = $state<DiscoverUser[]>([]);
19+
let skyreaderFollows = $state<FollowedUserDetailed[]>([]);
20+
let blueskyFollows = $state<FollowedUserDetailed[]>([]);
21+
let skyreaderFollowsNextOffset = $state<number | null>(null);
22+
let blueskyFollowsNextOffset = $state<number | null>(null);
1923
let isLoadingFeed = $state(false);
2024
let isLoadingUsers = $state(false);
25+
let isLoadingSkyreaderFollows = $state(false);
26+
let isLoadingBlueskyFollows = $state(false);
2127
let isDiscoverLoading = $state(false);
2228
let isSyncing = $state(false);
2329
let cursor = $state<string | null>(null);
@@ -131,6 +137,60 @@ function createSocialStore() {
131137
}
132138
}
133139

140+
async function loadSkyreaderFollows(reset = true) {
141+
if (isLoadingSkyreaderFollows) return;
142+
if (!reset && skyreaderFollowsNextOffset === null) return;
143+
144+
isLoadingSkyreaderFollows = true;
145+
error = null;
146+
147+
try {
148+
const offset = reset ? 0 : (skyreaderFollowsNextOffset ?? 0);
149+
const result = await api.getFollowingDetailed('skyreader', 50, offset);
150+
151+
if (reset) {
152+
skyreaderFollows = result.users;
153+
} else {
154+
skyreaderFollows = [...skyreaderFollows, ...result.users];
155+
}
156+
skyreaderFollowsNextOffset = result.nextOffset;
157+
158+
// Prefetch profiles from Bluesky
159+
profileService.prefetch(result.users.map((u) => u.did));
160+
} catch (e) {
161+
error = e instanceof Error ? e.message : 'Failed to load Skyreader follows';
162+
} finally {
163+
isLoadingSkyreaderFollows = false;
164+
}
165+
}
166+
167+
async function loadBlueskyFollows(reset = true) {
168+
if (isLoadingBlueskyFollows) return;
169+
if (!reset && blueskyFollowsNextOffset === null) return;
170+
171+
isLoadingBlueskyFollows = true;
172+
error = null;
173+
174+
try {
175+
const offset = reset ? 0 : (blueskyFollowsNextOffset ?? 0);
176+
const result = await api.getFollowingDetailed('bluesky', 50, offset);
177+
178+
if (reset) {
179+
blueskyFollows = result.users;
180+
} else {
181+
blueskyFollows = [...blueskyFollows, ...result.users];
182+
}
183+
blueskyFollowsNextOffset = result.nextOffset;
184+
185+
// Prefetch profiles from Bluesky
186+
profileService.prefetch(result.users.map((u) => u.did));
187+
} catch (e) {
188+
error = e instanceof Error ? e.message : 'Failed to load Bluesky follows';
189+
} finally {
190+
isLoadingBlueskyFollows = false;
191+
}
192+
}
193+
134194
async function followUser(did: string): Promise<boolean> {
135195
const rkey = generateTid();
136196

@@ -199,6 +259,10 @@ function createSocialStore() {
199259
popularShares = [];
200260
followedUsers = [];
201261
discoverUsers = [];
262+
skyreaderFollows = [];
263+
blueskyFollows = [];
264+
skyreaderFollowsNextOffset = null;
265+
blueskyFollowsNextOffset = null;
202266
cursor = null;
203267
hasMore = true;
204268
error = null;
@@ -221,9 +285,27 @@ function createSocialStore() {
221285
get discoverUsers() {
222286
return discoverUsers;
223287
},
288+
get skyreaderFollows() {
289+
return skyreaderFollows;
290+
},
291+
get blueskyFollows() {
292+
return blueskyFollows;
293+
},
294+
get hasMoreSkyreaderFollows() {
295+
return skyreaderFollowsNextOffset !== null;
296+
},
297+
get hasMoreBlueskyFollows() {
298+
return blueskyFollowsNextOffset !== null;
299+
},
224300
get isLoading() {
225301
return isLoading;
226302
},
303+
get isLoadingSkyreaderFollows() {
304+
return isLoadingSkyreaderFollows;
305+
},
306+
get isLoadingBlueskyFollows() {
307+
return isLoadingBlueskyFollows;
308+
},
227309
get isDiscoverLoading() {
228310
return isDiscoverLoading;
229311
},
@@ -240,6 +322,8 @@ function createSocialStore() {
240322
loadPopular,
241323
loadFollowedUsers,
242324
loadDiscoverUsers,
325+
loadSkyreaderFollows,
326+
loadBlueskyFollows,
243327
followUser,
244328
unfollowInApp,
245329
syncFollows,

src/lib/types/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,17 @@ export interface InappFollow {
193193
subjectDid: string;
194194
createdAt: string;
195195
}
196+
197+
export interface FollowedUserDetailed {
198+
did: string;
199+
source: 'bluesky' | 'inapp' | 'both';
200+
shareCount: number;
201+
lastSharedAt: string | null;
202+
followedAt: number;
203+
rkey?: string;
204+
recentShares?: Array<{
205+
itemUrl: string;
206+
itemTitle?: string;
207+
createdAt: string;
208+
}>;
209+
}

0 commit comments

Comments
 (0)