Add follows view with followers/following tabs and new follower badges

Followers tab fetches kind 3 events referencing the user, following tab
shows the contact list. Each row has avatar, NIP-05 badge, follow/unfollow
button, and "follows you" indicator. New follower notifications from the
background poller increment a sidebar badge that clears on view open.
This commit is contained in:
Jure
2026-03-25 10:21:18 +01:00
parent 4e04ad38c3
commit 255faefbdc
8 changed files with 224 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ interface NotificationsState {
currentPubkey: string | null;
dmLastSeen: Record<string, number>;
dmUnreadCount: number;
newFollowersCount: number;
fetchNotifications: (pubkey: string) => Promise<void>;
markRead: (eventId: string) => void;
@@ -21,6 +22,8 @@ interface NotificationsState {
isRead: (eventId: string) => boolean;
markDMRead: (partnerPubkey: string) => void;
computeDMUnread: (conversations: Array<{ partnerPubkey: string; lastAt: number }>) => void;
incrementNewFollowers: () => void;
clearNewFollowers: () => void;
}
function loadReadIds(): Set<string> {
@@ -54,6 +57,7 @@ export const useNotificationsStore = create<NotificationsState>((set, get) => ({
currentPubkey: null,
dmLastSeen: loadDMLastSeen(),
dmUnreadCount: 0,
newFollowersCount: 0,
fetchNotifications: async (pubkey: string) => {
const state = get();
@@ -120,4 +124,7 @@ export const useNotificationsStore = create<NotificationsState>((set, get) => ({
const dmUnreadCount = unreadConvos.length;
set({ dmUnreadCount });
},
incrementNewFollowers: () => set((s) => ({ newFollowersCount: s.newFollowersCount + 1 })),
clearNewFollowers: () => set({ newFollowersCount: 0 }),
}));

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
import { NDKEvent } from "@nostr-dev-kit/ndk";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "article" | "articles" | "media" | "podcasts" | "about" | "zaps" | "dm" | "notifications" | "bookmarks" | "hashtag";
type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "article" | "articles" | "media" | "podcasts" | "about" | "zaps" | "dm" | "notifications" | "bookmarks" | "hashtag" | "follows";
type FeedTab = "global" | "following" | "trending";
interface ViewStackEntry {
@@ -29,9 +29,11 @@ interface UIState {
showHelp: boolean;
showDebugPanel: boolean;
feedLanguageFilter: string | null;
followsTab: "followers" | "following";
fontSize: number;
themeId: string;
setView: (view: View) => void;
setFollowsTab: (tab: "followers" | "following") => void;
setFeedTab: (tab: FeedTab) => void;
openProfile: (pubkey: string) => void;
openThread: (note: NDKEvent, from?: View) => void;
@@ -68,10 +70,12 @@ export const useUIStore = create<UIState>((set, _get) => ({
showHelp: false,
showDebugPanel: false,
feedLanguageFilter: null,
followsTab: "followers",
fontSize: parseInt(localStorage.getItem(FONT_SIZE_KEY) || "14", 10),
themeId: localStorage.getItem(THEME_KEY) || "midnight",
setView: (currentView) => set({ currentView }),
setFeedTab: (feedTab) => set({ feedTab }),
setFollowsTab: (followsTab) => set({ followsTab }),
openProfile: (pubkey) => set((s) => {
const stack = [...s.viewStack, { view: s.currentView, selectedNote: s.selectedNote, selectedPubkey: s.selectedPubkey }].slice(-MAX_STACK);
return { currentView: "profile", selectedPubkey: pubkey, previousView: s.currentView as View, viewStack: stack };