From 7523b41610aae6f333644f62ab1f0b61ccf941ed Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 18 Dec 2023 15:57:00 +0000 Subject: [PATCH] feat: community leaders --- packages/app/config/default.json | 6 ++- packages/app/config/iris.json | 3 +- packages/app/custom.d.ts | 7 +++ .../src/Element/CommunityLeaders/Award.tsx | 54 +++++++++++++++++++ .../Element/CommunityLeaders/LeaderBadge.tsx | 48 +++++++++++++++++ packages/app/src/Element/Event/NoteInner.tsx | 1 + .../app/src/Element/User/ProfileImage.css | 4 -- .../app/src/Element/User/ProfileImage.tsx | 8 ++- .../app/src/Hooks/useCommunityLeaders.tsx | 41 ++++++++++++++ packages/app/src/Pages/Layout/index.tsx | 4 ++ .../app/src/Pages/Profile/ProfilePage.css | 4 -- packages/app/src/Pages/settings/Referrals.tsx | 11 ++++ packages/app/src/lang.json | 9 ++++ packages/app/src/translations/en.json | 3 ++ 14 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 packages/app/src/Element/CommunityLeaders/Award.tsx create mode 100644 packages/app/src/Element/CommunityLeaders/LeaderBadge.tsx create mode 100644 packages/app/src/Hooks/useCommunityLeaders.tsx diff --git a/packages/app/config/default.json b/packages/app/config/default.json index 46b33f04..498fc11b 100644 --- a/packages/app/config/default.json +++ b/packages/app/config/default.json @@ -16,7 +16,8 @@ "subscriptions": true, "deck": true, "zapPool": true, - "notificationGraph": true + "notificationGraph": true, + "communityLeaders": true }, "signUp": { "moderation": true, @@ -26,6 +27,9 @@ "bypassImgProxyError": false, "preferLargeMedia": true }, + "communityLeaders": { + "list": "naddr1qq4xc6tnw3ez6vp58y6rywpjxckngdtyxukngwr9vckkze33vcknzcnrxcenje35xqmn2cczyp3lucccm3v9s087z6qslpkap8schltk427zfgqgrn3g2menq5zw6qcyqqq82vqprpmhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv7rajfl" + }, "noteCreatorToast": true, "hideFromNavbar": ["/graph"], "deckSubKind": 1, diff --git a/packages/app/config/iris.json b/packages/app/config/iris.json index 8a977f35..b4e37335 100644 --- a/packages/app/config/iris.json +++ b/packages/app/config/iris.json @@ -16,7 +16,8 @@ "subscriptions": false, "deck": true, "zapPool": true, - "notificationGraph": false + "notificationGraph": false, + "communityLeaders": false }, "signUp": { "moderation": false, diff --git a/packages/app/custom.d.ts b/packages/app/custom.d.ts index f55b5a6c..89be2e93 100644 --- a/packages/app/custom.d.ts +++ b/packages/app/custom.d.ts @@ -58,6 +58,10 @@ declare const CONFIG: { deck: boolean; zapPool: boolean; notificationGraph: boolean; + communityLeaders: boolean; + }; + defaultPreferences: { + checkSigs: boolean; }; signUp: { moderation: boolean; @@ -67,6 +71,9 @@ declare const CONFIG: { bypassImgProxyError: boolean; preferLargeMedia: boolean; }; + communityLeaders?: { + list: string; + }; // Filter urls from nav sidebar hideFromNavbar: Array; // Limit deck to certain subscriber tier diff --git a/packages/app/src/Element/CommunityLeaders/Award.tsx b/packages/app/src/Element/CommunityLeaders/Award.tsx new file mode 100644 index 00000000..430c7a6c --- /dev/null +++ b/packages/app/src/Element/CommunityLeaders/Award.tsx @@ -0,0 +1,54 @@ +export default function AwardIcon({ size }: { size?: number }) { + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/app/src/Element/CommunityLeaders/LeaderBadge.tsx b/packages/app/src/Element/CommunityLeaders/LeaderBadge.tsx new file mode 100644 index 00000000..ce948a1d --- /dev/null +++ b/packages/app/src/Element/CommunityLeaders/LeaderBadge.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { FormattedMessage } from "react-intl"; +import AwardIcon from "./Award"; +import Modal from "../Modal"; +import { Link } from "react-router-dom"; +import CloseButton from "../Button/CloseButton"; + +export function LeaderBadge() { + const [showModal, setShowModal] = useState(false); + return ( + <> +
{ + e.preventDefault(); + e.stopPropagation(); + setShowModal(true); + }}> + +
+ +
+
+ {showModal && ( + setShowModal(false)} id="leaders"> +
+ setShowModal(false)} /> + +
+ +
+

+ +

+ + + +
+
+ )} + + ); +} diff --git a/packages/app/src/Element/Event/NoteInner.tsx b/packages/app/src/Element/Event/NoteInner.tsx index 54b3aa12..87313c26 100644 --- a/packages/app/src/Element/Event/NoteInner.tsx +++ b/packages/app/src/Element/Event/NoteInner.tsx @@ -336,6 +336,7 @@ export function NoteInner(props: NoteProps) { subHeader={replyTag() ?? undefined} link={opt?.canClick === undefined ? undefined : ""} showProfileCard={options.showProfileCard ?? true} + showBadges={true} />
{props.context} diff --git a/packages/app/src/Element/User/ProfileImage.css b/packages/app/src/Element/User/ProfileImage.css index 29d1c906..9e11ba91 100644 --- a/packages/app/src/Element/User/ProfileImage.css +++ b/packages/app/src/Element/User/ProfileImage.css @@ -19,10 +19,6 @@ a.pfp { text-decoration: none; } -.pfp .username { - font-weight: 600; -} - .pfp .profile-name { max-width: stretch; max-width: -webkit-fill-available; diff --git a/packages/app/src/Element/User/ProfileImage.tsx b/packages/app/src/Element/User/ProfileImage.tsx index b3cf8bdc..c433a55f 100644 --- a/packages/app/src/Element/User/ProfileImage.tsx +++ b/packages/app/src/Element/User/ProfileImage.tsx @@ -10,6 +10,8 @@ import DisplayName from "./DisplayName"; import { ProfileLink } from "./ProfileLink"; import { ProfileCard } from "./ProfileCard"; import FollowDistanceIndicator from "@/Element/User/FollowDistanceIndicator"; +import { useCommunityLeader } from "@/Hooks/useCommunityLeaders"; +import { LeaderBadge } from "@/Element/CommunityLeaders/LeaderBadge"; export interface ProfileImageProps { pubkey: HexKey; @@ -27,6 +29,7 @@ export interface ProfileImageProps { showFollowDistance?: boolean; icons?: ReactNode; showProfileCard?: boolean; + showBadges?: boolean; } export default function ProfileImage({ @@ -43,9 +46,11 @@ export default function ProfileImage({ showFollowDistance = true, icons, showProfileCard = false, + showBadges = false, }: ProfileImageProps) { const user = useUserProfile(profile ? "" : pubkey) ?? profile; const [isHovering, setIsHovering] = useState(false); + const leader = useCommunityLeader(pubkey); const hoverTimeoutRef = useRef(null); @@ -88,8 +93,9 @@ export default function ProfileImage({
{showUsername && (
-
+
{overrideUsername ? overrideUsername : } + {leader && showBadges && CONFIG.features.communityLeaders && }
{subHeader}
diff --git a/packages/app/src/Hooks/useCommunityLeaders.tsx b/packages/app/src/Hooks/useCommunityLeaders.tsx new file mode 100644 index 00000000..9558a35d --- /dev/null +++ b/packages/app/src/Hooks/useCommunityLeaders.tsx @@ -0,0 +1,41 @@ +import { ExternalStore, unwrap } from "@snort/shared"; +import { EventKind, parseNostrLink } from "@snort/system"; +import { useLinkList } from "./useLists"; +import { useEffect, useSyncExternalStore } from "react"; + +class CommunityLeadersStore extends ExternalStore> { + #leaders: Array = []; + + setLeaders(arr: Array) { + this.#leaders = arr; + this.notifyChange(); + } + + takeSnapshot(): string[] { + return [...this.#leaders]; + } +} + +const LeadersStore = new CommunityLeadersStore(); + +export function useCommunityLeaders() { + const link = parseNostrLink(unwrap(CONFIG.communityLeaders).list); + + const list = useLinkList("leaders", rb => { + rb.withFilter().kinds([EventKind.FollowSet]).link(link); + }); + + useEffect(() => { + console.debug("CommunityLeaders", list); + LeadersStore.setLeaders(list.map(a => a.id)); + }, [list]); +} + +export function useCommunityLeader(pubkey?: string) { + const store = useSyncExternalStore( + c => LeadersStore.hook(c), + () => LeadersStore.snapshot(), + ); + + return pubkey && store.includes(pubkey); +} diff --git a/packages/app/src/Pages/Layout/index.tsx b/packages/app/src/Pages/Layout/index.tsx index e506951f..ec8bdff0 100644 --- a/packages/app/src/Pages/Layout/index.tsx +++ b/packages/app/src/Pages/Layout/index.tsx @@ -17,6 +17,7 @@ import ErrorBoundary from "@/Element/ErrorBoundary"; import Footer from "@/Pages/Layout/Footer"; import { Header } from "@/Pages/Layout/Header"; import CloseButton from "@/Element/Button/CloseButton"; +import { useCommunityLeaders } from "@/Hooks/useCommunityLeaders"; export default function Index() { const location = useLocation(); @@ -25,6 +26,9 @@ export default function Index() { useTheme(); useLoginRelays(); useLoginFeed(); + if (CONFIG.features.communityLeaders) { + useCommunityLeaders(); + } const hideHeaderPaths = ["/login", "/new"]; const shouldHideFooter = location.pathname.startsWith("/messages/"); diff --git a/packages/app/src/Pages/Profile/ProfilePage.css b/packages/app/src/Pages/Profile/ProfilePage.css index 6b59da51..cb01fc69 100644 --- a/packages/app/src/Pages/Profile/ProfilePage.css +++ b/packages/app/src/Pages/Profile/ProfilePage.css @@ -166,10 +166,6 @@ border: none; } -.qr-modal .pfp .username { - align-items: center; -} - .qr-modal canvas { border-radius: 10px; } diff --git a/packages/app/src/Pages/settings/Referrals.tsx b/packages/app/src/Pages/settings/Referrals.tsx index 1d19b292..b103ba22 100644 --- a/packages/app/src/Pages/settings/Referrals.tsx +++ b/packages/app/src/Pages/settings/Referrals.tsx @@ -1,3 +1,4 @@ +import { LeaderBadge } from "@/Element/CommunityLeaders/LeaderBadge"; import Copy from "@/Element/Copy"; import SnortApi from "@/External/SnortApi"; import useEventPublisher from "@/Hooks/useEventPublisher"; @@ -37,6 +38,16 @@ export function ReferralsPage() {
+ +

+ +

+
+ +
+

+ +

); } diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 7cd73fd3..d4eb21cc 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -264,6 +264,9 @@ "7UOvbT": { "defaultMessage": "Offline" }, + "7YkSA2": { + "defaultMessage": "Community Leader" + }, "7hp70g": { "defaultMessage": "NIP-05" }, @@ -682,6 +685,9 @@ "M3Oirc": { "defaultMessage": "Debug Menus" }, + "M6C/px": { + "defaultMessage": "Become a leader" + }, "MBAYRO": { "defaultMessage": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message" }, @@ -1140,6 +1146,9 @@ "egib+2": { "defaultMessage": "{n,plural,=1{& {n} other} other{& {n} others}}" }, + "f1OxTe": { + "defaultMessage": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title." + }, "fBI91o": { "defaultMessage": "Zap" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 2ab8d663..93f847a8 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -87,6 +87,7 @@ "7+Domh": "Notes", "712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node", "7UOvbT": "Offline", + "7YkSA2": "Community Leader", "7hp70g": "NIP-05", "8/vBbP": "Reposts ({n})", "89q5wc": "Confirm Reposts", @@ -225,6 +226,7 @@ "Lw+I+J": "{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}", "LwYmVi": "Zaps on this note will be split to the following users.", "M3Oirc": "Debug Menus", + "M6C/px": "Become a leader", "MBAYRO": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message", "MI2jkA": "Not available:", "MP54GY": "Wallet password", @@ -375,6 +377,7 @@ "eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.", "eXT2QQ": "Group Chat", "egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}", + "f1OxTe": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title.", "fBI91o": "Zap", "fBlba3": "Thanks for using {site}, please consider donating if you can.", "fOksnD": "Can't vote because LNURL service does not support zaps",