Mark new followers with badge, sort to top of follows list

Track new follower pubkeys in notification store. When user opens
the follows view, new followers are highlighted with an accent "new"
badge and sorted to the top. Badge clears on view open (one-time).
This commit is contained in:
Jure
2026-04-01 19:23:23 +02:00
parent 6ae795e48d
commit 51a010bfc5
3 changed files with 25 additions and 7 deletions

View File

@@ -11,9 +11,11 @@ import { shortenPubkey, profileName } from "../../lib/utils";
function FollowRow({
pubkey,
followsYou,
isNew,
}: {
pubkey: string;
followsYou?: boolean;
isNew?: boolean;
}) {
const profile = useProfile(pubkey);
const name = profileName(profile, shortenPubkey(pubkey));
@@ -60,6 +62,9 @@ function FollowRow({
{followsYou && (
<span className="text-[9px] text-text-dim bg-bg-raised px-1.5 py-0.5 rounded-sm">follows you</span>
)}
{isNew && (
<span className="text-[9px] text-accent bg-accent/15 px-1.5 py-0.5 rounded-sm font-medium">new</span>
)}
</div>
</div>
@@ -82,12 +87,14 @@ function FollowRow({
export function FollowsView() {
const { followsTab, setFollowsTab } = useUIStore();
const { pubkey, follows } = useUserStore();
const { clearNewFollowers } = useNotificationsStore();
const { newFollowerPubkeys, clearNewFollowers } = useNotificationsStore();
const [followers, setFollowers] = useState<string[]>([]);
const [followersLoading, setFollowersLoading] = useState(false);
const [followersError, setFollowersError] = useState<string | null>(null);
const [followersFetched, setFollowersFetched] = useState(false);
// Snapshot new follower pubkeys on mount, before clearing
const [newPubkeys] = useState(() => new Set(newFollowerPubkeys));
// Clear badge when view opens
useEffect(() => {
@@ -191,8 +198,14 @@ export function FollowsView() {
{!followersLoading && !followersError && followers.length === 0 && followersFetched && (
<p className="px-4 py-8 text-text-dim text-[12px] text-center">No followers found yet.</p>
)}
{followers.map((pk) => (
<FollowRow key={pk} pubkey={pk} />
{[...followers]
.sort((a, b) => {
const aNew = newPubkeys.has(a) ? 1 : 0;
const bNew = newPubkeys.has(b) ? 1 : 0;
return bNew - aNew; // new followers first
})
.map((pk) => (
<FollowRow key={pk} pubkey={pk} isNew={newPubkeys.has(pk)} />
))}
</>
)}

View File

@@ -96,7 +96,7 @@ async function pollOnce(pubkey: string) {
for (const e of newFollowers) {
const name = await getProfileName(e.pubkey);
notifyFollower(name).catch(() => {});
useNotificationsStore.getState().incrementNewFollowers();
useNotificationsStore.getState().addNewFollower(e.pubkey);
}
}
} catch { /* non-critical */ }

View File

@@ -17,6 +17,7 @@ interface NotificationsState {
dmLastSeen: Record<string, number>;
dmUnreadCount: number;
newFollowersCount: number;
newFollowerPubkeys: Set<string>;
loadFromDb: (pubkey: string) => Promise<void>;
fetchNotifications: (pubkey: string) => Promise<void>;
@@ -25,7 +26,7 @@ interface NotificationsState {
isRead: (eventId: string) => boolean;
markDMRead: (partnerPubkey: string) => void;
computeDMUnread: (conversations: Array<{ partnerPubkey: string; lastAt: number }>) => void;
incrementNewFollowers: () => void;
addNewFollower: (pubkey: string) => void;
clearNewFollowers: () => void;
}
@@ -76,6 +77,7 @@ export const useNotificationsStore = create<NotificationsState>((set, get) => ({
dmLastSeen: loadDMLastSeen(),
dmUnreadCount: 0,
newFollowersCount: 0,
newFollowerPubkeys: new Set<string>(),
loadFromDb: async (pubkey: string) => {
const isNewAccount = pubkey !== get().currentPubkey;
@@ -210,6 +212,9 @@ export const useNotificationsStore = create<NotificationsState>((set, get) => ({
set({ dmUnreadCount });
},
incrementNewFollowers: () => set((s) => ({ newFollowersCount: s.newFollowersCount + 1 })),
clearNewFollowers: () => set({ newFollowersCount: 0 }),
addNewFollower: (pubkey: string) => set((s) => ({
newFollowersCount: s.newFollowersCount + 1,
newFollowerPubkeys: new Set([...s.newFollowerPubkeys, pubkey]),
})),
clearNewFollowers: () => set({ newFollowersCount: 0, newFollowerPubkeys: new Set() }),
}));