From ed1cb0423566e061d1f5110073811650a28a7188 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Fri, 13 Oct 2023 10:24:55 -0500 Subject: [PATCH] fix issue with breakpoints causing re-render --- package.json | 3 + .../event-types/embedded-stream.tsx | 3 +- src/components/embed-types/image.tsx | 3 +- src/components/layout/index.tsx | 5 +- .../note/components/note-reactions.tsx | 3 +- src/components/note/index.tsx | 2 +- src/components/note/note-relays.tsx | 2 +- src/components/open-graph-card.tsx | 2 +- src/components/sensitive-content-warning.tsx | 13 +-- .../timeline-page/media-timeline/index.tsx | 2 +- src/helpers/nostr/apps.ts | 2 +- src/providers/breakpoint-provider.tsx | 79 +++++++++++++++++++ src/providers/index.tsx | 37 +++++---- src/views/streams/stream/index.tsx | 2 +- src/views/user/components/header.tsx | 4 +- src/views/user/index.tsx | 25 +----- yarn.lock | 9 ++- 17 files changed, 133 insertions(+), 63 deletions(-) create mode 100644 src/providers/breakpoint-provider.tsx diff --git a/package.json b/package.json index b72054df8..aba737a9e 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,11 @@ "dependencies": { "@cashu/cashu-ts": "^0.8.2-rc.7", "@chakra-ui/anatomy": "^2.2.1", + "@chakra-ui/breakpoint-utils": "^2.0.8", "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/media-query": "^3.3.0", "@chakra-ui/react": "^2.8.1", + "@chakra-ui/shared-utils": "^2.0.4", "@chakra-ui/styled-system": "^2.9.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/src/components/embed-event/event-types/embedded-stream.tsx b/src/components/embed-event/event-types/embedded-stream.tsx index cb901a3c6..75a0a2cdf 100644 --- a/src/components/embed-event/event-types/embedded-stream.tsx +++ b/src/components/embed-event/event-types/embedded-stream.tsx @@ -1,4 +1,4 @@ -import { Card, CardBody, CardProps, Flex, Heading, Image, Link, Tag, Text, useBreakpointValue } from "@chakra-ui/react"; +import { Card, CardBody, CardProps, Flex, Heading, Image, Link, Tag, Text } from "@chakra-ui/react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { parseStreamEvent } from "../../../helpers/nostr/stream"; @@ -8,6 +8,7 @@ import { UserLink } from "../../user-link"; import { UserAvatar } from "../../user-avatar"; import useEventNaddr from "../../../hooks/use-event-naddr"; import Timestamp from "../../timestamp"; +import { useBreakpointValue } from "../../../providers/breakpoint-provider"; export default function EmbeddedStream({ event, ...props }: Omit & { event: NostrEvent }) { const stream = parseStreamEvent(event); diff --git a/src/components/embed-types/image.tsx b/src/components/embed-types/image.tsx index 796f0baaf..69ee6220c 100644 --- a/src/components/embed-types/image.tsx +++ b/src/components/embed-types/image.tsx @@ -8,7 +8,7 @@ import { useRef, useState, } from "react"; -import { Image, ImageProps, useBreakpointValue } from "@chakra-ui/react"; +import { Image, ImageProps } from "@chakra-ui/react"; import appSettings from "../../services/settings/app-settings"; import { useTrusted } from "../../providers/trust"; @@ -19,6 +19,7 @@ import { isImageURL } from "../../helpers/url"; import PhotoGallery, { PhotoWithoutSize } from "../photo-gallery"; import { NostrEvent } from "../../types/nostr-event"; import useAppSettings from "../../hooks/use-app-settings"; +import { useBreakpointValue } from "../../providers/breakpoint-provider"; function useElementBlur(initBlur = false): { style: CSSProperties; onClick: MouseEventHandler } { const [blur, setBlur] = useState(initBlur); diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 85d1fa949..9048f0f2b 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,13 +1,14 @@ import React from "react"; -import { Container, Flex, Spacer, useBreakpointValue } from "@chakra-ui/react"; -import { ErrorBoundary } from "../error-boundary"; +import { Container, Flex, Spacer } from "@chakra-ui/react"; +import { ErrorBoundary } from "../error-boundary"; import { ReloadPrompt } from "../reload-prompt"; import DesktopSideNav from "./desktop-side-nav"; import MobileBottomNav from "./mobile-bottom-nav"; import useSubject from "../../hooks/use-subject"; import accountService from "../../services/account"; import GhostToolbar from "./ghost-toolbar"; +import { useBreakpointValue } from "../../providers/breakpoint-provider"; export default function Layout({ children }: { children: React.ReactNode }) { const isMobile = useBreakpointValue({ base: true, md: false }); diff --git a/src/components/note/components/note-reactions.tsx b/src/components/note/components/note-reactions.tsx index 9ca854083..2dc06f8ff 100644 --- a/src/components/note/components/note-reactions.tsx +++ b/src/components/note/components/note-reactions.tsx @@ -1,9 +1,10 @@ -import { ButtonGroup, ButtonGroupProps, Divider, useBreakpointValue } from "@chakra-ui/react"; +import { ButtonGroup, ButtonGroupProps, Divider } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; import ReactionButton from "./reaction-button"; import EventReactionButtons from "../../event-reactions"; import useEventReactions from "../../../hooks/use-event-reactions"; +import { useBreakpointValue } from "../../../providers/breakpoint-provider"; export default function NoteReactions({ event, ...props }: Omit & { event: NostrEvent }) { const reactions = useEventReactions(event.id) ?? []; diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 69c7873eb..f96974534 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -11,7 +11,6 @@ import { IconButton, Link, Text, - useBreakpointValue, useDisclosure, } from "@chakra-ui/react"; import { NostrEvent, isATag } from "../../types/nostr-event"; @@ -44,6 +43,7 @@ import OpenInDrawerButton from "../open-in-drawer-button"; import { getSharableEventAddress } from "../../helpers/nip19"; import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; +import { useBreakpointValue } from "../../providers/breakpoint-provider"; export type NoteProps = Omit & { event: NostrEvent; diff --git a/src/components/note/note-relays.tsx b/src/components/note/note-relays.tsx index d1045725e..4f3d1ebac 100644 --- a/src/components/note/note-relays.tsx +++ b/src/components/note/note-relays.tsx @@ -1,11 +1,11 @@ import { memo } from "react"; -import { useBreakpointValue } from "@chakra-ui/react"; import { getEventRelays } from "../../services/event-relays"; import { NostrEvent } from "../../types/nostr-event"; import useSubject from "../../hooks/use-subject"; import { RelayIconStack, RelayIconStackProps } from "../relay-icon-stack"; import { getEventUID } from "../../helpers/nostr/events"; +import { useBreakpointValue } from "../../providers/breakpoint-provider"; export type NoteRelaysProps = { event: NostrEvent; diff --git a/src/components/open-graph-card.tsx b/src/components/open-graph-card.tsx index 8a3e61031..4ada0cd98 100644 --- a/src/components/open-graph-card.tsx +++ b/src/components/open-graph-card.tsx @@ -10,9 +10,9 @@ import { LinkBox, LinkOverlay, Text, - useBreakpointValue, } from "@chakra-ui/react"; import useOpenGraphData from "../hooks/use-open-graph-data"; +import { useBreakpointValue } from "../providers/breakpoint-provider"; export default function OpenGraphCard({ url, ...props }: { url: URL } & Omit) { const { value: data } = useOpenGraphData(url); diff --git a/src/components/sensitive-content-warning.tsx b/src/components/sensitive-content-warning.tsx index 2f71a7ffd..59dc58410 100644 --- a/src/components/sensitive-content-warning.tsx +++ b/src/components/sensitive-content-warning.tsx @@ -1,16 +1,7 @@ -import { - Alert, - AlertDescription, - AlertIcon, - AlertProps, - AlertTitle, - Button, - Spacer, - useBreakpointValue, - useModal, -} from "@chakra-ui/react"; +import { Alert, AlertDescription, AlertIcon, AlertProps, AlertTitle, Button, Spacer, useModal } from "@chakra-ui/react"; import { useExpand } from "../providers/expanded"; +import { useBreakpointValue } from "../providers/breakpoint-provider"; export default function SensitiveContentWarning({ description }: { description: string } & AlertProps) { const expand = useExpand(); diff --git a/src/components/timeline-page/media-timeline/index.tsx b/src/components/timeline-page/media-timeline/index.tsx index deb06fd3a..c908b0fdc 100644 --- a/src/components/timeline-page/media-timeline/index.tsx +++ b/src/components/timeline-page/media-timeline/index.tsx @@ -1,5 +1,4 @@ import { useMemo, useRef } from "react"; -import { useBreakpointValue } from "@chakra-ui/react"; import { Kind } from "nostr-tools"; import { Photo } from "react-photo-album"; @@ -14,6 +13,7 @@ import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import { NostrEvent } from "../../../types/nostr-event"; import { getEventUID } from "../../../helpers/nostr/events"; +import { useBreakpointValue } from "../../../providers/breakpoint-provider"; function GalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) { const ref = useRef(null); diff --git a/src/helpers/nostr/apps.ts b/src/helpers/nostr/apps.ts index 64af862df..abf012b65 100644 --- a/src/helpers/nostr/apps.ts +++ b/src/helpers/nostr/apps.ts @@ -1,3 +1,3 @@ export function buildAppSelectUrl(identifier: string, select = true) { - return `https://nostrapp.link/main/apps/social#${identifier}` + (select ? "?select=true" : ""); + return `https://nostrapp.link/#${identifier}` + (select ? "?select=true" : ""); } diff --git a/src/providers/breakpoint-provider.tsx b/src/providers/breakpoint-provider.tsx new file mode 100644 index 000000000..45da613ed --- /dev/null +++ b/src/providers/breakpoint-provider.tsx @@ -0,0 +1,79 @@ +import { PropsWithChildren, createContext, useContext } from "react"; +import { UseBreakpointOptions, useBreakpoint as useBaseBreakpoint, useTheme } from "@chakra-ui/react"; +import { isObject } from "@chakra-ui/shared-utils"; +import { arrayToObjectNotation } from "@chakra-ui/breakpoint-utils"; +import { breakpoints as defaultBreakPoints } from "@chakra-ui/breakpoint-utils"; + +// ChakraUIs useBreakpointValue renders twice, once with the fallback value then with the actual breakpoint value +// This causes a lot of re-renders and wasted processing. +// This provider is designed to solve that by providing the current breakpoint through context + +const BreakpointContext = createContext("base"); + +export function useBreakpoint(arg?: string | UseBreakpointOptions) { + return useContext(BreakpointContext) ?? (typeof arg === "object" ? arg.fallback : arg); +} + +// copied from https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/media-query/src/media-query.utils.ts +export function getClosestValue( + values: Record, + breakpoint: string, + breakpoints = defaultBreakPoints, +) { + let index = Object.keys(values).indexOf(breakpoint); + + if (index !== -1) { + return values[breakpoint]; + } + + let stopIndex = breakpoints.indexOf(breakpoint); + + while (stopIndex >= 0) { + const key = breakpoints[stopIndex]; + + if (values.hasOwnProperty(key)) { + index = stopIndex; + break; + } + stopIndex -= 1; + } + + if (index !== -1) { + const key = breakpoints[index]; + return values[key]; + } + + return undefined; +} + +// copied from https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/media-query/src/use-breakpoint-value.ts +export function useBreakpointValue( + values: Partial> | Array, + arg?: UseBreakpointOptions | string, +): T | undefined { + const opts = isObject(arg) ? arg : { fallback: arg ?? "base" }; + // NOTE: get the breakpoint from context instead of calling ChakraUIs useBreakpoint hook + const breakpoint = useBreakpoint(opts); + const theme = useTheme(); + + if (!breakpoint) return; + + /** + * Get the sorted breakpoint keys from the provided breakpoints + */ + const breakpoints = Array.from(theme.__breakpoints?.keys || []); + + const obj = Array.isArray(values) + ? Object.fromEntries( + Object.entries(arrayToObjectNotation(values, breakpoints)).map(([key, value]) => [key, value]), + ) + : values; + + return getClosestValue(obj, breakpoint, breakpoints); +} + +export default function BreakpointProvider({ children }: PropsWithChildren) { + const breakpoint = useBaseBreakpoint(); + + return {children}; +} diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 01adb4c94..132a4c434 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -11,6 +11,7 @@ import PostModalProvider from "./post-modal-provider"; import { DefaultEmojiProvider, UserEmojiProvider } from "./emoji-provider"; import { UserContactsUserDirectoryProvider } from "./user-directory-provider"; import MuteModalProvider from "./mute-modal-provider"; +import BreakpointProvider from "./breakpoint-provider"; // Top level providers, should be render as close to the root as possible export const GlobalProviders = ({ children }: { children: React.ReactNode }) => { @@ -30,22 +31,24 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) => /** Providers that provider functionality to pages (needs to be rendered under a router) */ export function PageProviders({ children }: { children: React.ReactNode }) { return ( - - - - - - - - - {children} - - - - - - - - + + + + + + + + + + {children} + + + + + + + + + ); } diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx index 7f6c99490..b23c766a4 100644 --- a/src/views/streams/stream/index.tsx +++ b/src/views/streams/stream/index.tsx @@ -14,7 +14,6 @@ import { Heading, Spacer, Spinner, - useBreakpointValue, useDisclosure, } from "@chakra-ui/react"; import { useParams, Navigate, useSearchParams, useNavigate } from "react-router-dom"; @@ -52,6 +51,7 @@ import StreamZapButton from "../components/stream-zap-button"; import StreamGoal from "../components/stream-goal"; import StreamShareButton from "../components/stream-share-button"; import VerticalPageLayout from "../../../components/vertical-page-layout"; +import { useBreakpointValue } from "../../../providers/breakpoint-provider"; function DesktopStreamPage({ stream }: { stream: ParsedStream }) { useAppTitle(stream.title); diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 269189370..75b2aea16 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -1,5 +1,6 @@ -import { Flex, Heading, IconButton, Spacer, useBreakpointValue } from "@chakra-ui/react"; +import { Flex, Heading, IconButton, Spacer } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; + import { EditIcon, GhostIcon } from "../../../components/icons"; import { UserAvatar } from "../../../components/user-avatar"; import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; @@ -9,6 +10,7 @@ import { useUserMetadata } from "../../../hooks/use-user-metadata"; import { UserProfileMenu } from "./user-profile-menu"; import { UserFollowButton } from "../../../components/user-follow-button"; import accountService from "../../../services/account"; +import { useBreakpointValue } from "../../../providers/breakpoint-provider"; export default function Header({ pubkey, diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index d50ec664d..5e010c4f8 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -99,27 +99,10 @@ const UserView = () => { const userTopRelays = useUserTopRelays(pubkey, relayCount); const relayModal = useDisclosure(); - const articleCount = useUserEventKindCount(pubkey, Kind.Article); - const streamCount = useUserEventKindCount(pubkey, STREAM_KIND); - const goalCount = useUserEventKindCount(pubkey, GOAL_KIND); - - const filteredTabs = useMemo( - () => - tabs.filter((t) => { - if (t.path === "streams" && streamCount === 0) return false; - if (t.path === "goals" && goalCount === 0) return false; - if (t.path === "articles" && articleCount === 0) return false; - return true; - }), - [streamCount, goalCount, articleCount], - ); - const matches = useMatches(); const lastMatch = matches[matches.length - 1]; - const activeTab = filteredTabs.indexOf( - filteredTabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? filteredTabs[0], - ); + const activeTab = tabs.indexOf(tabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? tabs[0]); const metadata = useUserMetadata(pubkey, userTopRelays, { alwaysRequest: true }); @@ -136,12 +119,12 @@ const UserView = () => { flexGrow="1" isLazy index={activeTab} - onChange={(v) => navigate(filteredTabs[v].path, { replace: true })} + onChange={(v) => navigate(tabs[v].path, { replace: true })} colorScheme="primary" h="full" > - {filteredTabs.map(({ label }) => ( + {tabs.map(({ label }) => ( {label} @@ -149,7 +132,7 @@ const UserView = () => { - {filteredTabs.map(({ label }) => ( + {tabs.map(({ label }) => ( }> diff --git a/yarn.lock b/yarn.lock index 34aa4cad8..28bd5e816 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1033,7 +1033,7 @@ "@chakra-ui/react-context" "2.1.0" "@chakra-ui/shared-utils" "2.0.5" -"@chakra-ui/breakpoint-utils@2.0.8": +"@chakra-ui/breakpoint-utils@2.0.8", "@chakra-ui/breakpoint-utils@^2.0.8": version "2.0.8" resolved "https://registry.yarnpkg.com/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz#750d3712668b69f6e8917b45915cee0e08688eed" integrity sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA== @@ -1232,7 +1232,7 @@ resolved "https://registry.yarnpkg.com/@chakra-ui/live-region/-/live-region-2.1.0.tgz#02b4b1d997075f19a7a9a87187e08c72e82ef0dd" integrity sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw== -"@chakra-ui/media-query@3.3.0": +"@chakra-ui/media-query@3.3.0", "@chakra-ui/media-query@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-3.3.0.tgz#40f9151dedb6a7af9df3be0474b59a799c92c619" integrity sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g== @@ -1596,6 +1596,11 @@ resolved "https://registry.yarnpkg.com/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz#cb2b49705e113853647f1822142619570feba081" integrity sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q== +"@chakra-ui/shared-utils@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@chakra-ui/shared-utils/-/shared-utils-2.0.4.tgz#8661f2b48dd93d04151b10a894a4290c9d9a080c" + integrity sha512-JGWr+BBj3PXGZQ2gxbKSD1wYjESbYsZjkCeE2nevyVk4rN3amV1wQzCnBAhsuJktMaZD6KC/lteo9ou9QUDzpA== + "@chakra-ui/skeleton@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-2.1.0.tgz#e3b25dd3afa330029d6d63be0f7cb8d44ad25531"