diff --git a/__tests__/components/header/user/proxy/HeaderUserProxyDropdown.test.tsx b/__tests__/components/header/user/HeaderUserMenuDropdown.test.tsx similarity index 94% rename from __tests__/components/header/user/proxy/HeaderUserProxyDropdown.test.tsx rename to __tests__/components/header/user/HeaderUserMenuDropdown.test.tsx index 1e1f0de990..8d5a494831 100644 --- a/__tests__/components/header/user/proxy/HeaderUserProxyDropdown.test.tsx +++ b/__tests__/components/header/user/HeaderUserMenuDropdown.test.tsx @@ -1,12 +1,11 @@ import React from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import HeaderUserProxyDropdown from "@/components/header/user/proxy/HeaderUserProxyDropdown"; +import HeaderUserMenuDropdown from "@/components/header/user/HeaderUserMenuDropdown"; import { AuthContext } from "@/components/auth/Auth"; -jest.mock( - "@/components/header/user/proxy/HeaderUserProxyDropdownItem", - () => () =>
-); +jest.mock("@/components/header/user/HeaderUserProxyDropdownItem", () => () => ( +
+)); jest.mock( "@/components/header/user/connected/HeaderUserConnectedAccounts", () => (props: any) => ( @@ -62,7 +61,7 @@ function renderDropdown(options: any) { const onClose = jest.fn(); render( - jest.clearAllMocks()); -describe("HeaderUserProxyDropdown", () => { +describe("HeaderUserMenuDropdown", () => { it("shows profile handle as label", () => { renderDropdown({ profile: profileBase, diff --git a/__tests__/components/header/user/proxy/HeaderUserProxyDropdownItem.test.tsx b/__tests__/components/header/user/HeaderUserProxyDropdownItem.test.tsx similarity index 50% rename from __tests__/components/header/user/proxy/HeaderUserProxyDropdownItem.test.tsx rename to __tests__/components/header/user/HeaderUserProxyDropdownItem.test.tsx index b0cf194e36..9e9cc860cb 100644 --- a/__tests__/components/header/user/proxy/HeaderUserProxyDropdownItem.test.tsx +++ b/__tests__/components/header/user/HeaderUserProxyDropdownItem.test.tsx @@ -1,14 +1,14 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import HeaderUserProxyDropdownItem from '@/components/header/user/proxy/HeaderUserProxyDropdownItem'; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import HeaderUserProxyDropdownItem from "@/components/header/user/HeaderUserProxyDropdownItem"; const profile = { id: 1, - created_by: { handle: 'alice', pfp: 'img.png' } + created_by: { handle: "alice", pfp: "img.png" }, }; -describe('HeaderUserProxyDropdownItem', () => { - it('activates proxy when not active', async () => { +describe("HeaderUserProxyDropdownItem", () => { + it("activates proxy when not active", async () => { const activate = jest.fn(); render( { onActivateProfileProxy={activate} /> ); - const btn = screen.getByRole('button'); + const btn = screen.getByRole("button"); await userEvent.click(btn); expect(activate).toHaveBeenCalledWith(profile); - expect(screen.getByText('alice')).toBeInTheDocument(); + expect(screen.getByText("alice")).toBeInTheDocument(); }); - it('deactivates proxy when active', async () => { + it("deactivates proxy when active", async () => { const activate = jest.fn(); render( { onActivateProfileProxy={activate} /> ); - const btn = screen.getByRole('button'); + const btn = screen.getByRole("button"); await userEvent.click(btn); expect(activate).toHaveBeenCalledWith(null); - expect(btn.querySelector('svg')).toBeInTheDocument(); + expect(btn.querySelector("svg")).toBeInTheDocument(); }); }); diff --git a/components/header/share/HeaderShare.tsx b/components/header/share/HeaderShare.tsx index d36524e9c5..dccffc6ec0 100644 --- a/components/header/share/HeaderShare.tsx +++ b/components/header/share/HeaderShare.tsx @@ -97,7 +97,7 @@ export default function HeaderShare({ ); } -function HeaderQRModal({ +export function HeaderQRModal({ show, onClose, }: { diff --git a/components/header/user/proxy/HeaderUserProxyDropdown.tsx b/components/header/user/HeaderUserMenuDropdown.tsx similarity index 87% rename from components/header/user/proxy/HeaderUserProxyDropdown.tsx rename to components/header/user/HeaderUserMenuDropdown.tsx index f1f29a47a7..f7ffff0c89 100644 --- a/components/header/user/proxy/HeaderUserProxyDropdown.tsx +++ b/components/header/user/HeaderUserMenuDropdown.tsx @@ -7,23 +7,26 @@ import { faRightFromBracket, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ShareIcon } from "@heroicons/react/24/outline"; import { AnimatePresence, motion } from "framer-motion"; import { useContext, useEffect, useState } from "react"; import { AuthContext } from "@/components/auth/Auth"; import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import type { ApiProfileProxy } from "@/generated/models/ApiProfileProxy"; -import HeaderUserConnectedAccounts from "../connected/HeaderUserConnectedAccounts"; +import HeaderUserConnectedAccounts from "./connected/HeaderUserConnectedAccounts"; import HeaderUserProxyDropdownItem from "./HeaderUserProxyDropdownItem"; -export default function HeaderUserProxyDropdown({ +export default function HeaderUserMenuDropdown({ isOpen, profile, onClose, + onOpenShare, }: { readonly isOpen: boolean; readonly profile: ApiIdentity; readonly onClose: () => void; + readonly onOpenShare?: (() => void) | undefined; }) { const { address, @@ -126,17 +129,14 @@ export default function HeaderUserProxyDropdown({ >
-
+
    {availableConnectedAccounts.length > 0 && ( -
    +
  • ({ ...account, unreadNotificationsCount: - connectedAccountUnreadNotifications[ + connectedAccountUnreadNotifications?.[ account.address.toLowerCase() ] ?? 0, }))} @@ -151,10 +151,10 @@ export default function HeaderUserProxyDropdown({ }); }} /> -
  • + )} {hasProxySection && ( -
    +
  • Proxy Profile

    @@ -179,16 +179,16 @@ export default function HeaderUserProxyDropdown({ > Profile Picture
  • ) : (
    ))} -
    + )} -
    +
  • {isConnected ? ( )} -
  • -
    + + {onOpenShare && ( +
  • + +
  • + )} +
  • )} -
  • -
+ +
diff --git a/components/header/user/proxy/HeaderUserProxyDropdownItem.tsx b/components/header/user/HeaderUserProxyDropdownItem.tsx similarity index 64% rename from components/header/user/proxy/HeaderUserProxyDropdownItem.tsx rename to components/header/user/HeaderUserProxyDropdownItem.tsx index f14ec57708..0cb1557e82 100644 --- a/components/header/user/proxy/HeaderUserProxyDropdownItem.tsx +++ b/components/header/user/HeaderUserProxyDropdownItem.tsx @@ -18,17 +18,15 @@ export default function HeaderUserProxyDropdownItem({ {showUserMenu && profile && (
- setShowUserMenu(false)} + onOpenShare={canUseDesktopShare ? onOpenShare : undefined} />
)} + setShowShareModal(false)} + />
); } diff --git a/components/nft-image/utils/arweave-fallback.ts b/components/nft-image/utils/arweave-fallback.ts index a76b9414e1..c12c3079ac 100644 --- a/components/nft-image/utils/arweave-fallback.ts +++ b/components/nft-image/utils/arweave-fallback.ts @@ -1,28 +1,12 @@ import type React from "react"; -const ARWEAVE_GATEWAYS_PRIORITY: readonly string[] = [ +const ARWEAVE_GATEWAYS: readonly string[] = [ "arweave.net", "gateway.arweave.net", - "g8way.io", -] as const; - -const ARWEAVE_GATEWAYS_LONG_TAIL: readonly string[] = [ - "arweave.org", - "arweave.dev", + "gateway.ar.io", "ar-io.net", - "arweave.live", - "arweave.surf", - "arweave.team", - "arweavetoday.com", - "arweave.fyi", - "arweave.guide", ] as const; -const ARWEAVE_GATEWAYS: readonly string[] = dedupe([ - ...ARWEAVE_GATEWAYS_PRIORITY, - ...ARWEAVE_GATEWAYS_LONG_TAIL, -]); - function dedupe(list: readonly string[]): string[] { return Array.from(new Set(list)); } diff --git a/hooks/isMobileDevice.ts b/hooks/isMobileDevice.ts index b35ebb6a9a..191e47f74f 100644 --- a/hooks/isMobileDevice.ts +++ b/hooks/isMobileDevice.ts @@ -2,17 +2,30 @@ import { useEffect, useState } from "react"; -export default function useIsMobileDevice() { - const [isMobile, setIsMobile] = useState(false); +const MOBILE_DEVICE_REGEX = + /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i; + +const detectIsMobileDevice = (): boolean => { + const userAgent = typeof navigator === "undefined" ? "" : navigator.userAgent; + return MOBILE_DEVICE_REGEX.test(userAgent); +}; + +export function useIsMobileDeviceStatus() { + const [status, setStatus] = useState({ + isMobileDevice: false, + isDeviceDetectionResolved: false, + }); useEffect(() => { - const userAgent = - typeof navigator === "undefined" ? "" : navigator.userAgent; - const regex = - /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i; - const mobile = regex.exec(userAgent) !== null; - setIsMobile(mobile); + setStatus({ + isMobileDevice: detectIsMobileDevice(), + isDeviceDetectionResolved: true, + }); }, []); - return isMobile; + return status; +} + +export default function useIsMobileDevice() { + return useIsMobileDeviceStatus().isMobileDevice; } diff --git a/package-lock.json b/package-lock.json index 73c6480d2e..ac79bd804a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2423,6 +2423,29 @@ "node": ">=18" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -14693,68 +14716,47 @@ } }, "node_modules/eslint-plugin-sonarjs": { - "version": "3.0.5", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-3.0.7.tgz", + "integrity": "sha512-62jB20krIPvcwBLAyG3VVKa2ce2j2lL1yCb8Y0ylMRR/dLvCCTiQx8gQbXb+G81k1alPZ2/I3muZinqWQdBbzw==", "dev": true, "license": "LGPL-3.0-only", "dependencies": { - "@eslint-community/regexpp": "4.12.1", + "@eslint-community/regexpp": "4.12.2", "builtin-modules": "3.3.0", "bytes": "3.1.2", "functional-red-black-tree": "1.0.1", "jsx-ast-utils-x": "0.1.0", "lodash.merge": "4.6.2", - "minimatch": "9.0.5", + "minimatch": "10.1.2", "scslre": "0.3.0", - "semver": "7.7.2", + "semver": "7.7.4", "typescript": ">=5" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-sonarjs/node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/eslint-plugin-sonarjs/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-sonarjs/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/eslint-plugin-sonarjs/node_modules/semver": { - "version": "7.7.2", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -16097,9 +16099,9 @@ } }, "node_modules/hono": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", - "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -16287,7 +16289,9 @@ } }, "node_modules/immutable": { - "version": "5.1.4", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, "node_modules/import-fresh": {