From d22ce56ebc97c817955a45afd6569bc4849ea9bb Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 6 May 2025 15:09:43 +0100 Subject: [PATCH] feat: follow sets page --- .../app/src/Components/Embed/PubkeyList.tsx | 52 ++++++------ .../src/Components/Event/EventComponent.tsx | 1 + .../app/src/Components/Feed/RootTabItems.tsx | 22 ++--- .../app/src/Components/SuggestedProfiles.tsx | 4 +- .../src/Components/User/FollowListBase.tsx | 21 ++--- packages/app/src/Components/kind-name.tsx | 2 + packages/app/src/Pages/Layout/Header.tsx | 34 ++++---- packages/app/src/Pages/ListFeedPage.tsx | 5 +- packages/app/src/Pages/Root/FollowSets.tsx | 80 +++++++++++++++++++ packages/app/src/Pages/Root/RootTabRoutes.tsx | 17 ++-- packages/app/src/lang.json | 27 +++++-- packages/app/src/translations/en.json | 9 ++- packages/system/src/event-kind.ts | 1 + 13 files changed, 195 insertions(+), 80 deletions(-) create mode 100644 packages/app/src/Pages/Root/FollowSets.tsx diff --git a/packages/app/src/Components/Embed/PubkeyList.tsx b/packages/app/src/Components/Embed/PubkeyList.tsx index 8f3f7c36..20f1a831 100644 --- a/packages/app/src/Components/Embed/PubkeyList.tsx +++ b/packages/app/src/Components/Embed/PubkeyList.tsx @@ -12,6 +12,8 @@ import usePreferences from "@/Hooks/usePreferences"; import { dedupe, findTag, getDisplayName, hexToBech32 } from "@/Utils"; import { useWallet } from "@/Wallet"; +import { ProxyImg } from "../ProxyImg"; + export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) { const wallet = useWallet(); const defaultZapAmount = usePreferences(s => s.defaultZapAmount); @@ -62,29 +64,33 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam } } + const picture = findTag(ev, "image"); return ( - - zapAll()}> - , - }} - /> - - - } - profilePreviewProps={{ - options: { - about: true, - }, - }} - /> + <> + {picture && } + + zapAll()}> + , + }} + /> + + + } + profilePreviewProps={{ + options: { + about: true, + }, + }} + /> + ); } diff --git a/packages/app/src/Components/Event/EventComponent.tsx b/packages/app/src/Components/Event/EventComponent.tsx index ba599b9d..8878cfb5 100644 --- a/packages/app/src/Components/Event/EventComponent.tsx +++ b/packages/app/src/Components/Event/EventComponent.tsx @@ -62,6 +62,7 @@ export default memo(function EventComponent(props: NoteProps) { case EventKind.ZapstrTrack: content = ; break; + case EventKind.StarterPackSet: case EventKind.FollowSet: case EventKind.ContactList: content = ; diff --git a/packages/app/src/Components/Feed/RootTabItems.tsx b/packages/app/src/Components/Feed/RootTabItems.tsx index e955132a..103e94de 100644 --- a/packages/app/src/Components/Feed/RootTabItems.tsx +++ b/packages/app/src/Components/Feed/RootTabItems.tsx @@ -61,17 +61,6 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr ), }, - { - tab: "suggested", - path: `${base}/suggested`, - show: Boolean(pubKey), - element: ( - <> - - - - ), - }, { tab: "trending/hashtags", path: `${base}/trending/hashtags`, @@ -105,6 +94,17 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr ), }, + { + tab: "follow-sets", + path: `${base}/follow-sets`, + show: true, + element: ( + <> + + + + ), + }, ] as Array<{ tab: RootTabRoutePath; path: string; diff --git a/packages/app/src/Components/SuggestedProfiles.tsx b/packages/app/src/Components/SuggestedProfiles.tsx index 55d82e4e..b61cee9c 100644 --- a/packages/app/src/Components/SuggestedProfiles.tsx +++ b/packages/app/src/Components/SuggestedProfiles.tsx @@ -1,4 +1,4 @@ -import { HexKey, NostrPrefix } from "@snort/system"; +import { NostrPrefix } from "@snort/system"; import { useState } from "react"; import { FormattedMessage } from "react-intl"; @@ -61,7 +61,7 @@ export default function SuggestedProfiles() { s.readonly); + const wot = useWoT(); async function followAll() { await control.addFollow(pubkeys); @@ -37,15 +38,17 @@ export default function FollowListBase({
{(showFollowAll ?? true) && (
-
{title}
+
{title}
{actions} - followAll()} disabled={login.readonly}> - + followAll()} disabled={readonly}> +
)} -
- {pubkeys?.slice(0, 20).map(a => )} +
+ {wot.sortPubkeys(pubkeys).map(a => ( + + ))}
); diff --git a/packages/app/src/Components/kind-name.tsx b/packages/app/src/Components/kind-name.tsx index 5fcc4d7c..f8d2d4e6 100644 --- a/packages/app/src/Components/kind-name.tsx +++ b/packages/app/src/Components/kind-name.tsx @@ -277,6 +277,8 @@ export default function KindName({ kind }: { kind: number }) { return ; case 38383: return ; + case 39089: + return ; case 39701: return ; default: diff --git a/packages/app/src/Pages/Layout/Header.tsx b/packages/app/src/Pages/Layout/Header.tsx index f9ad1dad..656b4458 100644 --- a/packages/app/src/Pages/Layout/Header.tsx +++ b/packages/app/src/Pages/Layout/Header.tsx @@ -1,5 +1,5 @@ -import { unwrap } from "@snort/shared"; -import { EventKind, NostrLink, NostrPrefix, parseNostrLink } from "@snort/system"; +import { Bech32Regex, unwrap } from "@snort/shared"; +import { EventKind, NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system"; import { useEventFeed } from "@snort/system-react"; import classNames from "classnames"; import React, { useCallback, useMemo } from "react"; @@ -9,24 +9,25 @@ import { useLocation, useNavigate } from "react-router-dom"; import { rootTabItems } from "@/Components/Feed/RootTabItems"; import { RootTabs } from "@/Components/Feed/RootTabs"; import Icon from "@/Components/Icons/Icon"; +import KindName from "@/Components/kind-name"; import DisplayName from "@/Components/User/DisplayName"; import useLogin from "@/Hooks/useLogin"; import { LogoHeader } from "@/Pages/Layout/LogoHeader"; import NotificationsHeader from "@/Pages/Layout/NotificationsHeader"; -import { bech32ToHex } from "@/Utils"; +import { bech32ToHex, findTag } from "@/Utils"; export function Header() { const navigate = useNavigate(); const location = useLocation(); - const pageName = decodeURIComponent(location.pathname.split("/")[1]); + const pathSplit = location.pathname.split("/"); + const pageName = decodeURIComponent(pathSplit[1]); const nostrLink = useMemo(() => { - try { - return parseNostrLink(pageName); - } catch (e) { - return undefined; + const nostrEntity = pathSplit.find(a => a.match(Bech32Regex)); + if (nostrEntity) { + return tryParseNostrLink(nostrEntity); } - }, [pageName]); + }, [pathSplit]); const { publicKey, tags } = useLogin(s => ({ publicKey: s.publicKey, @@ -115,17 +116,20 @@ export function Header() { function NoteTitle({ link }: { link: NostrLink }) { const ev = useEventFeed(link); - const values = useMemo(() => { - return { name: }; - }, [ev?.pubkey]); - if (!ev?.pubkey) { return ; } - + const title = findTag(ev, "title"); return ( <> - + , + name: , + title: title ? ` - ${title}` : "", + }} + /> ); } diff --git a/packages/app/src/Pages/ListFeedPage.tsx b/packages/app/src/Pages/ListFeedPage.tsx index 33a38c56..27865d6e 100644 --- a/packages/app/src/Pages/ListFeedPage.tsx +++ b/packages/app/src/Pages/ListFeedPage.tsx @@ -1,5 +1,5 @@ import { dedupe, unwrap } from "@snort/shared"; -import { EventKind, parseNostrLink } from "@snort/system"; +import { parseNostrLink } from "@snort/system"; import { useEventFeed } from "@snort/system-react"; import { useMemo } from "react"; import { FormattedMessage } from "react-intl"; @@ -27,7 +27,8 @@ export function ListFeedPage() { ); if (!data) return ; - if (data.kind !== EventKind.ContactList && data.kind !== EventKind.FollowSet) { + const hasPTags = data.tags.some(a => a[0] === "p"); + if (!hasPTags) { return ( diff --git a/packages/app/src/Pages/Root/FollowSets.tsx b/packages/app/src/Pages/Root/FollowSets.tsx new file mode 100644 index 00000000..d2e51af7 --- /dev/null +++ b/packages/app/src/Pages/Root/FollowSets.tsx @@ -0,0 +1,80 @@ +import { dedupe } from "@snort/shared"; +import { EventKind, NostrLink, RequestBuilder } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; +import { FormattedMessage } from "react-intl"; +import { Link } from "react-router-dom"; + +import AsyncButton from "@/Components/Button/AsyncButton"; +import { AvatarGroup } from "@/Components/User/AvatarGroup"; +import DisplayName from "@/Components/User/DisplayName"; +import { ProfileLink } from "@/Components/User/ProfileLink"; +import useFollowsControls from "@/Hooks/useFollowControls"; +import useWoT from "@/Hooks/useWoT"; +import { findTag } from "@/Utils"; + +export default function FollowSetsPage() { + const sub = new RequestBuilder("follow-sets"); + sub.withFilter().kinds([EventKind.StarterPackSet, EventKind.FollowSet]); + + const data = useRequestBuilder(sub); + const wot = useWoT(); + const control = useFollowsControls(); + const dataSorted = wot.sortEvents(data); + + return ( +
+ {dataSorted.map(a => { + const title = findTag(a, "title") ?? findTag(a, "d") ?? a.content; + const pTags = wot.sortPubkeys(dedupe(a.tags.filter(a => a[0] === "p").map(a => a[1]))); + const isFollowingAll = pTags.every(a => control.isFollowing(a)); + if (pTags.length === 0) return; + const link = NostrLink.fromEvent(a); + return ( +
+
+
+
{title}
+
+ + + + - + + + +
+
+ {!isFollowingAll && ( +
+ { + await control.addFollow(pTags); + }}> + + +
+ )} +
+
+ +
+
+ {c}, + name: ( + + + + ), + }} + /> +
+
+ ); + })} +
+ ); +} diff --git a/packages/app/src/Pages/Root/RootTabRoutes.tsx b/packages/app/src/Pages/Root/RootTabRoutes.tsx index 0f8c63bd..b6bf752d 100644 --- a/packages/app/src/Pages/Root/RootTabRoutes.tsx +++ b/packages/app/src/Pages/Root/RootTabRoutes.tsx @@ -1,4 +1,3 @@ -import SuggestedProfiles from "@/Components/SuggestedProfiles"; import TrendingHashtags from "@/Components/Trending/TrendingHashtags"; import TrendingNotes from "@/Components/Trending/TrendingPosts"; import Discover from "@/Pages/Discover"; @@ -6,6 +5,7 @@ import HashTagsPage from "@/Pages/HashTagsPage"; import { ConversationsTab } from "@/Pages/Root/ConversationsTab"; import { DefaultTab } from "@/Pages/Root/DefaultTab"; import { FollowedByFriendsTab } from "@/Pages/Root/FollowedByFriendsTab"; +import FollowSetsPage from "@/Pages/Root/FollowSets"; import { ForYouTab } from "@/Pages/Root/ForYouTab"; import MediaPosts from "@/Pages/Root/Media"; import { NotesTab } from "@/Pages/Root/NotesTab"; @@ -25,7 +25,8 @@ export type RootTabRoutePath = | "suggested" | "t/:tag" | "topics" - | "media"; + | "media" + | "follow-sets"; export type RootTabRoute = { path: RootTabRoutePath; @@ -69,14 +70,6 @@ export const RootTabRoutes: RootTabRoute[] = [ path: "trending/hashtags", element: , }, - { - path: "suggested", - element: ( -
- -
- ), - }, { path: "t/:tag", element: , @@ -89,4 +82,8 @@ export const RootTabRoutes: RootTabRoute[] = [ path: "media", element: , }, + { + path: "follow-sets", + element: , + }, ]; diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 21591882..aaabae76 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -465,6 +465,9 @@ "9HU8vw": { "defaultMessage": "Reply" }, + "9RNiUn": { + "defaultMessage": "View Feed" + }, "9SvQep": { "defaultMessage": "Follows {n}" }, @@ -493,9 +496,6 @@ "defaultMessage": "Parent", "description": "Link to parent note in thread" }, - "ALdW69": { - "defaultMessage": "Note by {name}" - }, "AN0Z7Q": { "defaultMessage": "Muted Words" }, @@ -587,9 +587,6 @@ "C8FsOr": { "defaultMessage": "Popular Servers" }, - "C8HhVE": { - "defaultMessage": "Suggested Follows" - }, "CA1efg": { "defaultMessage": "Video sets" }, @@ -608,6 +605,9 @@ "CM0k0d": { "defaultMessage": "Prune follow list" }, + "CSOaM+": { + "defaultMessage": "{note_type} by {name}{title}" + }, "CVWeJ6": { "defaultMessage": "Trending People" }, @@ -1398,6 +1398,9 @@ "V20Og0": { "defaultMessage": "Labeling" }, + "V93INS": { + "defaultMessage": "Created by {name}" + }, "VOjC1i": { "defaultMessage": "Pick which upload service you want to upload attachments to" }, @@ -1608,6 +1611,12 @@ "c3g2hL": { "defaultMessage": "Broadcast Again" }, + "c6BMLV": { + "defaultMessage": "Starter Pack" + }, + "cF3ruj": { + "defaultMessage": "Follow All" + }, "cFbU1B": { "defaultMessage": "Using Alby? Go to {link} to get your NWC config!" }, @@ -1786,6 +1795,9 @@ "gDzDRs": { "defaultMessage": "Emoji to send when reactiong to a note" }, + "gPxSgn": { + "defaultMessage": "Follow Sets" + }, "gXgY3+": { "defaultMessage": "Not all clients support this yet" }, @@ -1810,6 +1822,9 @@ "grQ+mI": { "defaultMessage": "Proof of Work" }, + "grRQTM": { + "defaultMessage": "{n} people" + }, "gtNjNP": { "defaultMessage": "Basic protocol flow description" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 9bc4dbec..2f667a91 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -154,6 +154,7 @@ "9+Ddtu": "Next", "92gdbw": "Relay Discovery", "9HU8vw": "Reply", + "9RNiUn": "View Feed", "9SvQep": "Follows {n}", "9V0wg3": "Calendar Event RSVP", "9WRlF4": "Send", @@ -163,7 +164,6 @@ "9wO4wJ": "Lightning Invoice", "A86fJ+": "Generic Repost", "ADmfQT": "Parent", - "ALdW69": "Note by {name}", "AN0Z7Q": "Muted Words", "ASRK0S": "This author has been muted", "AedFVZ": "Create or update a product", @@ -194,13 +194,13 @@ "C7642/": "Quote Repost", "C81/uG": "Logout", "C8FsOr": "Popular Servers", - "C8HhVE": "Suggested Follows", "CA1efg": "Video sets", "CHTbO3": "Failed to load invoice", "CJ0biq": "Poll Response", "CJx5Nd": "Profile Zaps", "CM+Cfj": "Follow List", "CM0k0d": "Prune follow list", + "CSOaM+": "{note_type} by {name}{title}", "CVWeJ6": "Trending People", "CYkOCI": "and {count} others you follow", "Cdxwi0": "Repository announcements", @@ -463,6 +463,7 @@ "UsCzPc": "Share a personalized invitation with friends!", "UxgyeY": "Your referral code is {code}", "V20Og0": "Labeling", + "V93INS": "Created by {name}", "VOjC1i": "Pick which upload service you want to upload attachments to", "VR5eHw": "Public key (npub/nprofile)", "VcwrfF": "Yes please", @@ -533,6 +534,8 @@ "c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}", "c3LlRO": "{n}KiB", "c3g2hL": "Broadcast Again", + "c6BMLV": "Starter Pack", + "cF3ruj": "Follow All", "cFbU1B": "Using Alby? Go to {link} to get your NWC config!", "cG/bKQ": "Native nostr wallet connection", "cHCwbF": "Photography", @@ -592,6 +595,7 @@ "g5pX+a": "About", "g985Wp": "Failed to send vote", "gDzDRs": "Emoji to send when reactiong to a note", + "gPxSgn": "Follow Sets", "gXgY3+": "Not all clients support this yet", "gczcC5": "Subscribe", "geppt8": "{count} ({count2} in memory)", @@ -600,6 +604,7 @@ "gl1NeW": "Lists", "go2/QF": "User server list", "grQ+mI": "Proof of Work", + "grRQTM": "{n} people", "gtNjNP": "Basic protocol flow description", "h1gtUi": "Poll", "h7jvCs": "{site} is more fun together!", diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts index 70372a69..0e817d94 100644 --- a/packages/system/src/event-kind.ts +++ b/packages/system/src/event-kind.ts @@ -46,6 +46,7 @@ const enum EventKind { CurationSet = 30_004, // NIP-51 InterestSet = 30_015, // NIP-15 EmojiSet = 30_030, // NIP-51 + StarterPackSet = 39_089, // NIP-51 Badge = 30009, // NIP-58 ProfileBadges = 30008, // NIP-58