fix issue with breakpoints causing re-render

This commit is contained in:
hzrd149 2023-10-13 10:24:55 -05:00
parent 85a9dad33a
commit ed1cb04235
17 changed files with 133 additions and 63 deletions

View File

@ -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",

View File

@ -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<CardProps, "children"> & { event: NostrEvent }) {
const stream = parseStreamEvent(event);

View File

@ -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);

View File

@ -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 });

View File

@ -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<ButtonGroupProps, "children"> & { event: NostrEvent }) {
const reactions = useEventReactions(event.id) ?? [];

View File

@ -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<CardProps, "children"> & {
event: NostrEvent;

View File

@ -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;

View File

@ -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<CardProps, "children">) {
const { value: data } = useOpenGraphData(url);

View File

@ -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();

View File

@ -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<HTMLImageElement | null>(null);

View File

@ -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" : "");
}

View File

@ -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<T = any>(
values: Record<string, T>,
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<T = any>(
values: Partial<Record<string, T>> | Array<T | null>,
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<any>(
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 <BreakpointContext.Provider value={breakpoint}>{children}</BreakpointContext.Provider>;
}

View File

@ -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 (
<SigningProvider>
<DeleteEventProvider>
<MuteModalProvider>
<InvoiceModalProvider>
<NotificationTimelineProvider>
<DefaultEmojiProvider>
<UserEmojiProvider>
<UserContactsUserDirectoryProvider>
<PostModalProvider>{children}</PostModalProvider>
</UserContactsUserDirectoryProvider>
</UserEmojiProvider>
</DefaultEmojiProvider>
</NotificationTimelineProvider>
</InvoiceModalProvider>
</MuteModalProvider>
</DeleteEventProvider>
</SigningProvider>
<BreakpointProvider>
<SigningProvider>
<DeleteEventProvider>
<MuteModalProvider>
<InvoiceModalProvider>
<NotificationTimelineProvider>
<DefaultEmojiProvider>
<UserEmojiProvider>
<UserContactsUserDirectoryProvider>
<PostModalProvider>{children}</PostModalProvider>
</UserContactsUserDirectoryProvider>
</UserEmojiProvider>
</DefaultEmojiProvider>
</NotificationTimelineProvider>
</InvoiceModalProvider>
</MuteModalProvider>
</DeleteEventProvider>
</SigningProvider>
</BreakpointProvider>
);
}

View File

@ -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);

View File

@ -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,

View File

@ -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"
>
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
{filteredTabs.map(({ label }) => (
{tabs.map(({ label }) => (
<Tab key={label} whiteSpace="pre">
{label}
</Tab>
@ -149,7 +132,7 @@ const UserView = () => {
</TabList>
<TabPanels>
{filteredTabs.map(({ label }) => (
{tabs.map(({ label }) => (
<TabPanel key={label} p={0}>
<ErrorBoundary>
<Suspense fallback={<Spinner />}>

View File

@ -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"