diff --git a/components/Blocks/TextBlock/index.tsx b/components/Blocks/TextBlock/index.tsx index 4d80912f..106ed215 100644 --- a/components/Blocks/TextBlock/index.tsx +++ b/components/Blocks/TextBlock/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useState } from "react"; +import { useRef, useEffect, useState, useLayoutEffect } from "react"; import { elementId } from "src/utils/elementId"; import { baseKeymap } from "prosemirror-commands"; import { keymap } from "prosemirror-keymap"; @@ -22,7 +22,6 @@ import { useUIState } from "src/useUIState"; import { addBlueskyPostBlock, addLinkBlock } from "src/utils/addLinkBlock"; import { BlockCommandBar } from "components/Blocks/BlockCommandBar"; import { useEditorStates } from "src/state/useEditorState"; -import { isIOS, useLayoutEffect } from "@react-aria/utils"; import { useEntitySetContext } from "components/EntitySetProvider"; import { useHandlePaste } from "./useHandlePaste"; import { highlightSelectionPlugin } from "./plugins"; @@ -35,6 +34,7 @@ import { useSmoker } from "components/Toast"; import { AddTiny } from "components/Icons/AddTiny"; import { BlockDocPageSmall } from "components/Icons/BlockDocPageSmall"; import { BlockImageSmall } from "components/Icons/BlockImageSmall"; +import { isIOS } from "src/utils/isDevice"; const HeadingStyle = { 1: "text-xl font-bold", diff --git a/components/HelpPopover.tsx b/components/HelpPopover.tsx index 96ed30b6..650cb064 100644 --- a/components/HelpPopover.tsx +++ b/components/HelpPopover.tsx @@ -1,5 +1,4 @@ "use client"; -import { isMac } from "@react-aria/utils"; import { ShortcutKey } from "./Layout"; import { Media } from "./Media"; import { Popover } from "./Popover"; @@ -8,6 +7,7 @@ import { useEntitySetContext } from "./EntitySetProvider"; import { useState } from "react"; import { ActionButton } from "components/ActionBar/ActionButton"; import { HelpSmall } from "./Icons/HelpSmall"; +import { isMac } from "src/utils/isDevice"; export const HelpPopover = (props: { noShortcuts?: boolean }) => { let entity_set = useEntitySetContext(); diff --git a/components/Input.tsx b/components/Input.tsx index 31a7969a..bebb32b1 100644 --- a/components/Input.tsx +++ b/components/Input.tsx @@ -1,7 +1,7 @@ "use client"; -import { isIOS } from "@react-aria/utils"; import { useCallback, useEffect, useRef, type JSX } from "react"; import { onMouseDown } from "src/utils/iosInputMouseDown"; +import { isIOS } from "src/utils/isDevice"; export function Input( props: React.DetailedHTMLProps< diff --git a/components/Toolbar/HighlightToolbar.tsx b/components/Toolbar/HighlightToolbar.tsx index 59c571eb..de93894d 100644 --- a/components/Toolbar/HighlightToolbar.tsx +++ b/components/Toolbar/HighlightToolbar.tsx @@ -22,7 +22,6 @@ import { useParams } from "next/navigation"; import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; import { Separator, ShortcutKey } from "components/Layout"; -import { isMac } from "@react-aria/utils"; import { ToolbarButton } from "."; import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; import { Props } from "components/Icons/Props"; @@ -30,6 +29,7 @@ import { PopoverArrow } from "components/Icons/PopoverArrow"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { PaintSmall } from "components/Icons/PaintSmall"; import { Color } from "react-aria-components"; +import { isMac } from "src/utils/isDevice"; export const HighlightButton = (props: { lastUsedHighlight: string; diff --git a/components/Toolbar/TextToolbar.tsx b/components/Toolbar/TextToolbar.tsx index 75564393..97fb4bda 100644 --- a/components/Toolbar/TextToolbar.tsx +++ b/components/Toolbar/TextToolbar.tsx @@ -1,4 +1,3 @@ -import { isMac } from "@react-aria/utils"; import { Separator, ShortcutKey } from "components/Layout"; import { metaKey } from "src/utils/metaKey"; import { LinkButton } from "./InlineLinkToolbar"; @@ -11,6 +10,7 @@ import { schema } from "components/Blocks/TextBlock/schema"; import { TextAlignmentButton } from "./TextAlignmentToolbar"; import { LockBlockButton } from "./LockBlockButton"; import { Props } from "components/Icons/Props"; +import { isMac } from "src/utils/isDevice"; export const TextToolbar = (props: { lastUsedHighlight: string; diff --git a/components/ViewportSizeLayout.tsx b/components/ViewportSizeLayout.tsx index 01f31beb..d6ff4e38 100644 --- a/components/ViewportSizeLayout.tsx +++ b/components/ViewportSizeLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { isIOS, useViewportSize } from "@react-aria/utils"; import { useEffect, useState } from "react"; +import { isIOS } from "src/utils/isDevice"; export function ViewportSizeLayout(props: { children: React.ReactNode }) { let viewheight = useViewportSize().height; @@ -53,3 +53,39 @@ function getViewportSize() { height: visualViewport?.height || window?.innerHeight, }; } + +export function useViewportSize(): { + width: number; + height: number; +} { + let [size, setSize] = useState(() => getViewportSize()); + + useEffect(() => { + // Use visualViewport api to track available height even on iOS virtual keyboard opening + let onResize = () => { + setSize((size) => { + let newSize = getViewportSize(); + if (newSize.width === size.width && newSize.height === size.height) { + return size; + } + return newSize; + }); + }; + + if (!visualViewport) { + window.addEventListener("resize", onResize); + } else { + visualViewport.addEventListener("resize", onResize); + } + + return () => { + if (!visualViewport) { + window.removeEventListener("resize", onResize); + } else { + visualViewport.removeEventListener("resize", onResize); + } + }; + }, []); + + return size; +} diff --git a/package-lock.json b/package-lock.json index 1a61bb0d..43252417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,12 @@ "@atproto/xrpc": "^0.6.9", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", + "@next/bundle-analyzer": "^15.3.2", "@next/mdx": "15.3.2", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-slider": "^1.3.4", "@radix-ui/react-tooltip": "^1.2.6", - "@react-aria/utils": "^3.24.1", "@react-spring/web": "^10.0.0-beta.0", "@rocicorp/undo": "^0.2.1", "@supabase/ssr": "^0.3.0", @@ -31,7 +31,6 @@ "@tiptap/core": "^2.11.5", "@types/mdx": "^2.0.13", "@vercel/analytics": "^1.3.1", - "@vercel/kv": "^1.0.1", "@vercel/sdk": "^1.3.1", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", @@ -791,6 +790,15 @@ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==" }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", @@ -2518,6 +2526,15 @@ "react": ">=16" } }, + "node_modules/@next/bundle-analyzer": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.3.2.tgz", + "integrity": "sha512-zY5O1PNKNxWEjaFX8gKzm77z2oL0cnj+m5aiqNBgay9LPLCDO13Cf+FJONeNq/nJjeXptwHFT9EMmTecF9U4Iw==", + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "15.3.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", @@ -2803,6 +2820,12 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -5958,14 +5981,6 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, - "node_modules/@upstash/redis": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.25.1.tgz", - "integrity": "sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==", - "dependencies": { - "crypto-js": "^4.2.0" - } - }, "node_modules/@vercel/analytics": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.3.1.tgz", @@ -5986,17 +6001,6 @@ } } }, - "node_modules/@vercel/kv": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-1.0.1.tgz", - "integrity": "sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==", - "dependencies": { - "@upstash/redis": "1.25.1" - }, - "engines": { - "node": ">=14.6" - } - }, "node_modules/@vercel/sdk": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vercel/sdk/-/sdk-1.3.1.tgz", @@ -6053,7 +6057,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -7135,11 +7138,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7588,6 +7586,12 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9255,6 +9259,21 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hanji": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/hanji/-/hanji-0.0.5.tgz", @@ -9671,6 +9690,12 @@ "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", "dev": true }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -10161,6 +10186,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -12003,6 +12037,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -12404,6 +12447,15 @@ "wrappy": "1" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14464,6 +14516,20 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -15115,6 +15181,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tr46": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", @@ -16198,6 +16273,63 @@ "node": ">=12" } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/whatwg-url": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", diff --git a/package.json b/package.json index 926bd933..f2706336 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "dev": "next dev", + "generate-db-types": "supabase gen types --local > supabase/database.types.ts && drizzle-kit introspect && rm -rf ./drizzle/*.sql ./drizzle/meta", "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* --yes", "wrangler-dev": "wrangler dev", "start-appview-dev": "tsx --env-file='./.env.local' --watch appview/index.ts", @@ -23,12 +24,12 @@ "@atproto/xrpc": "^0.6.9", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", + "@next/bundle-analyzer": "^15.3.2", "@next/mdx": "15.3.2", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-slider": "^1.3.4", "@radix-ui/react-tooltip": "^1.2.6", - "@react-aria/utils": "^3.24.1", "@react-spring/web": "^10.0.0-beta.0", "@rocicorp/undo": "^0.2.1", "@supabase/ssr": "^0.3.0", @@ -36,7 +37,6 @@ "@tiptap/core": "^2.11.5", "@types/mdx": "^2.0.13", "@vercel/analytics": "^1.3.1", - "@vercel/kv": "^1.0.1", "@vercel/sdk": "^1.3.1", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", diff --git a/src/shortcuts.ts b/src/shortcuts.ts index 3694ab19..fa66fff3 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -1,7 +1,7 @@ -import { isIOS, isMac } from "@react-aria/utils"; import { useUIState } from "./useUIState"; import { Replicache } from "replicache"; import { ReplicacheMutators } from "./replicache"; +import { isMac } from "./utils/isDevice"; type Shortcut = { metaKey?: boolean; diff --git a/src/utils/iosInputMouseDown.ts b/src/utils/iosInputMouseDown.ts index c083e876..c10456ad 100644 --- a/src/utils/iosInputMouseDown.ts +++ b/src/utils/iosInputMouseDown.ts @@ -1,4 +1,4 @@ -import { isIOS } from "@react-aria/utils"; +import { isIOS } from "./isDevice"; export function onMouseDown(e: React.MouseEvent) { if (!isIOS()) return; diff --git a/src/utils/isDevice.ts b/src/utils/isDevice.ts new file mode 100644 index 00000000..ef36fd5b --- /dev/null +++ b/src/utils/isDevice.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +function testUserAgent(re: RegExp) { + if (typeof window === "undefined" || window.navigator == null) { + return false; + } + return ( + //@ts-ignore + window.navigator["userAgentData"]?.brands.some( + (brand: { brand: string; version: string }) => re.test(brand.brand), + ) || re.test(window.navigator.userAgent) + ); +} + +function testPlatform(re: RegExp) { + return typeof window !== "undefined" && window.navigator != null + ? re.test( + //@ts-ignore + window.navigator["userAgentData"]?.platform || + window.navigator.platform, + ) + : false; +} + +function cached(fn: () => boolean) { + if (process.env.NODE_ENV === "test") { + return fn; + } + + let res: boolean | null = null; + return () => { + if (res == null) { + res = fn(); + } + return res; + }; +} + +export const isMac = cached(function () { + return testPlatform(/^Mac/i); +}); + +export const isIPhone = cached(function () { + return testPlatform(/^iPhone/i); +}); + +export const isIPad = cached(function () { + return ( + testPlatform(/^iPad/i) || + // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support. + (isMac() && navigator.maxTouchPoints > 1) + ); +}); + +export const isIOS = cached(function () { + return isIPhone() || isIPad(); +}); + +export const isAppleDevice = cached(function () { + return isMac() || isIOS(); +}); + +export const isWebKit = cached(function () { + return testUserAgent(/AppleWebKit/i) && !isChrome(); +}); + +export const isChrome = cached(function () { + return testUserAgent(/Chrome/i); +}); + +export const isAndroid = cached(function () { + return testUserAgent(/Android/i); +}); + +export const isFirefox = cached(function () { + return testUserAgent(/Firefox/i); +});