From 9d33abbf1ef3df27fba7199699d192d96e0c29a5 Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 10 Oct 2023 10:37:53 +0100 Subject: [PATCH] feature flags config / typed app config --- packages/app/config/default.json | 5 +- packages/app/config/iris.json | 5 +- packages/app/custom.d.ts | 14 +++ packages/app/src/Element/FormattedMessage.tsx | 2 +- packages/app/src/Element/Logo.tsx | 2 +- packages/app/src/Element/PinPrompt.tsx | 2 +- packages/app/src/Element/User/DisplayName.tsx | 4 +- packages/app/src/Feed/LoginFeed.ts | 2 +- packages/app/src/Pages/DonatePage.tsx | 4 +- packages/app/src/Pages/Layout.tsx | 2 +- packages/app/src/Pages/LoginPage.tsx | 6 +- packages/app/src/Pages/NostrLinkHandler.tsx | 2 +- .../app/src/Pages/Profile/ProfilePage.tsx | 4 +- packages/app/src/Pages/new/messages.ts | 10 +-- packages/app/src/Pages/settings/Root.tsx | 14 +-- packages/app/src/Tasks/DonateTask.tsx | 2 +- packages/app/src/index.tsx | 26 +++++- packages/app/webpack.config.js | 6 +- packages/system-react/src/useUserProfile.ts | 18 ---- packages/system/src/profile-cache.ts | 87 +++++++++---------- 20 files changed, 118 insertions(+), 99 deletions(-) diff --git a/packages/app/config/default.json b/packages/app/config/default.json index 59de7693..8c5714d2 100644 --- a/packages/app/config/default.json +++ b/packages/app/config/default.json @@ -6,5 +6,8 @@ "favicon": "public/favicon.ico", "appleTouchIconUrl": "/nostrich_512.png", "httpCache": "", - "animalNamePlaceholders": false + "animalNamePlaceholders": false, + "features": { + "subscriptions": true + } } diff --git a/packages/app/config/iris.json b/packages/app/config/iris.json index 823281ed..b1f59e8c 100644 --- a/packages/app/config/iris.json +++ b/packages/app/config/iris.json @@ -6,5 +6,8 @@ "favicon": "public/iris/favicon.ico", "appleTouchIconUrl": "/img/apple-touch-icon.png", "httpCache": "https://api.iris.to", - "animalNamePlaceholders": true + "animalNamePlaceholders": true, + "features": { + "subscriptions": false + } } diff --git a/packages/app/custom.d.ts b/packages/app/custom.d.ts index 8e871bb8..17267e74 100644 --- a/packages/app/custom.d.ts +++ b/packages/app/custom.d.ts @@ -34,3 +34,17 @@ declare module "emojilib" { const value: Record; export default value; } + +declare const CONFIG: { + appName: string; + appNameCapitalized: string; + appTitle: string; + nip05Domain: string; + favicon: string; + appleTouchIconUrl: string; + httpCache: string; + animalNamePlaceholders: boolean; + features: { + subscriptions: boolean; + }; +}; diff --git a/packages/app/src/Element/FormattedMessage.tsx b/packages/app/src/Element/FormattedMessage.tsx index e2978482..77141d39 100644 --- a/packages/app/src/Element/FormattedMessage.tsx +++ b/packages/app/src/Element/FormattedMessage.tsx @@ -12,7 +12,7 @@ const ExtendedFormattedMessage: FC = props => { useEffect(() => { const translatedMessage = formatMessage({ id, defaultMessage }, values); if (typeof translatedMessage === "string") { - setProcessedMessage(translatedMessage.replace("Snort", process.env.APP_NAME_CAPITALIZED || "Snort")); + setProcessedMessage(translatedMessage.replace("Snort", CONFIG.appNameCapitalized || "Snort")); } }, [id, defaultMessage, values, formatMessage]); diff --git a/packages/app/src/Element/Logo.tsx b/packages/app/src/Element/Logo.tsx index d33e3f95..7647e2ab 100644 --- a/packages/app/src/Element/Logo.tsx +++ b/packages/app/src/Element/Logo.tsx @@ -4,7 +4,7 @@ const Logo = () => { const navigate = useNavigate(); return (

navigate("/")}> - {process.env.APP_NAME} + {CONFIG.appNameCapitalized}

); }; diff --git a/packages/app/src/Element/PinPrompt.tsx b/packages/app/src/Element/PinPrompt.tsx index a9412931..fa8d5e2e 100644 --- a/packages/app/src/Element/PinPrompt.tsx +++ b/packages/app/src/Element/PinPrompt.tsx @@ -145,7 +145,7 @@ export function LoginUnlock() {

diff --git a/packages/app/src/Element/User/DisplayName.tsx b/packages/app/src/Element/User/DisplayName.tsx index b7e58f8f..e3fe57fa 100644 --- a/packages/app/src/Element/User/DisplayName.tsx +++ b/packages/app/src/Element/User/DisplayName.tsx @@ -1,6 +1,6 @@ import "./DisplayName.css"; -import React, { useMemo } from "react"; +import { useMemo } from "react"; import { HexKey, UserMetadata, NostrPrefix } from "@snort/system"; import AnimalName from "Element/User/AnimalName"; import { hexToBech32 } from "SnortUtils"; @@ -22,7 +22,7 @@ export function getDisplayNameOrPlaceHolder(user: UserMetadata | undefined, pubk name = user.display_name; } else if (typeof user?.name === "string" && user.name.length > 0) { name = user.name; - } else if (pubkey && process.env.ANIMAL_NAME_PLACEHOLDERS) { + } else if (pubkey && CONFIG.animalNamePlaceholders) { name = AnimalName(pubkey); isPlaceHolder = true; } diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 14f06539..400bb6dc 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -49,7 +49,7 @@ export default function useLoginFeed() { leaveOpen: true, }); b.withFilter().authors([pubKey]).kinds([EventKind.ContactList]); - if (!login.readonly) { + if (CONFIG.features.subscriptions && !login.readonly) { b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]); b.withFilter() .relay("wss://relay.snort.social") diff --git a/packages/app/src/Pages/DonatePage.tsx b/packages/app/src/Pages/DonatePage.tsx index f1df62c8..7617286b 100644 --- a/packages/app/src/Pages/DonatePage.tsx +++ b/packages/app/src/Pages/DonatePage.tsx @@ -94,13 +94,13 @@ const DonatePage = () => {

diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index a02150fd..001d2822 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -209,7 +209,7 @@ function LogoHeader() { return ( -

{process.env.APP_NAME}

+

{CONFIG.appName}

{currentSubscription && ( diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx index ba068cc3..c8e0a899 100644 --- a/packages/app/src/Pages/LoginPage.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -143,7 +143,7 @@ export default function LoginPage() { function generateNip46() { const meta = { - name: process.env.APP_NAME_CAPITALIZED, + name: CONFIG.appNameCapitalized, url: window.location.href, }; @@ -287,7 +287,7 @@ export default function LoginPage() {

navigate("/")}> - {process.env.APP_NAME} + {CONFIG.appName}

@@ -342,7 +342,7 @@ export default function LoginPage() {

diff --git a/packages/app/src/Pages/NostrLinkHandler.tsx b/packages/app/src/Pages/NostrLinkHandler.tsx index 801ab794..d716bfca 100644 --- a/packages/app/src/Pages/NostrLinkHandler.tsx +++ b/packages/app/src/Pages/NostrLinkHandler.tsx @@ -25,7 +25,7 @@ export default function NostrLinkHandler() { } } else { try { - const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`); + const pubkey = await getNip05PubKey(`${link}@${CONFIG.nip05Domain}`); if (pubkey) { setRenderComponent(); // Directly render ProfilePage } diff --git a/packages/app/src/Pages/Profile/ProfilePage.tsx b/packages/app/src/Pages/Profile/ProfilePage.tsx index 81b15b6e..fb515ddc 100644 --- a/packages/app/src/Pages/Profile/ProfilePage.tsx +++ b/packages/app/src/Pages/Profile/ProfilePage.tsx @@ -139,8 +139,8 @@ export default function ProfilePage({ id: propId }: ProfilePageProps) { useEffect(() => { if (user?.nip05 && user?.isNostrAddressValid) { - if (user.nip05.endsWith(`@${process.env.NIP05_DOMAIN}`)) { - const username = user.nip05?.replace(`@${process.env.NIP05_DOMAIN}`, ""); + if (user.nip05.endsWith(`@${CONFIG.nip05Domain}`)) { + const username = user.nip05?.replace(`@${CONFIG.nip05Domain}`, ""); navigate(`/${username}`, { replace: true }); } } diff --git a/packages/app/src/Pages/new/messages.ts b/packages/app/src/Pages/new/messages.ts index 5e7d0452..2a86b565 100644 --- a/packages/app/src/Pages/new/messages.ts +++ b/packages/app/src/Pages/new/messages.ts @@ -14,11 +14,11 @@ export default defineMessages({ KeysSaved: { defaultMessage: "I have saved my keys, continue" }, WhatIsSnort: { defaultMessage: "What is {site} and how does it work?", - values: { site: process.env.APP_NAME_CAPITALIZED }, + values: { site: CONFIG.appNameCapitalized }, }, WhatIsSnortIntro: { defaultMessage: `{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`, - values: { site: process.env.APP_NAME_CAPITALIZED }, + values: { site: CONFIG.appNameCapitalized }, }, WhatIsSnortNotes: { defaultMessage: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`, @@ -26,7 +26,7 @@ export default defineMessages({ WhatIsSnortExperience: { defaultMessage: "{site} is designed to have a similar experience to Twitter.", - values: { site: process.env.APP_NAME_CAPITALIZED }, + values: { site: CONFIG.appNameCapitalized }, }, HowKeysWork: { defaultMessage: "How do keys work?" }, DigitalSignatures: { @@ -70,9 +70,9 @@ export default defineMessages({ NameSquatting: { defaultMessage: "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.", - values: { site: process.env.APP_NAME_CAPITALIZED }, + values: { site: CONFIG.appNameCapitalized }, }, - PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: process.env.APP_NAME_CAPITALIZED } }, + PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: CONFIG.appNameCapitalized } }, GetSnortId: { defaultMessage: "Get a Snort identifier" }, GetSnortIdHelp: { defaultMessage: diff --git a/packages/app/src/Pages/settings/Root.tsx b/packages/app/src/Pages/settings/Root.tsx index 8edab2b8..3383b886 100644 --- a/packages/app/src/Pages/settings/Root.tsx +++ b/packages/app/src/Pages/settings/Root.tsx @@ -6,9 +6,9 @@ import Icon from "Icons/Icon"; import { LoginStore, logout } from "Login"; import useLogin from "Hooks/useLogin"; import { getCurrentSubscription } from "Subscription"; +import usePageWidth from "Hooks/usePageWidth"; import messages from "./messages"; -import usePageWidth from "Hooks/usePageWidth"; const SettingsIndex = () => { const login = useLogin(); @@ -61,11 +61,13 @@ const SettingsIndex = () => {

-
navigate("/subscribe/manage")}> - - - -
+ {CONFIG.features.subscriptions && ( +
navigate("/subscribe/manage")}> + + + +
+ )} {sub && (
navigate("accounts")}> diff --git a/packages/app/src/Tasks/DonateTask.tsx b/packages/app/src/Tasks/DonateTask.tsx index f6431f49..041e18df 100644 --- a/packages/app/src/Tasks/DonateTask.tsx +++ b/packages/app/src/Tasks/DonateTask.tsx @@ -15,7 +15,7 @@ export class DonateTask extends BaseUITask {

diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 417a9395..2a52a23c 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -16,6 +16,7 @@ import { ReqFilter, PowMiner, NostrEvent, + mapEventToProfile, } from "@snort/system"; import { SnortContext } from "@snort/system-react"; @@ -87,6 +88,29 @@ export const System = new NostrSystem({ }, }); +async function fetchProfile(key: string) { + const rsp = await fetch(`${CONFIG.httpCache}/profile/${key}`); + if (rsp.ok) { + try { + const data = (await rsp.json()) as NostrEvent; + if (data) { + return mapEventToProfile(data); + } + } catch (e) { + console.error(e); + } + } +} + +/** + * Add profile loader fn + */ +if (CONFIG.httpCache) { + System.ProfileLoader.loaderFn = async (keys: Array) => { + return (await Promise.all(keys.map(a => fetchProfile(a)))).filter(a => a !== undefined).map(a => unwrap(a)); + }; +} + /** * Singleton user profile loader */ @@ -191,7 +215,7 @@ export const router = createBrowserRouter([ }, ...NewUserRoutes, ...WalletRoutes, - ...SubscribeRoutes, + ...(CONFIG.features.subscriptions ? SubscribeRoutes : []), { path: "/debug", element: , diff --git a/packages/app/webpack.config.js b/packages/app/webpack.config.js index c20183fa..4f40b87f 100644 --- a/packages/app/webpack.config.js +++ b/packages/app/webpack.config.js @@ -88,11 +88,7 @@ const config = { }) : false, new DefinePlugin({ - "process.env.APP_NAME": JSON.stringify(appConfig.get("appName")), - "process.env.APP_NAME_CAPITALIZED": JSON.stringify(appConfig.get("appNameCapitalized")), - "process.env.NIP05_DOMAIN": JSON.stringify(appConfig.get("nip05Domain")), - "process.env.HTTP_CACHE": JSON.stringify(appConfig.get("httpCache")), - "process.env.ANIMAL_NAME_PLACEHOLDERS": JSON.stringify(appConfig.get("animalNamePlaceholders")), + CONFIG: JSON.stringify(appConfig), }), ], module: { diff --git a/packages/system-react/src/useUserProfile.ts b/packages/system-react/src/useUserProfile.ts index d6b12585..5d718761 100644 --- a/packages/system-react/src/useUserProfile.ts +++ b/packages/system-react/src/useUserProfile.ts @@ -11,24 +11,6 @@ export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined { h => { if (pubKey) { system.ProfileLoader.TrackMetadata(pubKey); - if (process.env.HTTP_CACHE && !system.ProfileLoader.Cache.getFromCache(pubKey)) { - fetch(`${process.env.HTTP_CACHE}/profile/${pubKey}`) - .then(async r => { - if (r.ok) { - try { - const data = await r.json(); - if (data) { - system.ProfileLoader.onProfileEvent(data); - } - } catch (e) { - console.error(e); - } - } - }) - .catch(e => { - console.error(e); - }); - } } const release = system.ProfileLoader.Cache.hook(h, pubKey); return () => { diff --git a/packages/system/src/profile-cache.ts b/packages/system/src/profile-cache.ts index 6118a8ae..ee5646c8 100644 --- a/packages/system/src/profile-cache.ts +++ b/packages/system/src/profile-cache.ts @@ -1,8 +1,9 @@ import debug from "debug"; import { unixNowMs, FeedCache } from "@snort/shared"; -import { EventKind, HexKey, SystemInterface, TaggedNostrEvent, NoteCollection, RequestBuilder } from "."; +import { EventKind, HexKey, SystemInterface, TaggedNostrEvent, RequestBuilder } from "."; import { ProfileCacheExpire } from "./const"; import { mapEventToProfile, MetadataCache } from "./cache"; +import { v4 as uuid } from "uuid"; const MetadataRelays = ["wss://purplepag.es"]; @@ -23,6 +24,11 @@ export class ProfileLoaderService { readonly #log = debug("ProfileCache"); + /** + * Custom loader function for fetching profiles from alternative sources + */ + loaderFn?: (pubkeys: Array) => Promise>; + constructor(system: SystemInterface, cache: FeedCache) { this.#system = system; this.#cache = cache; @@ -92,50 +98,8 @@ export class ProfileLoaderService { if (missing.size > 0) { this.#log("Wants profiles: %d missing, %d expired", missingFromCache.length, expired.length); - const sub = new RequestBuilder("profiles"); - sub - .withOptions({ - skipDiff: true, - }) - .withFilter() - .kinds([EventKind.SetMetadata]) - .authors([...missing]); + const results = await this.#loadProfiles([...missing]); - if (this.#missingLastRun.size > 0) { - const fMissing = sub - .withFilter() - .kinds([EventKind.SetMetadata]) - .authors([...this.#missingLastRun]); - MetadataRelays.forEach(r => fMissing.relay(r)); - } - const newProfiles = new Set(); - const q = this.#system.Query(NoteCollection, sub); - const feed = (q?.feed as NoteCollection) ?? new NoteCollection(); - // never release this callback, it will stop firing anyway after eose - const releaseOnEvent = feed.onEvent(async e => { - for (const pe of e) { - newProfiles.add(pe.id); - await this.onProfileEvent(pe); - } - }); - const results = await new Promise>>(resolve => { - let timeout: ReturnType | undefined = undefined; - const release = feed.hook(() => { - if (!feed.loading) { - clearTimeout(timeout); - resolve(feed.getSnapshotData() ?? []); - this.#log("Profiles finished: %s", sub.id); - release(); - } - }); - timeout = setTimeout(() => { - release(); - resolve(feed.getSnapshotData() ?? []); - this.#log("Profiles timeout: %s", sub.id); - }, 5_000); - }); - - releaseOnEvent(); const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a)); this.#missingLastRun = new Set(couldNotFetch); if (couldNotFetch.length > 0) { @@ -150,12 +114,43 @@ export class ProfileLoaderService { await Promise.all(empty); } - // When we fetch an expired profile and its the same as what we already have + /* When we fetch an expired profile and its the same as what we already have // onEvent is not fired and the loaded timestamp never gets updated const expiredSame = results.filter(a => !newProfiles.has(a.id) && expired.includes(a.pubkey)); - await Promise.all(expiredSame.map(v => this.onProfileEvent(v))); + await Promise.all(expiredSame.map(v => this.onProfileEvent(v)));*/ } setTimeout(() => this.#FetchMetadata(), 500); } + + async #loadProfiles(missing: Array) { + if (this.loaderFn) { + const results = await this.loaderFn(missing); + await Promise.all(results.map(a => this.#cache.update(a))); + return results; + } else { + const sub = new RequestBuilder(`profiles-${uuid()}`); + sub + .withOptions({ + skipDiff: true, + }) + .withFilter() + .kinds([EventKind.SetMetadata]) + .authors(missing); + + if (this.#missingLastRun.size > 0) { + const fMissing = sub + .withFilter() + .kinds([EventKind.SetMetadata]) + .authors([...this.#missingLastRun]); + MetadataRelays.forEach(r => fMissing.relay(r)); + } + const results = (await this.#system.Fetch(sub, async e => { + for (const pe of e) { + await this.onProfileEvent(pe); + } + })) as ReadonlyArray; + return results; + } + } }