diff --git a/.circleci/src/commands/@web-commands.yml b/.circleci/src/commands/@web-commands.yml index 13121e6fa50..80e53e4c623 100644 --- a/.circleci/src/commands/@web-commands.yml +++ b/.circleci/src/commands/@web-commands.yml @@ -153,12 +153,6 @@ web-distribute: $SLACK_DAILY_DEPLOY_WEBHOOK fi -web-install-wrangler: - steps: - - run: - name: install wrangler - command: 'cd packages/web && npm install @cloudflare/wrangler' - web-deploy-cloudflare: parameters: build-type: @@ -174,20 +168,11 @@ web-deploy-cloudflare: - checkout - attach_workspace: at: ./ - - web-install-wrangler - # - run: - # name: Move sourcemaps - # command: | - # cd packages/web - # mkdir -p sourcemaps/static/js - # mv build-<< parameters.build-type >>/static/js/*.map sourcemaps/static/js - run: - name: Set up workers site + name: Move build command: | - cd packages/web/scripts/workers-site - npm i - cd ../../ mv build-<< parameters.build-type >> build + mv build-ssr-<< parameters.build-type >> build-ssr - run: name: Copy robots.txt command: | @@ -198,6 +183,7 @@ web-deploy-cloudflare: command: | cd packages/web echo ${GA_ACCESS_TOKEN} | npx wrangler secret put GA_ACCESS_TOKEN --env << parameters.environment >> + npx wrangler publish --config ./src/ssr/wrangler.toml --env << parameters.environment >> npx wrangler publish --env << parameters.environment >> - run: name: slack announce diff --git a/.circleci/src/jobs/@web-jobs.yml b/.circleci/src/jobs/@web-jobs.yml index ae6d69b0d44..0ac2706ba4d 100644 --- a/.circleci/src/jobs/@web-jobs.yml +++ b/.circleci/src/jobs/@web-jobs.yml @@ -76,6 +76,17 @@ web-build-staging: build-directory: packages/web/build-staging build-name: build-staging +web-build-ssr-staging: + working_directory: ~/audius-protocol + docker: + - image: cimg/node:18.17 + resource_class: xlarge + steps: + - web-build: + build-type: ssr:stage + build-directory: packages/web/build-ssr-staging + build-name: build-ssr-staging + web-test-staging: working_directory: ~/audius-protocol resource_class: large @@ -108,6 +119,17 @@ web-build-production: build-directory: packages/web/build-production build-name: build-production +web-build-ssr-production: + working_directory: ~/audius-protocol + docker: + - image: cimg/node:18.17 + resource_class: xlarge + steps: + - web-build: + build-type: ssr:prod + build-directory: packages/web/build-ssr-production + build-name: build-ssr-production + web-deploy-demo: working_directory: ~/audius-protocol docker: diff --git a/.gitignore b/.gitignore index 71807074b0f..b434a36e777 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,6 @@ combined-patch-file.txt # Identity packages/identity-service/emailCache + +# CloudFlare +.wrangler \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8524df61d10..621cd5307d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,7 +3,6 @@ "version": "1.5.63", "lockfileVersion": 3, "requires": true, - "cacheBust": false, "packages": { "": { "name": "root", @@ -52,6 +51,7 @@ "standard": "17.0.0", "turbo": "1.10.14", "typescript": "5.0.4", + "vike": "^0.4.150", "wait-on": "7.2.0" } }, @@ -2712,6 +2712,34 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/@brillout/import": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@brillout/import/-/import-0.2.3.tgz", + "integrity": "sha512-1T8WlD75eeFSMrptGy8jiLHmfHgMmSjWvLOIUvHmSVZt+6k0eQqYUoK4KbmE4T9pVLIfxvZSOm2D68VEqKRHRw==" + }, + "node_modules/@brillout/json-serializer": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@brillout/json-serializer/-/json-serializer-0.5.8.tgz", + "integrity": "sha512-vEuXw30ok+mJfJutOxXKBb4lBJ0HymA7lev9PcYK6W/hzjhCTPk9Bdk85HrcNcKZWRQiwoWtw0F2Di4TRJ7ssQ==" + }, + "node_modules/@brillout/picocolors": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@brillout/picocolors/-/picocolors-1.0.10.tgz", + "integrity": "sha512-dh+JJlsBf3QYX+91Ezma8RLKNOjGDoBBmORv/NzRpQuasdyzwQCMXGGjsDu12ZhGz92TqQbL9pv79rvbheI21A==" + }, + "node_modules/@brillout/require-shim": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@brillout/require-shim/-/require-shim-0.1.2.tgz", + "integrity": "sha512-3I4LRHnVZXoSAsEoni5mosq9l6eiJED58d9V954W4CIZ88AUfYBanWGBGbJG3NztaRTpFHEA6wB3Hn93BmmJdg==" + }, + "node_modules/@brillout/vite-plugin-import-build": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@brillout/vite-plugin-import-build/-/vite-plugin-import-build-0.2.22.tgz", + "integrity": "sha512-n5sv0HdCB5WC2QJSnTN6iS/F+sJsF0AmtsCCaQ+5+dRjgsoGGsa3auinJV8tuEog5WsX+3MF8RIwn3A/u0e04w==", + "dependencies": { + "@brillout/import": "^0.2.3" + } + }, "node_modules/@certusone/wormhole-sdk": { "version": "0.1.1", "license": "Apache-2.0", @@ -10275,8 +10303,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.23", - "license": "MIT" + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==" }, "node_modules/@project-serum/anchor": { "version": "0.24.1", @@ -42449,7 +42478,6 @@ }, "node_modules/cac": { "version": "6.7.14", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -57898,6 +57926,14 @@ "url": "https://github.com/sponsors/gjtorikian/" } }, + "node_modules/isbot-fast": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isbot-fast/-/isbot-fast-1.2.0.tgz", + "integrity": "sha512-twjuQzy2gKMDVfKGQyQqrx6Uy4opu/fiVUTTpdqtFsd7OQijIp5oXvb27n5EemYXaijh5fomndJt/SPRLsEdSg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/isemail": { "version": "3.2.0", "dev": true, @@ -87377,6 +87413,20 @@ "react-dom": ">= 16.8.0" } }, + "node_modules/react-streaming": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/react-streaming/-/react-streaming-0.3.19.tgz", + "integrity": "sha512-kpxnj/nynMbdVVLUZKAI+AlSx7bJNX0WZO14LSJNtfIcbQJd7YH0ilnP+AFwKkvrjuG8zuNL36W5WCsczM62zg==", + "dependencies": { + "@brillout/import": "^0.2.3", + "@brillout/json-serializer": "^0.5.1", + "isbot-fast": "1.2.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "dev": true, @@ -101736,6 +101786,460 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vike": { + "version": "0.4.150", + "resolved": "https://registry.npmjs.org/vike/-/vike-0.4.150.tgz", + "integrity": "sha512-R2cfpRWTZb0WOgMDsh5oLGq9yJV4aPjEyjpnvy0Rkntv9YpX4EXp9MmNHUTqNJJqBeURtJKHQpfPbFRgbPjIbQ==", + "dependencies": { + "@brillout/import": "0.2.3", + "@brillout/json-serializer": "^0.5.8", + "@brillout/picocolors": "^1.0.10", + "@brillout/require-shim": "^0.1.2", + "@brillout/vite-plugin-import-build": "^0.2.20", + "acorn": "^8.8.2", + "cac": "^6.7.14", + "es-module-lexer": "^1.3.0", + "esbuild": "^0.17.18", + "fast-glob": "^3.2.12", + "sirv": "^2.0.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "vike": "node/cli/bin-entry.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "react-streaming": ">=0.3.5", + "vite": ">=3.1.0" + }, + "peerDependenciesMeta": { + "react-streaming": { + "optional": true + } + } + }, + "node_modules/vike-react": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/vike-react/-/vike-react-0.3.9.tgz", + "integrity": "sha512-klqi6eKdiDRsYbBKPJKXTf9cKOVIRrapZ+SG6melt1IdsQwbzZX0WeHXKn7tfHcKL7HqxMmPuamLJYQA06hVdw==", + "dependencies": { + "react-streaming": "^0.3.19" + }, + "peerDependencies": { + "react": "18.x.x", + "react-dom": "18.x.x", + "vike": "^0.4.151", + "vite": "^4.3.8 || ^5.0.10" + } + }, + "node_modules/vike/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vike/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/vike/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/vike/node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/vike/node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/vike/node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/vite": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", @@ -131024,7 +131528,7 @@ "bs58": "4.0.1", "cipher-base": "1.0.4", "crc-32": "1.2.2", - "cross-fetch": "3.1.5", + "cross-fetch": "4.0.0", "elliptic": "6.5.4", "esm": "3.2.25", "eth-sig-util": "2.5.4", @@ -131800,6 +132304,14 @@ "wrap-ansi": "^7.0.0" } }, + "packages/libs/node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "packages/libs/node_modules/eth-sig-util": { "version": "2.5.4", "license": "ISC", @@ -132298,6 +132810,25 @@ "node": ">= 10.13" } }, + "packages/libs/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "packages/libs/node_modules/nyc": { "version": "15.1.0", "dev": true, @@ -132634,6 +133165,11 @@ "node": ">=8.0" } }, + "packages/libs/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "packages/libs/node_modules/ts-mocha": { "version": "9.0.2", "dev": true, @@ -132765,6 +133301,20 @@ "uuid": "dist/bin/uuid" } }, + "packages/libs/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "packages/libs/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "packages/libs/node_modules/workerpool": { "version": "6.2.0", "dev": true, @@ -141714,6 +142264,7 @@ "@audius/sdk": "*", "@audius/stems": "*", "@audius/trpc-server": "*", + "@cloudflare/kv-asset-handler": "0.2.0", "@coinbase/cbpay-js": "1.2.0", "@coinflowlabs/react": "3.1.5", "@emotion/css": "^11.11.2", @@ -141758,6 +142309,7 @@ "clamp": "1.0.1", "classnames": "2.2.6", "connected-react-router": "6.9.3", + "cross-fetch": "^4.0.0", "electron-log": "5.0.3", "electron-updater": "6.1.7", "exif-parser": "0.1.12", @@ -141830,6 +142382,8 @@ "type-fest": "2.16.0", "typed-redux-saga": "1.3.1", "typesafe-actions": "5.1.0", + "vike": "0.4.150", + "vike-react": "^0.3.7", "walletlink": "2.0.3", "wasm-loader": "1.3.0", "web-vitals": "0.2.2", @@ -141913,6 +142467,25 @@ "vitest": "0.34.6" } }, + "packages/web/node_modules/@cloudflare/kv-asset-handler": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz", + "integrity": "sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==", + "dependencies": { + "mime": "^3.0.0" + } + }, + "packages/web/node_modules/@cloudflare/kv-asset-handler/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "packages/web/node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -142469,6 +143042,38 @@ "node": ">=11" } }, + "packages/web/node_modules/@project-serum/anchor/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "packages/web/node_modules/@project-serum/anchor/node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "packages/web/node_modules/@project-serum/anchor/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "packages/web/node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -142865,6 +143470,33 @@ "hasInstallScript": true, "license": "MIT" }, + "packages/web/node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "packages/web/node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "packages/web/node_modules/decode-uri-component": { "version": "0.2.2", "license": "MIT", @@ -143628,6 +144260,11 @@ "node": ">=8" } }, + "packages/web/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "packages/web/node_modules/tslib": { "version": "2.6.2", "license": "0BSD" @@ -143650,6 +144287,20 @@ "node": ">= 10.0.0" } }, + "packages/web/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "packages/web/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "packages/web/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 8b1ac2fc815..b454bb1aa6a 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "prettier-config-standard": "6.0.0", "standard": "17.0.0", "turbo": "1.10.14", + "vike": "^0.4.150", "typescript": "5.0.4", "wait-on": "7.2.0" }, diff --git a/packages/common/src/audius-query/AudiusQueryContext.ts b/packages/common/src/audius-query/AudiusQueryContext.ts index 5133bf5689e..16fa0335e6f 100644 --- a/packages/common/src/audius-query/AudiusQueryContext.ts +++ b/packages/common/src/audius-query/AudiusQueryContext.ts @@ -19,8 +19,8 @@ export type AudiusQueryContextType = { remoteConfigInstance: RemoteConfigInstance } -export const AudiusQueryContext = createContext( - null +export const AudiusQueryContext = createContext( + null as any ) export const useAudiusQueryContext = () => { diff --git a/packages/common/src/models/SsrPageProps.ts b/packages/common/src/models/SsrPageProps.ts new file mode 100644 index 00000000000..ca0b430aea7 --- /dev/null +++ b/packages/common/src/models/SsrPageProps.ts @@ -0,0 +1,6 @@ +import { full as FullSdk } from '@audius/sdk' + +export type SsrPageProps = { + track?: FullSdk.TrackFull + error?: { isErrorPageOpen: boolean } +} diff --git a/packages/common/src/models/index.ts b/packages/common/src/models/index.ts index 2968a660e8b..be356054196 100644 --- a/packages/common/src/models/index.ts +++ b/packages/common/src/models/index.ts @@ -27,6 +27,7 @@ export * from './PurchaseContent' export * from './Repost' export * from './Services' export * from './SmartCollectionVariant' +export * from './SsrPageProps' export * from './Status' export * from './Stems' export * from './Theme' diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index 77f6b4baaf2..7bc2829ce2d 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -249,7 +249,6 @@ type AudiusBackendParams = { identityServiceUrl: Maybe generalAdmissionUrl: Maybe isElectron: Maybe - isMobile: Maybe localStorage?: LocalStorage monitoringCallbacks: MonitoringCallbacks nativeMobile: Maybe @@ -294,7 +293,6 @@ export const audiusBackend = ({ identityServiceUrl, generalAdmissionUrl, isElectron, - isMobile, localStorage, monitoringCallbacks, nativeMobile, @@ -3163,7 +3161,8 @@ export const audiusBackend = ({ endpoints, AAOEndpoint, parallelization, - feePayerOverride + feePayerOverride, + source }: { challenges: { challenge_id: ChallengeRewardID @@ -3180,12 +3179,12 @@ export const audiusBackend = ({ parallelization: number feePayerOverride: Nullable isFinalAttempt: boolean + source: 'mobile' | 'electron' | 'web' }) { await waitForLibsInit() try { if (!challenges.length) return - const source = isMobile ? 'mobile' : isElectron ? 'electron' : 'web' const reporter = new ClientRewardsReporter({ libs: audiusLibs, recordAnalytics, diff --git a/packages/common/src/services/audius-backend/solana.ts b/packages/common/src/services/audius-backend/solana.ts index 200d83a0060..aefe5659a6c 100644 --- a/packages/common/src/services/audius-backend/solana.ts +++ b/packages/common/src/services/audius-backend/solana.ts @@ -19,8 +19,7 @@ import { AudiusBackend } from './AudiusBackend' const DEFAULT_RETRY_DELAY = 1000 const DEFAULT_MAX_RETRY_COUNT = 120 - -const PLACEHOLDER_SIGNATURE = Buffer.from(new Array(64).fill(0)) +const PLACEHOLDER_SIGNATURE = new Array(64).fill(0) const RECOVERY_MEMO_STRING = 'recovery' /** @@ -598,6 +597,7 @@ export const relayVersionedTransaction = async ( skipPreflight?: boolean } ) => { + const placeholderSignature = Buffer.from(PLACEHOLDER_SIGNATURE) const libs = await audiusBackendInstance.getAudiusLibsTyped() const decompiledMessage = TransactionMessage.decompile(transaction.message, { addressLookupTableAccounts @@ -608,7 +608,7 @@ export const relayVersionedTransaction = async ( publicKey: publicKey.toBase58(), signature: Buffer.from(transaction.signatures[index]) })) - .filter((meta) => !meta.signature.equals(PLACEHOLDER_SIGNATURE)) + .filter((meta) => !meta.signature.equals(placeholderSignature)) return await libs.solanaWeb3Manager!.transactionHandler.handleTransaction({ instructions: decompiledMessage.instructions, recentBlockhash: decompiledMessage.recentBlockhash, diff --git a/packages/common/src/services/local-storage/LocalStorage.ts b/packages/common/src/services/local-storage/LocalStorage.ts index a59dae02fdc..d6332960e09 100644 --- a/packages/common/src/services/local-storage/LocalStorage.ts +++ b/packages/common/src/services/local-storage/LocalStorage.ts @@ -1,8 +1,8 @@ import { CURRENT_USER_EXISTS_LOCAL_STORAGE_KEY } from '@audius/sdk/dist/core' +import { User } from 'models/User' import { PLAYBACK_RATE_LS_KEY } from 'store/index' -import { User } from '../../models' import { Nullable } from '../../utils' // TODO: the following should come from @audius/libs/dist/core when diff --git a/packages/common/src/store/cache/tracks/reducer.ts b/packages/common/src/store/cache/tracks/reducer.ts index 736437c9edf..7b50bb9ead2 100644 --- a/packages/common/src/store/cache/tracks/reducer.ts +++ b/packages/common/src/store/cache/tracks/reducer.ts @@ -1,8 +1,13 @@ +import snakecaseKeys from 'snakecase-keys' + import { Cache } from 'models/Cache' import { ID } from 'models/Identifiers' import { Kind } from 'models/Kind' +import { SsrPageProps } from 'models/SsrPageProps' import { Track } from 'models/Track' +import { makeTrack } from 'services/audius-api-client/ResponseAdapter' import { initialCacheState } from 'store/cache/reducer' +import { makeUid } from 'utils/uid' import { AddEntriesAction, @@ -72,10 +77,47 @@ const actionsMap = { } } -const reducer = (state = initialState, action: AddSuccededAction) => { - const matchingReduceFunction = actionsMap[action.type] - if (!matchingReduceFunction) return state - return matchingReduceFunction(state, action) +const buildInitialState = (ssrPageProps?: SsrPageProps) => { + // If we have preloaded data from the server, populate the initial + // cache state with it + if (ssrPageProps?.track) { + // @ts-ignore + const track = makeTrack(snakecaseKeys(ssrPageProps.track)) + if (!track) return initialState + + const id = track.track_id + const uid = makeUid(Kind.TRACKS, id) + + return { + ...initialState, + entries: { + [id]: { + metadata: track, + _timestamp: Date.now() + } + }, + uids: { + [uid]: track.track_id + }, + statuses: { + [id]: 'SUCCESS' + } + } + } + return initialState } +const reducer = + (ssrPageProps: SsrPageProps) => + (state: TracksCacheState, action: AddSuccededAction) => { + if (!state) { + // @ts-ignore + state = buildInitialState(ssrPageProps) + } + + const matchingReduceFunction = actionsMap[action.type] + if (!matchingReduceFunction) return state + return matchingReduceFunction(state, action) + } + export default reducer diff --git a/packages/common/src/store/cache/users/reducer.ts b/packages/common/src/store/cache/users/reducer.ts index e12a52718a3..178024627ee 100644 --- a/packages/common/src/store/cache/users/reducer.ts +++ b/packages/common/src/store/cache/users/reducer.ts @@ -1,8 +1,13 @@ +import snakecaseKeys from 'snakecase-keys' + import { Cache } from 'models/Cache' import { ID } from 'models/Identifiers' import { Kind } from 'models/Kind' +import { SsrPageProps } from 'models/SsrPageProps' import { User } from 'models/User' +import { makeUser } from 'services/audius-api-client/ResponseAdapter' import { initialCacheState } from 'store/cache/reducer' +import { makeUid } from 'utils/uid' import { AddEntriesAction, @@ -57,13 +62,51 @@ const actionsMap = { return addEntries(state, matchingEntries) } } -const reducer = ( - state: UsersCacheState = initialState, - action: AddSuccededAction -) => { - const matchingReduceFunction = actionsMap[action.type] - if (!matchingReduceFunction) return state - return matchingReduceFunction(state, action) + +const buildInitialState = (ssrPageProps?: SsrPageProps) => { + // TODO: support user profile page. Only track page supported for now. + + // If we have preloaded data from the server, populate the initial + // cache state with it + if (ssrPageProps?.track) { + // @ts-ignore + const user = makeUser(snakecaseKeys(ssrPageProps.track.user)) + if (!user) return initialState + + const id = user.user_id + const uid = makeUid(Kind.USERS, id) + + const initialCacheState = { + ...initialState, + entries: { + [id]: { + metadata: user, + _timestamp: Date.now() + } + }, + uids: { + [uid]: user.user_id + }, + statuses: { + [id]: 'SUCCESS' + } + } + return initialCacheState + } + return initialState } +const reducer = + (ssrPageProps: SsrPageProps) => + (state: UsersCacheState, action: AddSuccededAction) => { + if (!state) { + // @ts-ignore + state = buildInitialState(ssrPageProps) + } + + const matchingReduceFunction = actionsMap[action.type] + if (!matchingReduceFunction) return state + return matchingReduceFunction(state, action) + } + export default reducer diff --git a/packages/common/src/store/pages/chat/sagas.ts b/packages/common/src/store/pages/chat/sagas.ts index e4a44993dbf..93cfe951c82 100644 --- a/packages/common/src/store/pages/chat/sagas.ts +++ b/packages/common/src/store/pages/chat/sagas.ts @@ -31,8 +31,10 @@ import { actions as chatActions } from './slice' import { makeChatId } from './utils' // Attach ulid to window object for debugging DMs -// @ts-ignore -window.ulid = ulid +if (typeof window !== 'undefined') { + // @ts-ignore + window.ulid = ulid +} const { createChat, diff --git a/packages/common/src/store/pages/saved-page/reducer.ts b/packages/common/src/store/pages/saved-page/reducer.ts index 1f5d7eacc63..5ffb63ee0b9 100644 --- a/packages/common/src/store/pages/saved-page/reducer.ts +++ b/packages/common/src/store/pages/saved-page/reducer.ts @@ -208,7 +208,7 @@ const actionsMap: ActionsMap = { const tracksLineupReducer = asLineup(tracksPrefix, tracksReducer) -const reducer = (state = initialState, action: any) => { +export const savePageReducer = (state = initialState, action: any) => { const tracks = tracksLineupReducer(state.tracks as any, action) if (tracks !== state.tracks) return { ...state, tracks } @@ -224,5 +224,5 @@ export const savedPagePersistConfig = (storage: Storage) => ({ }) export const persistedSavePageReducer = (storage: Storage) => { - return persistReducer(savedPagePersistConfig(storage), reducer) + return persistReducer(savedPagePersistConfig(storage), savePageReducer) } diff --git a/packages/common/src/store/pages/track/actions.ts b/packages/common/src/store/pages/track/actions.ts index d456088cfbf..9069c7b6b1a 100644 --- a/packages/common/src/store/pages/track/actions.ts +++ b/packages/common/src/store/pages/track/actions.ts @@ -7,6 +7,8 @@ export const SET_TRACK_ID = 'TRACK_PAGE/SET_TRACK_ID' export const SET_TRACK_PERMALINK = 'TRACK_PAGE/SET_TRACK_PERMALINK' export const MAKE_TRACK_PUBLIC = 'TRACK_PAGE/MAKE_TRACK_PUBLIC' export const SET_TRACK_TRENDING_RANKS = 'TRACK_PAGE/SET_TRACK_TRENDING_RANKS' +export const SET_IS_INITIAL_FETCH_AFTER_SSR = + 'TRACK_PAGE/SET_IS_INITIAL_FETCH_AFTER_SSR' export const FETCH_TRACK = 'TRACK_PAGE/FETCH_TRACK' export const FETCH_TRACK_SUCCEEDED = 'TRACK_PAGE/FETCH_TRACK_SUCCEEDED' @@ -74,3 +76,8 @@ export const setTrackTrendingRanks = (trendingTrackRanks) => ({ type: SET_TRACK_TRENDING_RANKS, trendingTrackRanks }) + +export const setIsInitialFetchAfterSsr = (isInitialFetchAfterSsr: boolean) => ({ + type: SET_IS_INITIAL_FETCH_AFTER_SSR, + isInitialFetchAfterSsr +}) diff --git a/packages/common/src/store/pages/track/reducer.ts b/packages/common/src/store/pages/track/reducer.ts index b72612c562b..ee0ba019ea6 100644 --- a/packages/common/src/store/pages/track/reducer.ts +++ b/packages/common/src/store/pages/track/reducer.ts @@ -1,20 +1,25 @@ // @ts-nocheck // TODO(nkang) - convert to TS + +import { SsrPageProps } from 'models/SsrPageProps' import { asLineup } from 'store/lineup/reducer' import tracksReducer, { initialState as initialLineupState } from 'store/pages/track/lineup/reducer' +import { decodeHashId } from 'utils/hashIds' import { SET_TRACK_ID, SET_TRACK_PERMALINK, RESET, SET_TRACK_RANK, - SET_TRACK_TRENDING_RANKS + SET_TRACK_TRENDING_RANKS, + SET_IS_INITIAL_FETCH_AFTER_SSR } from './actions' import { PREFIX as tracksPrefix } from './lineup/actions' +import TrackPageState from './types' -const initialState = { +const initialState: TrackPageState = { trackId: null, rank: { week: null, @@ -26,7 +31,8 @@ const initialState = { month: null, year: null }, - tracks: initialLineupState + tracks: initialLineupState, + isInitialFetchAfterSsr: false } const actionsMap = { @@ -66,12 +72,35 @@ const actionsMap = { ...initialState, tracks: tracksLineupReducer(undefined, action) } + }, + [SET_IS_INITIAL_FETCH_AFTER_SSR](state, action) { + return { + ...state, + isInitialFetchAfterSsr: action.isInitialFetchAfterSsr + } } } const tracksLineupReducer = asLineup(tracksPrefix, tracksReducer) -const reducer = (state = initialState, action) => { +const buildInitialState = (ssrPageProps?: SsrPageProps) => { + // If we have preloaded data from the server, populate the initial + // page state with it + if (ssrPageProps?.track) { + return { + ...initialState, + trackId: decodeHashId(ssrPageProps.track.id), + isInitialFetchAfterSsr: true + } + } + return initialState +} + +const reducer = (ssrPageProps?: SsrPageProps) => (state, action) => { + if (!state) { + state = buildInitialState(ssrPageProps) + } + const tracks = tracksLineupReducer(state.tracks, action) if (tracks !== state.tracks) return { ...state, tracks } diff --git a/packages/common/src/store/pages/track/selectors.ts b/packages/common/src/store/pages/track/selectors.ts index 3a9f74837c2..d169674203e 100644 --- a/packages/common/src/store/pages/track/selectors.ts +++ b/packages/common/src/store/pages/track/selectors.ts @@ -60,3 +60,7 @@ export const getTrendingTrackRanks = (state: CommonState) => { } export const getSourceSelector = (state: CommonState) => `${PREFIX}:${getTrackId(state)}` + +export const getIsInitialFetchAfterSsr = (state: CommonState) => { + return getBaseState(state).isInitialFetchAfterSsr +} diff --git a/packages/common/src/store/pages/track/types.ts b/packages/common/src/store/pages/track/types.ts index 97015bd79fc..7643013a793 100644 --- a/packages/common/src/store/pages/track/types.ts +++ b/packages/common/src/store/pages/track/types.ts @@ -14,4 +14,5 @@ export default interface TrackPageState { year: ID[] | null } tracks: LineupState<{ id: ID }> + isInitialFetchAfterSsr: boolean } diff --git a/packages/common/src/store/pages/trending/reducer.ts b/packages/common/src/store/pages/trending/reducer.ts index eb28b54ffd9..fce30602f71 100644 --- a/packages/common/src/store/pages/trending/reducer.ts +++ b/packages/common/src/store/pages/trending/reducer.ts @@ -1,5 +1,7 @@ // @ts-nocheck // TODO(nkang) - convert to TS +import { History } from 'history' + import { asLineup } from 'store/lineup/reducer' import { SET_TRENDING_GENRE, @@ -22,21 +24,6 @@ import { makeInitialState } from './lineup/reducer' -const urlParams = new URLSearchParams(window.location.search) -const genre = urlParams.get('genre') -const timeRange = urlParams.get('timeRange') - -const initialState = { - trendingTimeRange: Object.values(TimeRange).includes(timeRange) - ? timeRange - : TimeRange.WEEK, - trendingGenre: Object.values(GENRES).includes(genre) ? genre : null, - lastFetchedTrendingGenre: null, - trendingWeek: makeInitialState(TRENDING_WEEK_PREFIX), - trendingMonth: makeInitialState(TRENDING_MONTH_PREFIX), - trendingAllTime: makeInitialState(TRENDING_ALL_TIME_PREFIX) -} - const actionsMap = { [SET_TRENDING_TIME_RANGE](state, action) { return { @@ -65,7 +52,30 @@ const trendingAllTimeReducer = asLineup( trendingAllTime ) -const reducer = (state = initialState, action) => { +const reducer = (history?: History) => (state, action) => { + if (!state) { + const initialState = { + lastFetchedTrendingGenre: null, + trendingWeek: makeInitialState(TRENDING_WEEK_PREFIX), + trendingMonth: makeInitialState(TRENDING_MONTH_PREFIX), + trendingAllTime: makeInitialState(TRENDING_ALL_TIME_PREFIX) + } + + if (history) { + const urlParams = new URLSearchParams(history.location.search) + const genre = urlParams.get('genre') + const timeRange = urlParams.get('timeRange') + return { + ...initialState, + trendingTimeRange: Object.values(TimeRange).includes(timeRange) + ? timeRange + : TimeRange.WEEK, + trendingGenre: Object.values(GENRES).includes(genre) ? genre : null + } + } + + return initialState + } const trendingWeek = trendingWeekReducer(state.trendingWeek, action) if (trendingWeek !== state.trendingWeek) return { ...state, trendingWeek } diff --git a/packages/common/src/store/reducers.ts b/packages/common/src/store/reducers.ts index e393c17408e..27133eed8df 100644 --- a/packages/common/src/store/reducers.ts +++ b/packages/common/src/store/reducers.ts @@ -1,6 +1,9 @@ +import { History } from 'history' import { combineReducers } from 'redux' import type { Storage } from 'redux-persist' +import { SsrPageProps } from 'models/SsrPageProps' + import apiReducer from '../api/reducer' import { Kind } from '../models' @@ -46,7 +49,10 @@ import premiumTracks from './pages/premium-tracks/slice' import profileReducer from './pages/profile/reducer' import { ProfilePageState } from './pages/profile/types' import remixes from './pages/remixes/slice' -import { persistedSavePageReducer } from './pages/saved-page/reducer' +import { + savePageReducer, + persistedSavePageReducer +} from './pages/saved-page/reducer' import searchResults from './pages/search-results/reducer' import { SearchPageState } from './pages/search-results/types' import settings from './pages/settings/reducer' @@ -55,10 +61,10 @@ import smartCollection from './pages/smart-collection/slice' import tokenDashboardSlice from './pages/token-dashboard/slice' import track from './pages/track/reducer' import TrackPageState from './pages/track/types' -import trending from './pages/trending/reducer' -import { TrendingPageState } from './pages/trending/types' import trendingPlaylists from './pages/trending-playlists/slice' import trendingUnderground from './pages/trending-underground/slice' +import trending from './pages/trending/reducer' +import { TrendingPageState } from './pages/trending/types' import { PlaybackPositionState } from './playback-position' import playbackPosition from './playback-position/slice' import player, { PlayerState } from './player/slice' @@ -139,7 +145,12 @@ import wallet from './wallet/slice' * A function that creates common reducers. * @returns an object of all reducers to be used with `combineReducers` */ -export const reducers = (storage: Storage) => ({ +export const reducers = ( + storage: Storage, + ssrPageProps?: SsrPageProps, + isServerSide?: boolean, + history?: History +) => ({ account, api: apiReducer, @@ -157,10 +168,10 @@ export const reducers = (storage: Storage) => ({ collections: asCache(collectionsReducer, Kind.COLLECTIONS), // TODO: Fix type error // @ts-ignore - tracks: asCache(tracksReducer, Kind.TRACKS), + tracks: asCache(tracksReducer(ssrPageProps), Kind.TRACKS), // TODO: Fix type error // @ts-ignore - users: asCache(usersReducer, Kind.USERS), + users: asCache(usersReducer(ssrPageProps), Kind.USERS), savedCollections: savedCollectionsReducer, @@ -238,11 +249,13 @@ export const reducers = (storage: Storage) => ({ historyPage: historyPageReducer, profile: profileReducer, smartCollection, - savedPage: persistedSavePageReducer(storage), + savedPage: isServerSide + ? savePageReducer + : persistedSavePageReducer(storage), searchResults, tokenDashboard: tokenDashboardSlice.reducer, - track, - trending, + track: track(ssrPageProps), + trending: trending(history), trendingPlaylists, trendingUnderground, settings, diff --git a/packages/common/src/store/storeContext.ts b/packages/common/src/store/storeContext.ts index 11aa9d365cd..faf81ede7d6 100644 --- a/packages/common/src/store/storeContext.ts +++ b/packages/common/src/store/storeContext.ts @@ -1,4 +1,5 @@ import type { AudiusSdk } from '@audius/sdk' +import { Location } from 'history' import { AllTrackingEvents, @@ -31,7 +32,7 @@ export type CommonStoreContext = { fallbackFlag?: FeatureFlags ) => Promise analytics: { - init: () => Promise + init: (isMobile: boolean) => Promise track: (event: AnalyticsEvent, callback?: () => void) => Promise identify: ( handle: string, @@ -58,7 +59,9 @@ export type CommonStoreContext = { explore: Explore // A helper that returns the appropriate lineup selector for the current // route or screen. - getLineupSelectorForRoute?: () => (state: CommonState) => LineupState + getLineupSelectorForRoute?: ( + location: Location + ) => (state: CommonState) => LineupState audioPlayer: AudioPlayer solanaClient: SolanaClient sentry: { @@ -77,4 +80,5 @@ export type CommonStoreContext = { urls: string[] ) => Promise<{ file: File; url: string }> } + isMobile: boolean } diff --git a/packages/libs/package.json b/packages/libs/package.json index 95d02136e40..04a4fe20d39 100644 --- a/packages/libs/package.json +++ b/packages/libs/package.json @@ -89,7 +89,7 @@ "bs58": "4.0.1", "cipher-base": "1.0.4", "crc-32": "1.2.2", - "cross-fetch": "3.1.5", + "cross-fetch": "4.0.0", "elliptic": "6.5.4", "esm": "3.2.25", "eth-sig-util": "2.5.4", diff --git a/packages/libs/src/utils/fileHasher.ts b/packages/libs/src/utils/fileHasher.ts index aa626ac16e8..51578251ea0 100644 --- a/packages/libs/src/utils/fileHasher.ts +++ b/packages/libs/src/utils/fileHasher.ts @@ -1,6 +1,6 @@ import fs from 'fs' import { Stream } from 'stream' -import { promisify } from 'util' +import * as util from 'util' import type { Blockstore, Options } from 'interface-blockstore' import type { @@ -36,6 +36,8 @@ export interface HashedImage { size: number } +const { promisify } = util + const block: Blockstore = { get: async (key: CID, _options?: Options) => { throw new Error(`unexpected block API get for ${key}`) diff --git a/packages/libs/src/utils/multiProvider.ts b/packages/libs/src/utils/multiProvider.ts index 4843799a866..1ffc6dd3a2d 100644 --- a/packages/libs/src/utils/multiProvider.ts +++ b/packages/libs/src/utils/multiProvider.ts @@ -1,10 +1,11 @@ -import { callbackify, promisify } from 'util' +import * as util from 'util' import { shuffle } from 'lodash' import type { HttpProvider, AbstractProvider } from 'web3-core' import type { JsonRpcPayload } from 'web3-core-helpers' import Web3 from '../LibsWeb3' +const { callbackify, promisify } = util const getSendMethod = (provider: HttpProvider | AbstractProvider) => { if ('sendAsync' in provider) { diff --git a/packages/mobile/src/services/audius-backend-instance.ts b/packages/mobile/src/services/audius-backend-instance.ts index dde077fdccd..709d3bac00d 100644 --- a/packages/mobile/src/services/audius-backend-instance.ts +++ b/packages/mobile/src/services/audius-backend-instance.ts @@ -58,7 +58,6 @@ export const audiusBackendInstance = audiusBackend({ identityServiceUrl: env.IDENTITY_SERVICE, generalAdmissionUrl: env.GENERAL_ADMISSION, isElectron: false, - isMobile: true, localStorage: AsyncStorage, monitoringCallbacks, nativeMobile: true, diff --git a/packages/mobile/src/store/storeContext.ts b/packages/mobile/src/store/storeContext.ts index 39f266df526..24a49d5f240 100644 --- a/packages/mobile/src/store/storeContext.ts +++ b/packages/mobile/src/store/storeContext.ts @@ -54,5 +54,6 @@ export const storeContext: CommonStoreContext = { audiusSdk, imageUtils: { generatePlaylistArtwork - } + }, + isMobile: true } diff --git a/packages/spl/src/reward-manager/AttestationLayout.ts b/packages/spl/src/reward-manager/AttestationLayout.ts index 6b348b8c153..3d52082c24d 100644 --- a/packages/spl/src/reward-manager/AttestationLayout.ts +++ b/packages/spl/src/reward-manager/AttestationLayout.ts @@ -5,7 +5,6 @@ import { ethAddress } from '../layout-utils' import { Attestation } from './types' -const delimiter = Buffer.from('_', 'utf-8') export class AttestationLayout extends Layout { constructor(property?: string) { super( @@ -21,6 +20,7 @@ export class AttestationLayout extends Layout { } decode(b: Uint8Array, offset = 0): Attestation { + const delimiter = Buffer.from('_', 'utf-8') const recipientEthAddress = ethAddress().decode(b, offset) offset += ethAddress().span + 1 const amount = u64().decode(b, offset) @@ -43,6 +43,7 @@ export class AttestationLayout extends Layout { } encode(src: Attestation, b: Uint8Array, offset = 0) { + const delimiter = Buffer.from('_', 'utf-8') let layoutOffset = offset layoutOffset += ethAddress().encode( src.recipientEthAddress, diff --git a/packages/stems/src/components/Modal/Modal.tsx b/packages/stems/src/components/Modal/Modal.tsx index 95f53fa8b3f..92075130639 100644 --- a/packages/stems/src/components/Modal/Modal.tsx +++ b/packages/stems/src/components/Modal/Modal.tsx @@ -291,7 +291,9 @@ export const Modal = forwardRef(function Modal( [headerContainerClassName!]: !!headerContainerClassName }) - const [height, setHeight] = useState(window.innerHeight) + const [height, setHeight] = useState( + typeof window !== 'undefined' ? window.innerHeight : 0 + ) useEffect(() => { const onResize = () => { diff --git a/packages/stems/src/components/Modal/ModalContentPages.tsx b/packages/stems/src/components/Modal/ModalContentPages.tsx index e637bd754c6..b631034cd5e 100644 --- a/packages/stems/src/components/Modal/ModalContentPages.tsx +++ b/packages/stems/src/components/Modal/ModalContentPages.tsx @@ -3,7 +3,7 @@ import { Children, ReactChild, useState } from 'react' import { ResizeObserver } from '@juggle/resize-observer' import cn from 'classnames' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { animated, Transition } from 'react-spring/renderprops' +import { animated, Transition } from 'react-spring/renderprops.cjs' import useMeasure from 'react-use-measure' import { ModalContent } from './ModalContent' diff --git a/packages/stems/src/components/Modal/hooks.ts b/packages/stems/src/components/Modal/hooks.ts index 9abe8caaec4..4ec18ac54ab 100644 --- a/packages/stems/src/components/Modal/hooks.ts +++ b/packages/stems/src/components/Modal/hooks.ts @@ -1,7 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { useGlobal } from 'hooks/useGlobal' - export const setOverflowHidden = () => { document.body.setAttribute('style', 'overflow:hidden;') } @@ -12,23 +10,25 @@ export const removeOverflowHidden = () => { export const setModalRootTop = () => { const root = document.getElementById('modalRootContainer') + const scrollY = typeof window !== 'undefined' ? window.scrollY : 0 if (root) { - root.setAttribute('style', `top: ${window.scrollY}px`) + root.setAttribute('style', `top: ${scrollY}px`) } } +let modalCount = 0 + export const useModalScrollCount = () => { - const [getCount, setCount] = useGlobal('modal-scroll-count', 0) // Keep a state toggle to trigger recomputations of the effect const [toggle, setToggle] = useState(false) const [isOverflowHidden, setIsOverflowHidden] = useState(false) useEffect(() => { - if (!isOverflowHidden && getCount() > 0) { + if (!isOverflowHidden && modalCount > 0) { setIsOverflowHidden(true) setOverflowHidden() setModalRootTop() - } else if (isOverflowHidden && getCount() === 0) { + } else if (isOverflowHidden && modalCount === 0) { setIsOverflowHidden(false) removeOverflowHidden() } @@ -44,23 +44,23 @@ export const useModalScrollCount = () => { * NOTE: This should only be triggered on un-mount when not closed */ setImmediate(() => { - if (isOverflowHidden && getCount() === 0) { + if (isOverflowHidden && modalCount === 0) { removeOverflowHidden() } }) } - }, [getCount, isOverflowHidden, toggle]) + }, [isOverflowHidden, toggle]) const incrementScrollCount = useCallback(() => { - setCount((count) => count + 1) + modalCount = modalCount + 1 setToggle((toggle) => !toggle) - }, [setCount, setToggle]) + }, [setToggle]) const decrementScrollCount = useCallback(() => { // Though we should in theory never be decrementing past zero, getting into // that state would be bad for us, so guard against it defensively - setCount((count) => Math.max(0, count - 1)) + modalCount = Math.max(0, modalCount - 1) setToggle((toggle) => !toggle) - }, [setCount, setToggle]) + }, [setToggle]) return { incrementScrollCount, diff --git a/packages/stems/src/components/Popup/Popup.tsx b/packages/stems/src/components/Popup/Popup.tsx index 28ec5ed906c..138ec63dc02 100644 --- a/packages/stems/src/components/Popup/Popup.tsx +++ b/packages/stems/src/components/Popup/Popup.tsx @@ -259,6 +259,8 @@ export const Popup = forwardRef(function Popup( zIndex, containerRef } = props + const [isClientSide, setIsClientSide] = useState(false) + const handleClose = useCallback(() => { onClose() setTimeout(() => { @@ -416,52 +418,59 @@ export const Popup = forwardRef(function Popup( } }, [dismissOnMouseLeave, onClose]) + // useEffect only runs on the client + useEffect(() => { + setIsClientSide(true) + }, []) + + // Portal the popup out of the dom structure so that it has a separate stacking context return ( <> - {/* Portal the popup out of the dom structure so that it has a separate stacking context */} - {ReactDOM.createPortal( -
- {transitions.map(({ item, key, props }) => - item ? ( - - {showHeader && ( -
+ {transitions.map(({ item, key, props }) => + item ? ( + - {hideCloseButton ? null : ( - } - /> + {showHeader && ( +
+ {hideCloseButton ? null : ( + } + /> + )} +
+ {title} +
+
)} -
- {title} -
-
- )} - {children} -
- ) : null - )} -
, - document.body - )} + {children} + + ) : null + )} + , + document.body + ) + : null} ) }) diff --git a/packages/stems/src/components/Scrollbar/Scrollbar.tsx b/packages/stems/src/components/Scrollbar/Scrollbar.tsx index dbf63308927..d7d83f21154 100644 --- a/packages/stems/src/components/Scrollbar/Scrollbar.tsx +++ b/packages/stems/src/components/Scrollbar/Scrollbar.tsx @@ -1,8 +1,7 @@ -import { useEffect, useRef, useMemo, forwardRef, Ref, useCallback } from 'react' +import { useEffect, useRef, forwardRef, Ref, useCallback, useId } from 'react' import { ResizeObserver } from '@juggle/resize-observer' import cn from 'classnames' -import { uniqueId } from 'lodash' import PerfectScrollbar from 'react-perfect-scrollbar' import useMeasure from 'react-use-measure' @@ -24,7 +23,8 @@ export const Scrollbar = forwardRef( // useMeasure ref is required for infinite scrolling to work const [ref] = useMeasure({ polyfill: ResizeObserver }) const timerRef = useRef(null) - const elementId = useMemo(() => id || uniqueId('scrollbar-'), [id]) + const reactId = useId() + const elementId = id || reactId useEffect(() => { return () => { diff --git a/packages/stems/src/hooks/useGlobal.ts b/packages/stems/src/hooks/useGlobal.ts deleted file mode 100644 index 95c5949b718..00000000000 --- a/packages/stems/src/hooks/useGlobal.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useMemo } from 'react' - -declare global { - interface Window { - AudiusStems: any - } -} - -window.AudiusStems = window.AudiusStems || {} - -/** - * Hook to "share state" between components using the global window object. - * Obviously, comes with caveats with globals. - * - * @param name shared name between users of a useGlobal - * @param initialValue - * @returns getter, setter - * Similar to useState, except - * 1. The getter is a function to allow for fresh fetches (pulls off of window at each invocation) - * 2. The setter can/should only be invoked with a mutator function rather than a "new value" - */ -export const useGlobal = ( - name: string, - initialValue: T -): [() => T, (mutator: (cur: T) => void) => void] => { - useEffect(() => { - if (window.AudiusStems[name] === undefined) { - window.AudiusStems[name] = initialValue - } - }, [name, initialValue]) - - const getter = useMemo(() => () => window.AudiusStems[name], [name]) - const setter = useMemo( - () => (mutator: (cur: T) => void) => { - window.AudiusStems[name] = mutator(window.AudiusStems[name]) - }, - [name] - ) - - return [getter, setter] -} diff --git a/packages/web/.eslintrc.js b/packages/web/.eslintrc.js index 84e1e8ed4c8..0f11cdfb03c 100644 --- a/packages/web/.eslintrc.js +++ b/packages/web/.eslintrc.js @@ -25,7 +25,9 @@ module.exports = { ['types', './src/types'], ['utils', './src/utils'], ['workers', './src/workers'], - ['pages', './src/pages'] + ['pages', './src/pages'], + ['ssr', './src/ssr'], + ['app', './src/app'] ], extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] } diff --git a/packages/web/.gitignore b/packages/web/.gitignore index a48ee82c4b3..166b5a5ff51 100644 --- a/packages/web/.gitignore +++ b/packages/web/.gitignore @@ -23,6 +23,7 @@ env/.env.dev.local /build-ci /build-production /build-mobile-production +/build-ssr /storybook-static /build-ipfs-staging /build-ipfs-staging.zip diff --git a/packages/web/env/.env.ssr b/packages/web/env/.env.ssr new file mode 100644 index 00000000000..3c64dd8810e --- /dev/null +++ b/packages/web/env/.env.ssr @@ -0,0 +1 @@ +VITE_SSR=true \ No newline at end of file diff --git a/packages/web/index.html b/packages/web/index.html index 21fe5be6785..ce62d53b33f 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -53,13 +53,9 @@ + - +
- + - diff --git a/packages/web/package.json b/packages/web/package.json index 193ca293ac0..f17d58c8c09 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,54 +1,64 @@ { "name": "audius-client", "productName": "Audius", - "description": "The Audius decentralized application", + "description": "The Audius web client reference implementation", "author": "Audius", "version": "1.5.63", "private": true, "scripts": { - "publish-scripts": "./scripts/publishScripts.sh", - "start": "vite", - "start:dev": "npm run write-sha && npm run publish-scripts && env-cmd ./env/.env.git env-cmd --no-override ./env/.env.dev turbo run start", - "start:stage": "npm run write-sha && npm run publish-scripts && env-cmd ./env/.env.git env-cmd --no-override ./env/.env.stage turbo run start", - "start:prod": "npm run write-sha && npm run publish-scripts && env-cmd ./env/.env.git env-cmd --no-override ./env/.env.prod turbo run start", - "preview:prod": "npm run build:prod && vite preview --outDir build-production", - "prebuild": "npm run publish-scripts", - "build": "vite build && cp package.json build/package.json", - "build:dev": "npm run write-sha && env-cmd ./env/.env.git env-cmd ./env/.env.dev turbo run build && rm -rf build-development && mv build build-development", - "build:stage": "npm run write-sha && env-cmd ./env/.env.git env-cmd ./env/.env.stage turbo run build && rm -rf build-staging && mv build build-staging", - "build:prod": "npm run write-sha && env-cmd ./env/.env.git env-cmd ./env/.env.prod turbo run build && rm -rf build-production && mv build build-production", - "test": "vitest", - "test:coverage": "vitest --resetMocks=false --coverage --watchAll=false", - "eject": "react-scripts eject", + "DEV & BUILD========================================": "", + "analyze": "env-cmd ./env/.env.bundle npm run build:prod", + "build:dev": "env-cmd ./env/.env.dev turbo run build && rm -rf build-development && mv build build-development", + "build:prod-source-maps": "env-cmd ./env/.env.prod env-cmd ./env/.env.source-maps turbo run build && rm -rf build-production && mv build build-production", + "build:prod": "env-cmd ./env/.env.prod turbo run build && rm -rf build-production && mv build build-production", + "build:ssr:dev": "env-cmd ./env/.env.dev env-cmd ./env/.env.ssr turbo run build && rm -rf build-ssr-development && mv build-ssr build-ssr-development", + "build:ssr:prod": "env-cmd ./env/.env.prod env-cmd ./env/.env.ssr turbo run build && rm -rf build-ssr-production && mv build-ssr build-ssr-production", + "build:ssr:stage": "env-cmd ./env/.env.stage env-cmd ./env/.env.ssr turbo run build && rm -rf build-ssr-staging && mv build-ssr build-ssr-staging", + "build:stage": "env-cmd ./env/.env.stage turbo run build && rm -rf build-staging && mv build build-staging", + "build": "npm run write-sha && NODE_OPTIONS=--max-old-space-size=8192 env-cmd ./env/.env.git vite build", "lint:fix": "eslint --cache --fix --ext=js,jsx,ts,tsx src", "lint": "eslint --cache --ext=js,jsx,ts,tsx src", - "stylelint": "stylelint 'src/**/*.css'", + "prebuild": "npm run publish-scripts", + "preview:prod": "npm run build:prod && vite preview --outDir build-production", + "publish-scripts": "./scripts/publishScripts.sh", + "start:dev": "env-cmd --no-override ./env/.env.dev env-cmd ./env/.env.ssr turbo run start", + "start:prod": "env-cmd --no-override ./env/.env.prod env-cmd ./env/.env.ssr env-cmd ./env/.env.source-maps turbo run start", + "start:stage": "env-cmd --no-override ./env/.env.stage env-cmd ./env/.env.ssr turbo run start", + "start:spa:dev": "env-cmd --no-override ./env/.env.dev turbo run start", + "start:spa:prod": "env-cmd --no-override ./env/.env.prod env-cmd ./env/.env.source-maps turbo run start", + "start:spa:stage": "env-cmd --no-override ./env/.env.stage turbo run start", + "start": "npm run write-sha && npm run publish-scripts && env-cmd ./env/.env.git vite", "stylelint:fix": "npm run stylelint -- --fix", - "electron:stage": "electron . staging", - "electron:prod": "electron . production", - "electron:localhost": "electron . localhost", - "pack": "electron-builder --dir", - "dist": "node ./scripts/dist.js", - "dist:all": "npm run dist -- --mac --win --linux", - "dist:mac": "npm run dist -- --mac", - "dist:win": "npm run dist -- --win", - "dist:linux": "npm run dist -- --linux", - "dist-publish": "npm run dist -- --mac --win --linux --publish always", + "stylelint": "stylelint 'src/**/*.css'", + "test:coverage": "vitest --resetMocks=false --coverage --watchAll=false", + "test": "vitest", + "typecheck:watch": "tsc --watch", + "typecheck": "tsc", + "verify": "concurrently \"npm:typecheck\" \"npm:lint:fix\" \"npm:stylelint:fix\"", + "worker-ssr:dev": "npx wrangler dev --env test --config ./src/ssr/wrangler.toml", + "worker:dev": "npx wrangler dev --env test", + "write-sha": "./scripts/writeSHA.sh", + "ELECTRON========================================": "", "dist-publish-production": "npm run dist -- --mac --win --linux --publish always --env production", - "dist:mac-publish": "npm run dist -- --mac --publish always", - "dist:win-publish": "npm run dist -- --win --publish always", + "dist-publish": "npm run dist -- --mac --win --linux --publish always", + "dist:all": "npm run dist -- --mac --win --linux", + "dist:linux-publish-production": "npm run dist -- --linux --publish always --env production", "dist:linux-publish": "npm run dist -- --linux --publish always", - "dist:win:linux-publish": "npm run dist -- --win --linux --publish always", + "dist:linux": "npm run dist -- --linux", "dist:mac-publish-production": "npm run dist -- --mac --publish always --env production", + "dist:mac-publish": "npm run dist -- --mac --publish always", + "dist:mac": "npm run dist -- --mac", "dist:win-publish-production": "npm run dist -- --win --publish always --env production", - "dist:linux-publish-production": "npm run dist -- --linux --publish always --env production", + "dist:win-publish": "npm run dist -- --win --publish always", "dist:win:linux-publish-production": "npm run dist -- --win --linux --publish always --env production", - "write-sha": "./scripts/writeSHA.sh", - "analyze": "env-cmd ./env/.env.bundle npm run build:prod", - "typecheck": "tsc", - "typecheck:watch": "tsc --watch", + "dist:win:linux-publish": "npm run dist -- --win --linux --publish always", + "dist:win": "npm run dist -- --win", + "dist": "node ./scripts/dist.js", + "electron:localhost": "electron . localhost", + "electron:prod": "electron . production", + "electron:stage": "electron . staging", "install-dmg-license": "npm add dmg-license -w audius-client", - "verify": "concurrently \"npm:typecheck\" \"npm:lint:fix\" \"npm:stylelint:fix\"" + "pack": "electron-builder --dir" }, "dependencies": { "@audius/common": "*", @@ -56,6 +66,7 @@ "@audius/sdk": "*", "@audius/stems": "*", "@audius/trpc-server": "*", + "@cloudflare/kv-asset-handler": "0.2.0", "@coinbase/cbpay-js": "1.2.0", "@coinflowlabs/react": "3.1.5", "@emotion/css": "^11.11.2", @@ -100,6 +111,7 @@ "clamp": "1.0.1", "classnames": "2.2.6", "connected-react-router": "6.9.3", + "cross-fetch": "^4.0.0", "electron-log": "5.0.3", "electron-updater": "6.1.7", "exif-parser": "0.1.12", @@ -172,6 +184,8 @@ "type-fest": "2.16.0", "typed-redux-saga": "1.3.1", "typesafe-actions": "5.1.0", + "vike": "0.4.150", + "vike-react": "^0.3.7", "walletlink": "2.0.3", "wasm-loader": "1.3.0", "web-vitals": "0.2.2", diff --git a/packages/web/patches/@react-spring+web+9.7.2.patch b/packages/web/patches/@react-spring+web+9.7.2.patch new file mode 100644 index 00000000000..6647cd2258b --- /dev/null +++ b/packages/web/patches/@react-spring+web+9.7.2.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@react-spring/web/package.json b/node_modules/@react-spring/web/package.json +index bb6053d..926d23c 100644 +--- a/node_modules/@react-spring/web/package.json ++++ b/node_modules/@react-spring/web/package.json +@@ -4,6 +4,7 @@ + "main": "dist/index.js", + "module": "dist/esm/index.js", + "types": "dist/index.d.ts", ++ "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", diff --git a/packages/web/patches/lottie-web+5.12.2.patch b/packages/web/patches/lottie-web+5.12.2.patch new file mode 100644 index 00000000000..cd7d9577842 --- /dev/null +++ b/packages/web/patches/lottie-web+5.12.2.patch @@ -0,0 +1,44 @@ +diff --git a/node_modules/lottie-web/build/player/lottie.js b/node_modules/lottie-web/build/player/lottie.js +index cddf7ca..c9ad885 100644 +--- a/node_modules/lottie-web/build/player/lottie.js ++++ b/node_modules/lottie-web/build/player/lottie.js +@@ -1313,6 +1313,10 @@ + + var ImagePreloader = function () { + var proxyImage = function () { ++ if (typeof document === 'undefined') { ++ return undefined ++ } ++ + var canvas = createTag('canvas'); + canvas.width = 1; + canvas.height = 1; +@@ -5276,7 +5280,7 @@ + lottie.version = '5.12.2'; + + function checkReady() { +- if (document.readyState === 'complete') { ++ if (typeof document !== 'undefined' && document.readyState === 'complete') { + clearInterval(readyStateCheckInterval); + searchAnimations(); + } +@@ -5299,7 +5303,7 @@ + + var queryString = ''; + +- if (standalone) { ++ if (standalone && typeof document !== 'undefined') { + var scripts = document.getElementsByTagName('script'); + var index = scripts.length - 1; + var myScript = scripts[index] || { +@@ -13885,7 +13889,9 @@ + } + + extendPrototype([BaseElement, TransformElement, CVBaseElement, HierarchyElement, FrameElement, RenderableElement, ITextElement], CVTextElement); +- CVTextElement.prototype.tHelper = createTag('canvas').getContext('2d'); ++ if (typeof document !== 'undefined') { ++ CVTextElement.prototype.tHelper = createTag('canvas').getContext('2d'); ++ } + + CVTextElement.prototype.buildNewText = function () { + var documentData = this.textProperty.currentData; diff --git a/packages/web/patches/vike+0.4.150.patch b/packages/web/patches/vike+0.4.150.patch new file mode 100644 index 00000000000..b576f5b4c0b --- /dev/null +++ b/packages/web/patches/vike+0.4.150.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/vike/package.json b/node_modules/vike/package.json +index 7010b9f..47eadd8 100644 +--- a/node_modules/vike/package.json ++++ b/node_modules/vike/package.json +@@ -47,6 +47,11 @@ + "types": "./dist/esm/client/server-routing-runtime/index.d.ts" + }, + "./types": { ++ "worker": "./dist/esm/types/index.d.ts", ++ "edge-light": "./dist/esm/types/index.d.ts", ++ "require": "./dist/esm/types/index.d.ts", ++ "node": "./dist/esm/types/index.d.ts", ++ "browser": "./dist/esm/types/index.d.ts", + "types": "./dist/esm/types/index.d.ts" + }, + "./client/router": { diff --git a/packages/web/scripts/workers-site/index.js b/packages/web/scripts/workers-site/index.js index 949de761178..9fd65845851 100644 --- a/packages/web/scripts/workers-site/index.js +++ b/packages/web/scripts/workers-site/index.js @@ -1,14 +1,14 @@ import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler' +import manifestJSON from '__STATIC_CONTENT_MANIFEST' -/* globals GA, GA_ACCESS_TOKEN, EMBED, DISCOVERY_NODES, HTMLRewriter */ +/* globals HTMLRewriter */ +const assetManifest = JSON.parse(manifestJSON) + +const SSR = false const DEBUG = false const BROWSER_CACHE_TTL_SECONDS = 60 * 60 * 24 -const discoveryNodes = DISCOVERY_NODES.split(',') -const discoveryNode = - discoveryNodes[Math.floor(Math.random() * discoveryNodes.length)] - let h1 = null const routes = [ @@ -30,21 +30,6 @@ const routes = [ } ] -addEventListener('fetch', (event) => { - try { - event.respondWith(handleEvent(event)) - } catch (e) { - if (DEBUG) { - return event.respondWith( - new Response(e.message || e.toString(), { - status: 500 - }) - ) - } - event.respondWith(new Response('Internal Error', { status: 500 })) - } -}) - function matchRoute(input) { for (const route of routes) { const match = route.pattern.exec(input) @@ -68,7 +53,15 @@ function checkIsBot(val) { return botTest.test(val) } -async function getMetadata(pathname) { +function checkIsCrawler(val) { + if (!val) { + return false + } + const crawlerTest = /Googlebot|forceSsr/i + return crawlerTest.test(val) +} + +async function getMetadata(pathname, discoveryNode) { if (pathname.startsWith('/scripts')) { return { metadata: null, name: null } } @@ -140,12 +133,16 @@ class SEOHandlerBody { } class SEOHandlerHead { - constructor(pathname) { + constructor(pathname, discoveryNode) { self.pathname = pathname + self.discoveryNode = discoveryNode } async element(element) { - const { metadata, name } = await getMetadata(self.pathname) + const { metadata, name } = await getMetadata( + self.pathname, + self.discoveryNode + ) if (!metadata || !name || !metadata.data || metadata.data.length === 0) { // We didn't parse this to anything we have custom tags for, so just return the default tags @@ -209,9 +206,7 @@ class SEOHandlerHead { return } const tags = `${clean(title)} - + @@ -228,8 +223,8 @@ class SEOHandlerHead { } } -async function handleEvent(event) { - const url = new URL(event.request.url) +async function handleEvent(request, env, ctx) { + const url = new URL(request.url) const { pathname, search, hash } = url const isUndefined = pathname === '/undefined' @@ -237,14 +232,18 @@ async function handleEvent(event) { return Response.redirect(url.origin, 302) } + const discoveryNodes = env.DISCOVERY_NODES.split(',') + const discoveryNode = + discoveryNodes[Math.floor(Math.random() * discoveryNodes.length)] + const isSitemap = pathname.startsWith('/sitemaps') if (isSitemap) { const destinationURL = discoveryNode + pathname + search + hash - const newRequest = new Request(destinationURL, event.request) + const newRequest = new Request(destinationURL, request) return await fetch(newRequest) } - const userAgent = event.request.headers.get('User-Agent') || '' + const userAgent = request.headers.get('User-Agent') || '' const is204 = pathname === '/204' if (is204) { @@ -257,29 +256,23 @@ async function handleEvent(event) { const isBot = checkIsBot(userAgent) if (isBot) { - const destinationURL = GA + pathname + search + hash - const newRequest = new Request(destinationURL, event.request) - newRequest.headers.set('host', GA) - newRequest.headers.set('x-access-token', GA_ACCESS_TOKEN) + const destinationURL = env.GA + pathname + search + hash + const newRequest = new Request(destinationURL, request) + newRequest.headers.set('host', env.GA) + newRequest.headers.set('x-access-token', env.GA_ACCESS_TOKEN) return await fetch(newRequest) } const isEmbed = pathname.startsWith('/embed') if (isEmbed) { - const destinationURL = EMBED + pathname + search + hash - const newRequest = new Request(destinationURL, event.request) + const destinationURL = env.EMBED + pathname + search + hash + const newRequest = new Request(destinationURL, request) return await fetch(newRequest) } const options = {} - // Always map requests to `/` - options.mapRequestToAsset = (request) => { - const url = new URL(request.url) - url.pathname = `/` - return mapRequestToAsset(new Request(url, request)) - } try { if (DEBUG) { @@ -289,27 +282,79 @@ async function handleEvent(event) { } } - const asset = await getAssetFromKV(event, options) - - const rewritten = new HTMLRewriter() - .on('head', new SEOHandlerHead(pathname)) - .on('body', new SEOHandlerBody()) - .transform(asset) - - // Adjust browser cache on assets that don't change frequently and/or - // are given unique hashes when they do. - if ( - pathname.startsWith('/assets') || - pathname.startsWith('/scripts') || - pathname.startsWith('/fonts') - ) { - const response = new Response(rewritten.body, rewritten) - response.headers.set('cache-control', BROWSER_CACHE_TTL_SECONDS) - return response + // For now, only SSR for crawlers + if (SSR && checkIsCrawler(userAgent)) { + const ssrResponse = await env.SSR.fetch(request.clone()) + return ssrResponse + } else { + if (!isAssetUrl(request.url)) { + // Map all non-asset requests to the root path + options.mapRequestToAsset = (request) => { + const url = new URL(request.url) + url.pathname = `/` + return mapRequestToAsset(new Request(url, request)) + } + + const asset = await getAsset(request, env, ctx, options) + + const rewritten = new HTMLRewriter() + .on('head', new SEOHandlerHead(pathname, discoveryNode)) + .on('body', new SEOHandlerBody()) + .transform(asset) + + return rewritten + } else { + const asset = await getAsset(request, env, ctx, options) + + // Adjust browser cache on assets that don't change frequently and/or + // are given unique hashes when they do. + const response = new Response(asset.body, asset) + response.headers.set('cache-control', BROWSER_CACHE_TTL_SECONDS) + + return response + } } - - return rewritten } catch (e) { return new Response(e.message || e.toString(), { status: 500 }) } } + +async function getAsset(request, env, ctx, options) { + return await getAssetFromKV( + { + request, + waitUntil: ctx.waitUntil.bind(ctx) + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: assetManifest, + ...options + } + ) +} + +function isAssetUrl(url) { + const { pathname } = new URL(url) + return ( + pathname.startsWith('/assets') || + pathname.startsWith('/scripts') || + pathname.startsWith('/fonts') || + pathname.startsWith('/favicons') || + pathname.startsWith('/manifest.json') + ) +} + +export default { + fetch(request, env, ctx) { + try { + return handleEvent(request, env, ctx) + } catch (e) { + if (DEBUG) { + return new Response(e.message || e.toString(), { + status: 500 + }) + } + return new Response('Internal Error', { status: 500 }) + } + } +} diff --git a/packages/web/scripts/workers-site/package-lock.json b/packages/web/scripts/workers-site/package-lock.json deleted file mode 100644 index 06c6d86eb66..00000000000 --- a/packages/web/scripts/workers-site/package-lock.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "worker", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@cloudflare/kv-asset-handler": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.0.11.tgz", - "integrity": "sha512-D2kGr8NF2Er//Mx0c4+8FtOHuLrnwOlpC48TbtyxRSegG/Js15OKoqxxlG9BMUj3V/YSqtN8bUU6pjaRlsoSqg==", - "requires": { - "@cloudflare/workers-types": "^2.0.0", - "@types/mime": "^2.0.2", - "mime": "^2.4.6" - } - }, - "@cloudflare/workers-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-2.0.0.tgz", - "integrity": "sha512-SFUPQzR5aV2TBLP4Re+xNX5KfAGArcRGA44OLulBDnfblEf3J+6kFvdJAQwFhFpqru3wImwT1cX0wahk6EeWTw==" - }, - "@types/mime": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", - "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - } - } -} diff --git a/packages/web/scripts/workers-site/package.json b/packages/web/scripts/workers-site/package.json deleted file mode 100644 index bfa88a666ee..00000000000 --- a/packages/web/scripts/workers-site/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "private": true, - "name": "web-workers-site", - "version": "1.0.0", - "description": "A template for kick starting a Cloudflare Workers project", - "main": "index.js", - "author": "Ashley Lewis ", - "license": "MIT", - "dependencies": { - "@cloudflare/kv-asset-handler": "~0.0.11" - } -} diff --git a/packages/web/src/Root.tsx b/packages/web/src/Root.tsx index dfd3d825b4d..5ba285f6beb 100644 --- a/packages/web/src/Root.tsx +++ b/packages/web/src/Root.tsx @@ -3,30 +3,45 @@ import '@audius/harmony/dist/harmony.css' import { Suspense, useState, useEffect, lazy } from 'react' +import { Location } from 'history' import { useAsync } from 'react-use' +import { useIsMobile } from 'hooks/useIsMobile' import { localStorage } from 'services/local-storage' -import { useIsMobile, isElectron } from 'utils/clientUtil' +import { remoteConfigInstance } from 'services/remote-config/remote-config-instance' +import { isElectron } from 'utils/clientUtil' import { getPathname, HOME_PAGE, publicSiteRoutes } from 'utils/route' -const App = lazy(() => import('./app')) +import App from './app' +import { + HistoryContextProvider, + useHistoryContext +} from './app/HistoryProvider' + const PublicSite = lazy(() => import('./public-site')) -const isPublicSiteRoute = (location = window.location) => { +const isPublicSiteRoute = (location: Location) => { const pathname = getPathname(location).toLowerCase() return [...publicSiteRoutes, HOME_PAGE].includes(pathname) } -const isPublicSiteSubRoute = (location = window.location) => { +const isPublicSiteSubRoute = (location: Location) => { const pathname = getPathname(location).toLowerCase() return publicSiteRoutes.includes(pathname) } const clientIsElectron = isElectron() -export const Root = () => { - const [renderPublicSite, setRenderPublicSite] = useState(isPublicSiteRoute()) - const isMobileClient = useIsMobile() +const AppOrPublicSite = () => { + const isMobile = useIsMobile() + const { history } = useHistoryContext() + const [renderPublicSite, setRenderPublicSite] = useState( + isPublicSiteRoute(history.location) + ) + + useEffect(() => { + remoteConfigInstance.init() + }, []) const { value: foundUser } = useAsync(() => localStorage.getCurrentUserExists() @@ -35,25 +50,30 @@ export const Root = () => { useEffect(() => { // TODO: listen to history and change routes based on history... window.onpopstate = () => { - setRenderPublicSite(isPublicSiteRoute()) + setRenderPublicSite(isPublicSiteRoute(history.location)) } - }, []) + }, [history.location]) - const shouldRedirectToApp = foundUser && !isPublicSiteSubRoute() + const shouldRedirectToApp = + foundUser && !isPublicSiteSubRoute(history.location) if (renderPublicSite && !clientIsElectron && !shouldRedirectToApp) { return ( }> ) } + return +} + +export const Root = () => { return ( - }> - - + + + ) } diff --git a/packages/web/src/app/App.tsx b/packages/web/src/app/App.tsx index c63b3f9eecd..fe3e730dad9 100644 --- a/packages/web/src/app/App.tsx +++ b/packages/web/src/app/App.tsx @@ -1,6 +1,6 @@ // @refresh reset -import { Suspense, lazy } from 'react' +import { useEffect, Suspense, lazy } from 'react' import { FeatureFlags, useFeatureFlag } from '@audius/common' import { CoinflowPurchaseProtection } from '@coinflowlabs/react' @@ -9,14 +9,14 @@ import { Redirect, Route, Switch } from 'react-router-dom' import { CoinbasePayButtonProvider } from 'components/coinbase-pay-button' import { SomethingWrong } from 'pages/something-wrong/SomethingWrong' import { env } from 'services/env' +import { initWebVitals } from 'services/webVitals' import { SIGN_IN_PAGE, SIGN_ON_ALIASES, SIGN_UP_PAGE } from 'utils/route' import { AppErrorBoundary } from './AppErrorBoundary' import { AppProviders } from './AppProviders' +import { useHistoryContext } from './HistoryProvider' import WebPlayer from './web-player/WebPlayer' -import '../services/webVitals' - const SignOnPage = lazy(() => import('pages/sign-on-page')) const SignOn = lazy(() => import('pages/sign-on/SignOn')) const OAuthLoginPage = lazy(() => import('pages/oauth-login-page')) @@ -27,6 +27,12 @@ const MERCHANT_ID = env.COINFLOW_MERCHANT_ID const IS_PRODUCTION = env.ENVIRONMENT === 'production' export const AppInner = () => { + const { history } = useHistoryContext() + + useEffect(() => { + initWebVitals(history.location) + }, [history]) + const { isEnabled: isSignInRedesignEnabled, isLoaded } = useFeatureFlag( FeatureFlags.SIGN_UP_REDESIGN ) diff --git a/packages/web/src/app/AppProviders.tsx b/packages/web/src/app/AppProviders.tsx index 3b03f104091..9e9232d660a 100644 --- a/packages/web/src/app/AppProviders.tsx +++ b/packages/web/src/app/AppProviders.tsx @@ -1,22 +1,20 @@ import { ReactNode } from 'react' import { ConnectedRouter } from 'connected-react-router' -import { Provider } from 'react-redux' import { LastLocationProvider } from 'react-router-last-location' -import { PersistGate } from 'redux-persist/integration/react' import { RouterContextProvider } from 'components/animated-switch/RouterContextProvider' import { HeaderContextProvider } from 'components/header/mobile/HeaderContextProvider' import { NavProvider } from 'components/nav/store/context' import { ScrollProvider } from 'components/scroll-provider/ScrollProvider' import { ToastContextProvider } from 'components/toast/ToastContext' -import { persistor, store } from 'store/configureStore' -import history from 'utils/history' import { MainContentContextProvider } from '../pages/MainContentContext' import { AppContextProvider } from './AppContextProvider' import { AudiusQueryProvider } from './AudiusQueryProvider' +import { useHistoryContext } from './HistoryProvider' +import { ReduxProvider } from './ReduxProvider' import { ThemeProvider } from './ThemeProvider' import { TrpcProvider } from './TrpcProvider' @@ -25,35 +23,34 @@ type AppContextProps = { } export const AppProviders = ({ children }: AppContextProps) => { + const { history } = useHistoryContext() return ( - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + ) } diff --git a/packages/web/src/app/AudiusQueryProvider.tsx b/packages/web/src/app/AudiusQueryProvider.tsx index ffd0f004ec4..626aec1b765 100644 --- a/packages/web/src/app/AudiusQueryProvider.tsx +++ b/packages/web/src/app/AudiusQueryProvider.tsx @@ -1,34 +1,35 @@ import { ReactNode } from 'react' import { AudiusQueryContext } from '@audius/common' +import { useDispatch } from 'react-redux' import { apiClient } from 'services/audius-api-client' import { audiusBackendInstance } from 'services/audius-backend/audius-backend-instance' import { audiusSdk } from 'services/audius-sdk' import { env } from 'services/env' import { remoteConfigInstance } from 'services/remote-config/remote-config-instance' -import { store } from 'store/configureStore' import { reportToSentry } from 'store/errors/reportToSentry' type AudiusQueryProviderProps = { children: ReactNode } -export const audiusQueryContext = { - apiClient, - audiusBackend: audiusBackendInstance, - audiusSdk, - dispatch: store.dispatch, - reportToSentry, - env, - fetch, - remoteConfigInstance -} - export const AudiusQueryProvider = (props: AudiusQueryProviderProps) => { const { children } = props + const dispatch = useDispatch() return ( - + {children} ) diff --git a/packages/web/src/app/HistoryProvider.tsx b/packages/web/src/app/HistoryProvider.tsx new file mode 100644 index 00000000000..2d85f503d24 --- /dev/null +++ b/packages/web/src/app/HistoryProvider.tsx @@ -0,0 +1,31 @@ +import { createContext, memo, useContext } from 'react' + +import { History } from 'history' + +import { useSsrContext } from 'ssr/SsrContext' +import { createHistory } from 'utils/history' + +export type HistoryContextType = { + history: History +} + +export const useHistoryContext = () => { + return useContext(HistoryContext) +} + +export const HistoryContext = createContext({ + history: null as any +}) + +// TODO: could put getPathname in here +export const HistoryContextProvider = memo( + (props: { children: JSX.Element }) => { + const { history: ssrHistory } = useSsrContext() + const history = ssrHistory || createHistory() + return ( + + {props.children} + + ) + } +) diff --git a/packages/web/src/app/ReduxProvider.tsx b/packages/web/src/app/ReduxProvider.tsx new file mode 100644 index 00000000000..17f7df0343e --- /dev/null +++ b/packages/web/src/app/ReduxProvider.tsx @@ -0,0 +1,47 @@ +import { ReactNode, useState } from 'react' + +import { Provider } from 'react-redux' +import { Persistor, persistStore } from 'redux-persist' +import { PersistGate } from 'redux-persist/integration/react' + +import { useIsMobile } from 'hooks/useIsMobile' +import { configureStore } from 'store/configureStore' +import logger from 'utils/logger' + +import { useSsrContext } from '../ssr/SsrContext' + +import { useHistoryContext } from './HistoryProvider' + +export const ReduxProvider = ({ children }: { children: ReactNode }) => { + const { pageProps, isServerSide } = useSsrContext() + const { history } = useHistoryContext() + const isMobile = useIsMobile() + + const [store, setStore] = useState>() + const [persistor, setPersistor] = useState() + + if (!store) { + const store = configureStore(history, isMobile, pageProps, isServerSide) + setStore(store) + const persistor = persistStore(store) + setPersistor(persistor) + + // Mount store to window for easy access + if (typeof window !== 'undefined') { + window.store = store + } + + // Set up logger on store + if (!isServerSide) { + logger(store) + } + } + + return store && persistor ? ( + + + {() => children} + + + ) : null +} diff --git a/packages/web/src/app/web-player/WebPlayer.jsx b/packages/web/src/app/web-player/WebPlayer.jsx index ac8fbb9e2f7..4689ae60eec 100644 --- a/packages/web/src/app/web-player/WebPlayer.jsx +++ b/packages/web/src/app/web-player/WebPlayer.jsx @@ -35,10 +35,10 @@ import { DownloadAppBanner } from 'components/banner/DownloadAppBanner' import { UpdateAppBanner } from 'components/banner/UpdateAppBanner' import { Web3ErrorBanner } from 'components/banner/Web3ErrorBanner' import { ChatListener } from 'components/chat-listener/ChatListener' +import { ClientOnly } from 'components/client-only/ClientOnly' import CookieBanner from 'components/cookie-banner/CookieBanner' import { DevModeMananger } from 'components/dev-mode-manager/DevModeManager' import { HeaderContextConsumer } from 'components/header/mobile/HeaderContextProvider' -import ConnectedMusicConfetti from 'components/music-confetti/ConnectedMusicConfetti' import Navigator from 'components/nav/Navigator' import TopLevelPage from 'components/nav/mobile/TopLevelPage' import Notice from 'components/notice/Notice' @@ -97,13 +97,14 @@ import Visualizer from 'pages/visualizer/Visualizer' import { getFeatureEnabled } from 'services/remote-config/featureFlagHelpers' import { remoteConfigInstance } from 'services/remote-config/remote-config-instance' import { initializeSentry } from 'services/sentry' +import { SsrContext } from 'ssr/SsrContext' import { setVisibility as setAppModalCTAVisibility } from 'store/application/ui/app-cta-modal/slice' import { getShowCookieBanner } from 'store/application/ui/cookieBanner/selectors' import { incrementScrollCount as incrementScrollCountAction, decrementScrollCount as decrementScrollCountAction } from 'store/application/ui/scrollLock/actions' -import { isMobile, getClient } from 'utils/clientUtil' +import { getClient } from 'utils/clientUtil' import 'utils/redirect' import { FEED_PAGE, @@ -181,7 +182,7 @@ import { } from 'utils/route' import { getTheme as getSystemTheme } from 'utils/theme/theme' -import styles from './App.module.css' +import styles from './WebPlayer.module.css' const { setTheme } = themeActions const { getTheme } = themeSelectors @@ -191,6 +192,9 @@ const { getHasAccount, getAccountStatus, getUserId, getUserHandle } = const UploadPage = lazy(() => import('pages/upload-page')) const Modals = lazy(() => import('pages/modals/Modals')) +const ConnectedMusicConfetti = lazy(() => + import('components/music-confetti/ConnectedMusicConfetti') +) const includeSearch = (search) => { return search.includes('oauth_token') || search.includes('code') @@ -199,6 +203,8 @@ const includeSearch = (search) => { initializeSentry() class WebPlayer extends Component { + static contextType = SsrContext + state = { mainContent: null, @@ -232,7 +238,7 @@ class WebPlayer extends Component { this.scrollToTop() this.setState({ initialPage: false, - currentRoute: getPathname(location) + currentRoute: getPathname(this.props.history.location) }) } ) @@ -281,44 +287,46 @@ class WebPlayer extends Component { } }) - const windowOpen = window.open - - const a = document.createElement('a') - window.open = (...args) => { - const url = args[0] - if (!url) { - const popup = windowOpen(window.location) - const win = { - popup, - closed: popup.closed, - close: () => { - popup.close() + if (typeof window !== 'undefined') { + const windowOpen = window.open + + const a = document.createElement('a') + window.open = (...args) => { + const url = args[0] + if (!url) { + const popup = windowOpen(window.location) + const win = { + popup, + closed: popup.closed, + close: () => { + popup.close() + } } - } - Object.defineProperty(win, 'location', { - get: () => { - a.href = popup.location - if (!a.search) { + Object.defineProperty(win, 'location', { + get: () => { + a.href = popup.location + if (!a.search) { + return { + href: popup.location, + search: a.search, + hostname: '' + } + } return { href: popup.location, search: a.search, - hostname: '' + hostname: a.hostname } + }, + set: (locationHref) => { + popup.location = locationHref + this.locationHref = locationHref } - return { - href: popup.location, - search: a.search, - hostname: a.hostname - } - }, - set: (locationHref) => { - popup.location = locationHref - this.locationHref = locationHref - } - }) - return win + }) + return win + } + return windowOpen(...args) } - return windowOpen(...args) } } @@ -418,8 +426,8 @@ class WebPlayer extends Component { showRequiresWebUpdate, initialPage } = this.state - const client = getClient() - const isMobileClient = client === Client.MOBILE + + const isMobile = this.context.isMobile if (showRequiresUpdate) return ( @@ -439,7 +447,7 @@ class WebPlayer extends Component { /> ) - const SwitchComponent = isMobile() ? AnimatedSwitch : Switch + const SwitchComponent = this.context.isMobile ? AnimatedSwitch : Switch const noScroll = matchPath(this.state.currentRoute, CHAT_PAGE) return ( @@ -464,7 +472,7 @@ class WebPlayer extends Component { {this.props.isChatEnabled ? : null} -
+
{this.props.showCookieBanner ? : null} @@ -473,12 +481,12 @@ class WebPlayer extends Component { id={MAIN_CONTENT_ID} role='main' className={cn(styles.mainContentWrapper, { - [styles.mainContentWrapperMobile]: isMobileClient, + [styles.mainContentWrapperMobile]: isMobile, [styles.noScroll]: noScroll })} > - {isMobileClient && } - {isMobileClient && } + {isMobile && } + {isMobile && } @@ -489,7 +497,7 @@ class WebPlayer extends Component { ))} @@ -501,7 +509,7 @@ class WebPlayer extends Component { ( } /> } /> ( )} @@ -706,13 +714,13 @@ class WebPlayer extends Component { ( ( ( )} @@ -742,56 +750,56 @@ class WebPlayer extends Component { } /> } /> ( )} @@ -799,7 +807,7 @@ class WebPlayer extends Component { ( )} @@ -807,7 +815,7 @@ class WebPlayer extends Component { } /> @@ -893,43 +901,43 @@ class WebPlayer extends Component {
- + - - - - - + + + + {/* Non-mobile */} - {!isMobileClient ? : null} - {!isMobileClient ? : null} - {!isMobileClient ? : null} + {!isMobile ? : null} + {!isMobile ? : null} + {!isMobile ? : null} {/* Mobile-only */} - {isMobileClient ? ( + {isMobile ? ( null) vitest.mock('./visualizer/Visualizer', () => () => null) -vitest.mock('react-spring/renderprops', () => ({ +vitest.mock('react-spring/renderprops.cjs', () => ({ Spring: () => null, Transition: () => null })) @@ -28,7 +28,9 @@ vitest.mock('services/solana-client/SolanaClient', () => ({ describe('smoke test', () => { it('renders without crashing', () => { + const history = createHistory() const rootNode = document.createElement('div') + const store = configureStore(history, false, {}, false) ReactDOM.render( diff --git a/packages/web/src/assets/styles/index.css b/packages/web/src/assets/styles/index.css index 38d731054b1..6e767ff880f 100644 --- a/packages/web/src/assets/styles/index.css +++ b/packages/web/src/assets/styles/index.css @@ -19,3 +19,10 @@ h5, h6 { color: var(--neutral); } + +body { + background: var(--background); + backface-visibility: hidden; + /** Turn off user select so the web app behaves more like "an app" */ + user-select: none; +} diff --git a/packages/web/src/common/store/analytics/sagas.ts b/packages/web/src/common/store/analytics/sagas.ts index 1af7d91e37a..9848fc716b7 100644 --- a/packages/web/src/common/store/analytics/sagas.ts +++ b/packages/web/src/common/store/analytics/sagas.ts @@ -37,7 +37,9 @@ function* watchIdentifyEvent() { function* initProviders() { const analytics = yield* getContext('analytics') - yield call(analytics.init) + const isMobile = yield* getContext('isMobile') + + yield call(analytics.init, isMobile) } export default function sagas() { diff --git a/packages/web/src/common/store/cache/tracks/utils/helpers.ts b/packages/web/src/common/store/cache/tracks/utils/helpers.ts index 743ae2417b8..440f595d93f 100644 --- a/packages/web/src/common/store/cache/tracks/utils/helpers.ts +++ b/packages/web/src/common/store/cache/tracks/utils/helpers.ts @@ -20,7 +20,8 @@ const getAccountUser = accountSelectors.getAccountUser * @param metadataArray */ export function* addUsersFromTracks( - metadataArray: T[] + metadataArray: T[], + isInitialFetchAfterSsr?: boolean ) { yield* waitForRead() const audiusBackendInstance = yield* getContext('audiusBackendInstance') @@ -44,6 +45,11 @@ export function* addUsersFromTracks( users = users.filter((user) => !(currentUserId && user.id === currentUserId)) yield put( - cacheActions.add(Kind.USERS, users, /* replace */ false, /* persist */ true) + cacheActions.add( + Kind.USERS, + users, + /* replace */ isInitialFetchAfterSsr, + /* persist */ true + ) ) } diff --git a/packages/web/src/common/store/cache/tracks/utils/retrieveTracks.ts b/packages/web/src/common/store/cache/tracks/utils/retrieveTracks.ts index 83e7091442e..80eb74a7841 100644 --- a/packages/web/src/common/store/cache/tracks/utils/retrieveTracks.ts +++ b/packages/web/src/common/store/cache/tracks/utils/retrieveTracks.ts @@ -9,7 +9,9 @@ import { getContext, cacheSelectors, cacheTracksSelectors, - cacheTracksActions + cacheTracksActions, + trackPageSelectors, + trackPageActions } from '@audius/common' import { call, put, select, spawn } from 'typed-redux-saga' @@ -27,6 +29,8 @@ const { getEntryTimestamp } = cacheSelectors const { getTracks: getTracksSelector } = cacheTracksSelectors const { setPermalink } = cacheTracksActions const getUserId = accountSelectors.getUserId +const { getIsInitialFetchAfterSsr } = trackPageSelectors +const { setIsInitialFetchAfterSsr } = trackPageActions type UnlistedTrackRequest = { id: ID; url_title: string; handle: string } type RetrieveTracksArgs = { @@ -55,6 +59,9 @@ export function* retrieveTrackByHandleAndSlug({ forceRetrieveFromSource = false }: RetrieveTrackByHandleAndSlugArgs) { const permalink = `/${handle}/${slug}` + + // Check if this is the first fetch after server side rendering the track page + const isInitialFetchAfterSsr = yield* select(getIsInitialFetchAfterSsr) const tracks = (yield* call( // @ts-ignore retrieve should be refactored to ts first retrieve, @@ -84,9 +91,12 @@ export function* retrieveTrackByHandleAndSlug({ }, kind: Kind.TRACKS, idField: 'track_id', - forceRetrieveFromSource, + // If this is the first fetch after server side rendering the track page, + // force retrieve from source to ensure we have personalized data + forceRetrieveFromSource: + forceRetrieveFromSource ?? isInitialFetchAfterSsr, shouldSetLoading: true, - deleteExistingEntry: false, + deleteExistingEntry: isInitialFetchAfterSsr, getEntriesTimestamp: function* (ids: ID[]) { const selected = yield* select( (state: CommonState, ids: ID[]) => @@ -100,12 +110,15 @@ export function* retrieveTrackByHandleAndSlug({ }, onBeforeAddToCache: function* (tracks: TrackMetadata[]) { const audiusBackendInstance = yield* getContext('audiusBackendInstance') - yield* addUsersFromTracks(tracks) + yield* addUsersFromTracks(tracks, isInitialFetchAfterSsr) const [track] = tracks const isLegacyPermalink = track.permalink !== permalink if (isLegacyPermalink) { yield* put(setPermalink(permalink, track.track_id)) } + if (isInitialFetchAfterSsr) { + yield* put(setIsInitialFetchAfterSsr(false)) + } return tracks.map((track) => reformat(track, audiusBackendInstance)) } } diff --git a/packages/web/src/common/store/pages/audio-rewards/sagas.ts b/packages/web/src/common/store/pages/audio-rewards/sagas.ts index 367ab32c5f7..9564e1968ae 100644 --- a/packages/web/src/common/store/pages/audio-rewards/sagas.ts +++ b/packages/web/src/common/store/pages/audio-rewards/sagas.ts @@ -34,6 +34,7 @@ import { delay } from 'typed-redux-saga' +import { isElectron } from 'utils/clientUtil' import { AUDIO_PAGE } from 'utils/route' import { waitForRead } from 'utils/sagaHelpers' import { @@ -264,6 +265,7 @@ function* claimChallengeRewardAsync( })) .filter(({ amount }) => amount > 0) // We shouldn't have any 0 amount challenges, but just in case. + const isMobile = yield* getContext('isMobile') const response: { error?: string; aaoErrorCode?: number } = yield* call( audiusBackendInstance.submitAndEvaluateAttestations, { @@ -277,7 +279,8 @@ function* claimChallengeRewardAsync( AAOEndpoint, parallelization, feePayerOverride, - isFinalAttempt: !retryOnFailure + isFinalAttempt: !retryOnFailure, + source: isMobile ? 'mobile' : isElectron() ? 'electron' : 'web' } ) if (response.error) { diff --git a/packages/web/src/common/store/pages/signon/reducer.js b/packages/web/src/common/store/pages/signon/reducer.js index 3a7efd674e9..6880fccada9 100644 --- a/packages/web/src/common/store/pages/signon/reducer.js +++ b/packages/web/src/common/store/pages/signon/reducer.js @@ -1,4 +1,3 @@ -import { isMobile } from 'utils/clientUtil' import { FEED_PAGE } from 'utils/route' import { @@ -116,7 +115,7 @@ const actionsMap = { page: action.page || state.page } }, - [NEXT_PAGE](state) { + [NEXT_PAGE](state, action) { let newPage switch (state.page) { case Pages.EMAIL: @@ -129,7 +128,7 @@ const actionsMap = { newPage = Pages.FOLLOW break case Pages.FOLLOW: { - if (!isMobile()) { + if (!action.isMobile) { newPage = Pages.APP_CTA } else { newPage = Pages.LOADING diff --git a/packages/web/src/common/store/pages/track/sagas.js b/packages/web/src/common/store/pages/track/sagas.js index d90d1e0dfff..049360ed972 100644 --- a/packages/web/src/common/store/pages/track/sagas.js +++ b/packages/web/src/common/store/pages/track/sagas.js @@ -170,7 +170,6 @@ function* watchFetchTrack() { if (!track) { if (isReachable) { yield put(pushRoute(NOT_FOUND_PAGE)) - return } } else { yield put(trackPageActions.setTrackId(track.track_id)) diff --git a/packages/web/src/common/store/queue/sagas.ts b/packages/web/src/common/store/queue/sagas.ts index 5642a1357a0..d7d26a81956 100644 --- a/packages/web/src/common/store/queue/sagas.ts +++ b/packages/web/src/common/store/queue/sagas.ts @@ -30,6 +30,7 @@ import { all, call, put, select, takeEvery, takeLatest } from 'typed-redux-saga' import { make } from 'common/store/analytics/actions' import { getRecommendedTracks } from 'common/store/recommendation/sagas' import { isPreview } from 'common/utils/isPreview' +import { getLocation } from 'store/routing/selectors' const { getCollectible, @@ -229,9 +230,12 @@ export function* watchPlay() { 'getLineupSelectorForRoute' ) if (!getLineupSelectorForRoute) return + + const location = yield* select(getLocation) + // @ts-ignore todo const lineup: LineupState<{ id: number }> = yield* select( - getLineupSelectorForRoute() + getLineupSelectorForRoute(location) ) if (!lineup) return if (lineup.entries.length > 0) { diff --git a/packages/web/src/components/add-funds-modal/AddFundsModal.tsx b/packages/web/src/components/add-funds-modal/AddFundsModal.tsx index 778fbc1f032..98ede56d9ea 100644 --- a/packages/web/src/components/add-funds-modal/AddFundsModal.tsx +++ b/packages/web/src/components/add-funds-modal/AddFundsModal.tsx @@ -16,8 +16,8 @@ import { useDispatch, useSelector } from 'react-redux' import { AddFunds } from 'components/add-funds/AddFunds' import { Text } from 'components/typography' import { USDCManualTransfer } from 'components/usdc-manual-transfer/USDCManualTransfer' +import { useIsMobile } from 'hooks/useIsMobile' import ModalDrawer from 'pages/audio-rewards-page/components/modals/ModalDrawer' -import { isMobile } from 'utils/clientUtil' import zIndex from 'utils/zIndex' import styles from './AddFundsModal.module.css' @@ -35,7 +35,7 @@ export const AddFundsModal = () => { const { isOpen, onClose } = useAddFundsModal() const dispatch = useDispatch() const buyUSDCStage = useSelector(getBuyUSDCFlowStage) - const mobile = isMobile() + const isMobile = useIsMobile() const [page, setPage] = useState('add-funds') @@ -87,9 +87,9 @@ export const AddFundsModal = () => { isFullscreen={false} > (undefined) - const mobile = isMobile() + const isMobile = useIsMobile() const { data: balanceBN } = useUSDCBalance({ isPolling: true }) const balance = USDC(balanceBN ?? new BN(0)).value @@ -50,7 +50,7 @@ export const AddFunds = ({
diff --git a/packages/web/src/components/address-tile/AddressTile.tsx b/packages/web/src/components/address-tile/AddressTile.tsx index f28222f0626..07d4231accb 100644 --- a/packages/web/src/components/address-tile/AddressTile.tsx +++ b/packages/web/src/components/address-tile/AddressTile.tsx @@ -14,7 +14,7 @@ import { import { BN } from 'bn.js' import { ToastContext } from 'components/toast/ToastContext' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { copyToClipboard } from 'utils/clipboardUtil' const messages = { @@ -35,7 +35,7 @@ export const AddressTile = ({ }: AddressTileProps) => { const { color } = useTheme() const { toast } = useContext(ToastContext) - const mobile = isMobile() + const isMobile = useIsMobile() const { data: balanceBN } = useUSDCBalance({ isPolling: true }) const handleCopyPress = useCallback(() => { @@ -86,7 +86,7 @@ export const AddressTile = ({ }} variant='body' > - {mobile ? shortenSPLAddress(address, 12) : address} + {isMobile ? shortenSPLAddress(address, 12) : address} diff --git a/packages/web/src/components/animated-switch/RouterContextProvider.tsx b/packages/web/src/components/animated-switch/RouterContextProvider.tsx index a0aefba7dcb..3a71355015e 100644 --- a/packages/web/src/components/animated-switch/RouterContextProvider.tsx +++ b/packages/web/src/components/animated-switch/RouterContextProvider.tsx @@ -1,6 +1,6 @@ import { createContext, memo, useState } from 'react' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' export enum SlideDirection { FROM_LEFT = 'left', @@ -34,7 +34,9 @@ export const RouterContextProvider = memo( SlideDirection.FROM_LEFT ) - if (useIsMobile()) { + const isMobile = useIsMobile() + + if (isMobile) { return ( { onBeforeClickApp = () => {}, onBeforeClickDismissed = () => {} } = props + const { history } = useHistoryContext() + const isMobile = useIsMobile() const [isDismissed, setIsDismissed] = useSessionStorage( 'app-redirect-popover', false @@ -103,10 +106,10 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { }, []) const shouldShow = - !matchPath(window.location.pathname, { path: '/', exact: true }) && + !matchPath(history.location.pathname, { path: '/', exact: true }) && animDelay && !isDismissed && - isMobile() && + isMobile && !(navigator.userAgent === 'probers') useEffect(() => { @@ -127,7 +130,7 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { const onClick = () => { onBeforeClickApp() - const pathname = getPathname() + const pathname = getPathname(history.location) const newHref = `https://redirect.audius.co${APP_REDIRECT}${pathname}` // If we're on the signup page, copy the URL to clipboard on app redirect diff --git a/packages/web/src/components/artist-recommendations/ArtistRecommendations.tsx b/packages/web/src/components/artist-recommendations/ArtistRecommendations.tsx index 6078627d027..b4c69a3fcf4 100644 --- a/packages/web/src/components/artist-recommendations/ArtistRecommendations.tsx +++ b/packages/web/src/components/artist-recommendations/ArtistRecommendations.tsx @@ -26,8 +26,8 @@ import { FollowButton } from 'components/follow-button/FollowButton' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import { MountPlacement } from 'components/types' import UserBadges from 'components/user-badges/UserBadges' +import { useIsMobile } from 'hooks/useIsMobile' import { useUserProfilePicture } from 'hooks/useUserProfilePicture' -import { useIsMobile } from 'utils/clientUtil' import { profilePage } from 'utils/route' import styles from './ArtistRecommendations.module.css' diff --git a/packages/web/src/components/background-animations/MusicConfetti.tsx b/packages/web/src/components/background-animations/MusicConfetti.tsx index 905893d9966..99ee36b5ac8 100644 --- a/packages/web/src/components/background-animations/MusicConfetti.tsx +++ b/packages/web/src/components/background-animations/MusicConfetti.tsx @@ -1,4 +1,4 @@ -import { useRef, useCallback, useEffect, useState } from 'react' +import { useRef, useCallback, useEffect, useState, useMemo } from 'react' import { Theme } from '@audius/common' @@ -14,25 +14,6 @@ const DEFAULTS = { particleRate: 0.1 } -const PATHS = [ - // Heart - new Path2D( - 'M4.8294,0 C1.83702857,0 0,2.65379464 0,4.61012277 C0,8.84082589 5.27554286,12.500625 9,15 C12.7244571,12.4996875 18,8.84082589 18,4.61012277 C18,2.65363058 16.1638457,0 13.1706,0 C11.4991714,0 10.0707429,1.21490625 9,2.36835938 C7.92822857,1.21478906 6.50088,0 4.8294,0 Z' - ), - // Listens - new Path2D( - 'M9.01215343,0 C4.03768506,0 0,4.14271233 0,9.24657534 L0,13.9315068 C0.0244073147,16.175737 1.82684761,18 4.013172,18 L4.54169274,18 C4.997924,18 5.38282706,17.6050849 5.38282706,17.1369863 L5.38282706,10.6027397 C5.38282706,10.1346411 4.997924,9.73972603 4.54169274,9.73972603 L4.013172,9.73972603 C3.14762075,9.73972603 2.33090336,10.0354192 1.65780365,10.5285699 L1.65780365,9.24657534 C1.65780365,5.10386301 4.95000337,1.7260274 8.98768843,1.7260274 C13.0253735,1.7260274 16.3420863,5.10386301 16.3420863,9.24657534 L16.3420863,10.5285699 C15.6689866,10.0354192 14.8757248,9.73972603 13.9867179,9.73972603 L13.4581972,9.73972603 C13.0019659,9.73972603 12.6170629,10.1346411 12.6170629,10.6027397 L12.6170629,17.1369863 C12.6170629,17.6050849 13.0019659,18 13.4581972,18 L13.9867179,18 C16.1975073,18 17.9765785,16.175737 17.9998899,13.9315068 L17.9998899,9.24657534 C18.0242972,4.14271233 13.9867179,0 9.01224955,0 L9.01215343,0 Z' - ), - // Note - new Path2D( - 'M3,13.0400072 L3,3.61346039 C3,3.16198653 3.28424981,2.76289841 3.70172501,2.62823505 L12.701725,0.0477059646 C13.3456556,-0.160004241 14,0.3365598 14,1.0329313 L14,12.9033651 C14,12.9179063 13.9997087,12.9323773 13.9991318,12.9467722 C13.9997094,12.9644586 14,12.9822021 14,13 C14,14.1045695 12.8807119,15 11.5,15 C10.1192881,15 9,14.1045695 9,13 C9,11.8954305 10.1192881,11 11.5,11 C11.6712329,11 11.838445,11.0137721 12,11.0400072 L12,3.61346039 L5,5.5488572 L5,14.9677884 C5,14.9726204 4.99996783,14.9774447 4.99990371,14.9822611 C4.99996786,14.988168 5,14.994081 5,15 C5,16.1045695 3.88071187,17 2.5,17 C1.11928813,17 0,16.1045695 0,15 C0,13.8954305 1.11928813,13 2.5,13 C2.67123292,13 2.83844503,13.0137721 3,13.0400072 Z' - ), - // Playlists - new Path2D( - 'M5.46563786,16.9245466 C4.92388449,17.5744882 4.02157241,18 3,18 C1.34314575,18 0,16.8807119 0,15.5 C0,14.1192881 1.34314575,13 3,13 C3.35063542,13 3.68722107,13.0501285 4,13.1422548 L4,3.02055066 C4,2.49224061 4.31978104,2.02523181 4.78944063,1.86765013 L10.5394406,0.0558250276 C11.2638626,-0.187235311 12,0.393838806 12,1.20872555 C12,2.01398116 12,2.61792286 12,3.02055066 C12,3.62449236 11.4511634,4.01020322 11,4.12121212 C10.3508668,4.28093157 8.68420009,4.62436591 6,5.15151515 L6,16.0224658 C6,16.5009995 5.80514083,16.7960063 5.46563786,16.9245466 Z M13,6 L17,6 C17.5522847,6 18,6.44771525 18,7 C18,7.55228475 17.5522847,8 17,8 L13,8 C12.4477153,8 12,7.55228475 12,7 C12,6.44771525 12.4477153,6 13,6 Z M11,10 L17,10 C17.5522847,10 18,10.4477153 18,11 C18,11.5522847 17.5522847,12 17,12 L11,12 C10.4477153,12 10,11.5522847 10,11 C10,10.4477153 10.4477153,10 11,10 Z M11,14 L17,14 C17.5522847,14 18,14.4477153 18,15 C18,15.5522847 17.5522847,16 17,16 L11,16 C10.4477153,16 10,15.5522847 10,15 C10,14.4477153 10.4477153,14 11,14 Z' - ) -] - type MusicConfettiProps = { withBackground?: boolean zIndex?: number @@ -48,6 +29,28 @@ export const MusicConfetti = ({ theme, isMobile }: MusicConfettiProps) => { + const PATHS = useMemo( + () => [ + // Heart + new Path2D( + 'M4.8294,0 C1.83702857,0 0,2.65379464 0,4.61012277 C0,8.84082589 5.27554286,12.500625 9,15 C12.7244571,12.4996875 18,8.84082589 18,4.61012277 C18,2.65363058 16.1638457,0 13.1706,0 C11.4991714,0 10.0707429,1.21490625 9,2.36835938 C7.92822857,1.21478906 6.50088,0 4.8294,0 Z' + ), + // Listens + new Path2D( + 'M9.01215343,0 C4.03768506,0 0,4.14271233 0,9.24657534 L0,13.9315068 C0.0244073147,16.175737 1.82684761,18 4.013172,18 L4.54169274,18 C4.997924,18 5.38282706,17.6050849 5.38282706,17.1369863 L5.38282706,10.6027397 C5.38282706,10.1346411 4.997924,9.73972603 4.54169274,9.73972603 L4.013172,9.73972603 C3.14762075,9.73972603 2.33090336,10.0354192 1.65780365,10.5285699 L1.65780365,9.24657534 C1.65780365,5.10386301 4.95000337,1.7260274 8.98768843,1.7260274 C13.0253735,1.7260274 16.3420863,5.10386301 16.3420863,9.24657534 L16.3420863,10.5285699 C15.6689866,10.0354192 14.8757248,9.73972603 13.9867179,9.73972603 L13.4581972,9.73972603 C13.0019659,9.73972603 12.6170629,10.1346411 12.6170629,10.6027397 L12.6170629,17.1369863 C12.6170629,17.6050849 13.0019659,18 13.4581972,18 L13.9867179,18 C16.1975073,18 17.9765785,16.175737 17.9998899,13.9315068 L17.9998899,9.24657534 C18.0242972,4.14271233 13.9867179,0 9.01224955,0 L9.01215343,0 Z' + ), + // Note + new Path2D( + 'M3,13.0400072 L3,3.61346039 C3,3.16198653 3.28424981,2.76289841 3.70172501,2.62823505 L12.701725,0.0477059646 C13.3456556,-0.160004241 14,0.3365598 14,1.0329313 L14,12.9033651 C14,12.9179063 13.9997087,12.9323773 13.9991318,12.9467722 C13.9997094,12.9644586 14,12.9822021 14,13 C14,14.1045695 12.8807119,15 11.5,15 C10.1192881,15 9,14.1045695 9,13 C9,11.8954305 10.1192881,11 11.5,11 C11.6712329,11 11.838445,11.0137721 12,11.0400072 L12,3.61346039 L5,5.5488572 L5,14.9677884 C5,14.9726204 4.99996783,14.9774447 4.99990371,14.9822611 C4.99996786,14.988168 5,14.994081 5,15 C5,16.1045695 3.88071187,17 2.5,17 C1.11928813,17 0,16.1045695 0,15 C0,13.8954305 1.11928813,13 2.5,13 C2.67123292,13 2.83844503,13.0137721 3,13.0400072 Z' + ), + // Playlists + new Path2D( + 'M5.46563786,16.9245466 C4.92388449,17.5744882 4.02157241,18 3,18 C1.34314575,18 0,16.8807119 0,15.5 C0,14.1192881 1.34314575,13 3,13 C3.35063542,13 3.68722107,13.0501285 4,13.1422548 L4,3.02055066 C4,2.49224061 4.31978104,2.02523181 4.78944063,1.86765013 L10.5394406,0.0558250276 C11.2638626,-0.187235311 12,0.393838806 12,1.20872555 C12,2.01398116 12,2.61792286 12,3.02055066 C12,3.62449236 11.4511634,4.01020322 11,4.12121212 C10.3508668,4.28093157 8.68420009,4.62436591 6,5.15151515 L6,16.0224658 C6,16.5009995 5.80514083,16.7960063 5.46563786,16.9245466 Z M13,6 L17,6 C17.5522847,6 18,6.44771525 18,7 C18,7.55228475 17.5522847,8 17,8 L13,8 C12.4477153,8 12,7.55228475 12,7 C12,6.44771525 12.4477153,6 13,6 Z M11,10 L17,10 C17.5522847,10 18,10.4477153 18,11 C18,11.5522847 17.5522847,12 17,12 L11,12 C10.4477153,12 10,11.5522847 10,11 C10,10.4477153 10.4477153,10 11,10 Z M11,14 L17,14 C17.5522847,14 18,14.4477153 18,15 C18,15.5522847 17.5522847,16 17,16 L11,16 C10.4477153,16 10,15.5522847 10,15 C10,14.4477153 10.4477153,14 11,14 Z' + ) + ], + [] + ) + const confettiRef = useRef(null) const [colors, setColors] = useState([ getCurrentThemeColors()['--primary'], @@ -83,7 +86,7 @@ export const MusicConfetti = ({ confetti.run() confettiRef.current = confetti }, - [onCompletion, isMatrix, isMobile, colors] + [onCompletion, isMatrix, isMobile, colors, PATHS] ) return ( diff --git a/packages/web/src/components/banner/AppBannerWrapper.tsx b/packages/web/src/components/banner/AppBannerWrapper.tsx index ab1ddea2369..9a981c8d16d 100644 --- a/packages/web/src/components/banner/AppBannerWrapper.tsx +++ b/packages/web/src/components/banner/AppBannerWrapper.tsx @@ -1,6 +1,6 @@ import cn from 'classnames' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import styles from './AppBannerWrapper.module.css' @@ -12,8 +12,11 @@ export const AppBannerWrapper = ({ children }: { children: React.ReactNode -}) => ( -
- {children} -
-) +}) => { + const isMobile = useIsMobile() + return ( +
+ {children} +
+ ) +} diff --git a/packages/web/src/components/banner/DirectMessagesBanner.tsx b/packages/web/src/components/banner/DirectMessagesBanner.tsx index c42b2d95488..efda0bb5d9b 100644 --- a/packages/web/src/components/banner/DirectMessagesBanner.tsx +++ b/packages/web/src/components/banner/DirectMessagesBanner.tsx @@ -1,12 +1,12 @@ import { useCallback, useState } from 'react' -import { Client, Name, accountSelectors } from '@audius/common' +import { Name, accountSelectors } from '@audius/common' import { push as pushRoute } from 'connected-react-router' import { useDispatch } from 'react-redux' import { useSelector } from 'common/hooks/useSelector' import { make } from 'common/store/analytics/actions' -import { getClient } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { CHATS_PAGE } from 'utils/route' import { CallToActionBanner } from './CallToActionBanner' @@ -27,7 +27,7 @@ const DIRECT_MESSAGES_BANNER_LOCAL_STORAGE_KEY = 'dismissDirectMessagesBanner' export const DirectMessagesBanner = () => { const dispatch = useDispatch() const signedIn = useSelector(getHasAccount) - const isMobile = getClient() === Client.MOBILE + const isMobile = useIsMobile() const hasDismissed = window.localStorage.getItem( DIRECT_MESSAGES_BANNER_LOCAL_STORAGE_KEY ) diff --git a/packages/web/src/components/banner/DownloadAppBanner.tsx b/packages/web/src/components/banner/DownloadAppBanner.tsx index c921865f965..425165ece68 100644 --- a/packages/web/src/components/banner/DownloadAppBanner.tsx +++ b/packages/web/src/components/banner/DownloadAppBanner.tsx @@ -4,6 +4,7 @@ import { Client, accountSelectors } from '@audius/common' import { useDispatch } from 'react-redux' import { useSelector } from 'common/hooks/useSelector' +import { localStorage } from 'services/local-storage' import { setVisibility as setAppModalCTAVisibility } from 'store/application/ui/app-cta-modal/slice' import { getClient } from 'utils/clientUtil' @@ -25,9 +26,7 @@ const messages = { export const DownloadAppBanner = () => { const dispatch = useDispatch() const signedIn = useSelector(getHasAccount) - const hasDismissed = window.localStorage.getItem( - MOBILE_BANNER_LOCAL_STORAGE_KEY - ) + const hasDismissed = localStorage.getItem(MOBILE_BANNER_LOCAL_STORAGE_KEY) const isDesktopWeb = getClient() === Client.DESKTOP const [isVisible, setIsVisible] = useState( !hasDismissed && isDesktopWeb && !signedIn @@ -35,7 +34,7 @@ export const DownloadAppBanner = () => { const handleClose = useCallback(() => { setIsVisible(false) - window.localStorage.setItem(MOBILE_BANNER_LOCAL_STORAGE_KEY, 'true') + localStorage.setItem(MOBILE_BANNER_LOCAL_STORAGE_KEY, 'true') }, []) const handleAccept = useCallback(() => { diff --git a/packages/web/src/components/banner/Web3ErrorBanner.tsx b/packages/web/src/components/banner/Web3ErrorBanner.tsx index d14468748cf..93228dc4333 100644 --- a/packages/web/src/components/banner/Web3ErrorBanner.tsx +++ b/packages/web/src/components/banner/Web3ErrorBanner.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react' import { getWeb3Error } from 'common/store/backend/selectors' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { useSelector } from 'utils/reducer' import { CallToActionBanner } from './CallToActionBanner' @@ -23,6 +23,7 @@ const META_MASK_SETUP_URL = export const Web3ErrorBanner = () => { const web3Error = useSelector(getWeb3Error) const [isVisible, setIsVisible] = useState(web3Error) + const isMobile = useIsMobile() const handleAccept = useCallback(() => { const win = window.open(META_MASK_SETUP_URL, '_blank') @@ -42,8 +43,8 @@ export const Web3ErrorBanner = () => { return isVisible ? ( diff --git a/packages/web/src/components/browser-push-confirmation-modal/BrowserPushConfirmationModal.tsx b/packages/web/src/components/browser-push-confirmation-modal/BrowserPushConfirmationModal.tsx index 4065480bd06..5549a036486 100644 --- a/packages/web/src/components/browser-push-confirmation-modal/BrowserPushConfirmationModal.tsx +++ b/packages/web/src/components/browser-push-confirmation-modal/BrowserPushConfirmationModal.tsx @@ -12,6 +12,7 @@ import cn from 'classnames' import { connect } from 'react-redux' import { Dispatch } from 'redux' +import { useIsMobile } from 'hooks/useIsMobile' import { audiusBackendInstance } from 'services/audius-backend/audius-backend-instance' import { AppState } from 'store/types' import { @@ -20,7 +21,7 @@ import { subscribeSafariPushBrowser, Permission } from 'utils/browserNotifications' -import { isElectron, isMobile } from 'utils/clientUtil' +import { isElectron } from 'utils/clientUtil' import styles from './BrowserPushConfirmationModal.module.css' const { setVisibility } = modalsActions @@ -61,6 +62,7 @@ const ConnectedBrowserPushConfirmationModal = ({ }: BrowserPushConfirmationModal) => { const { permission } = browserNotificationSettings const [pushPermission] = useState(permission) + const isMobile = useIsMobile() const onEnabled = useCallback(() => { let cancelled = false @@ -123,7 +125,7 @@ const ConnectedBrowserPushConfirmationModal = ({ title={messages.title} titleClassName={styles.title} wrapperClassName={cn(styles.wrapperClassName, { - [styles.mobile]: isMobile() + [styles.mobile]: isMobile })} headerContainerClassName={styles.headerContainerClassName} bodyClassName={styles.modalBody} diff --git a/packages/web/src/components/change-password/ConfirmCredentials.tsx b/packages/web/src/components/change-password/ConfirmCredentials.tsx index ffb801e94d7..057d47bfaeb 100644 --- a/packages/web/src/components/change-password/ConfirmCredentials.tsx +++ b/packages/web/src/components/change-password/ConfirmCredentials.tsx @@ -9,7 +9,7 @@ import { Button, ButtonType, IconArrow } from '@audius/stems' import cn from 'classnames' import { useDispatch } from 'react-redux' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { Spring } from 'react-spring/renderprops' +import { Spring } from 'react-spring/renderprops.cjs' import Input from 'components/data-entry/Input' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' diff --git a/packages/web/src/components/client-only/ClientOnly.tsx b/packages/web/src/components/client-only/ClientOnly.tsx new file mode 100644 index 00000000000..09b716a6d98 --- /dev/null +++ b/packages/web/src/components/client-only/ClientOnly.tsx @@ -0,0 +1,25 @@ +import { ReactNode, useEffect, useState } from 'react' + +type ClientOnlyProps = { + children: ReactNode + fallback?: ReactNode +} + +/** + * A simple wrapper that only renders its children on the client. + * This is used to disable certain parts of the app from rendering on the server. + * + * If you need to lazy load something on the client only, use vike-react/ClientOnly + */ +export const ClientOnly = (props: ClientOnlyProps) => { + const { children, fallback = null } = props + + const [isClientSide, setIsClientSide] = useState(false) + + // useEffect only runs on the client + useEffect(() => { + setIsClientSide(true) + }, []) + + return <>{isClientSide ? children : fallback} +} diff --git a/packages/web/src/components/co-sign/CoSign.tsx b/packages/web/src/components/co-sign/CoSign.tsx index d674a1b3e47..5cb6866e5ee 100644 --- a/packages/web/src/components/co-sign/CoSign.tsx +++ b/packages/web/src/components/co-sign/CoSign.tsx @@ -4,7 +4,7 @@ import { ID } from '@audius/common' import cn from 'classnames' import Tooltip from 'components/tooltip/Tooltip' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import Check from './Check' import styles from './CoSign.module.css' diff --git a/packages/web/src/components/collectibles/components/CollectibleDetailsModal.tsx b/packages/web/src/components/collectibles/components/CollectibleDetailsModal.tsx index ff8e5084bc0..e543666c315 100644 --- a/packages/web/src/components/collectibles/components/CollectibleDetailsModal.tsx +++ b/packages/web/src/components/collectibles/components/CollectibleDetailsModal.tsx @@ -41,8 +41,8 @@ import Toast from 'components/toast/Toast' import { ToastContext } from 'components/toast/ToastContext' import Tooltip from 'components/tooltip/Tooltip' import { ComponentPlacement, MountPlacement } from 'components/types' +import { useIsMobile } from 'hooks/useIsMobile' import { MIN_COLLECTIBLES_TIER } from 'pages/profile-page/ProfilePageProvider' -import { useIsMobile } from 'utils/clientUtil' import { copyToClipboard, getCopyableLink } from 'utils/clipboardUtil' import zIndex from 'utils/zIndex' diff --git a/packages/web/src/components/collectibles/components/CollectiblesPage.tsx b/packages/web/src/components/collectibles/components/CollectiblesPage.tsx index 5c656d92f6f..2fe8b41a8bb 100644 --- a/packages/web/src/components/collectibles/components/CollectiblesPage.tsx +++ b/packages/web/src/components/collectibles/components/CollectiblesPage.tsx @@ -37,6 +37,7 @@ import { } from 'react-beautiful-dnd' import { useDispatch, useSelector } from 'react-redux' +import { useHistoryContext } from 'app/HistoryProvider' import IconGradientCollectibles from 'assets/img/iconGradientCollectibles.svg' import { useModalState } from 'common/hooks/useModalState' import CollectibleDetails from 'components/collectibles/components/CollectibleDetails' @@ -132,6 +133,7 @@ const CollectiblesPage = (props: CollectiblesPageProps) => { const solanaCollectibleList = useMemo(() => { return profile?.solanaCollectibleList ?? null }, [profile]) + const { history } = useHistoryContext() const collectibleList = useMemo(() => { return ethCollectibleList || solanaCollectibleList @@ -489,7 +491,10 @@ const CollectiblesPage = (props: CollectiblesPageProps) => { // Handle rendering details modal based on route useEffect(() => { - const match = doesMatchRoute(PROFILE_PAGE_COLLECTIBLE_DETAILS) + const match = doesMatchRoute( + history.location, + PROFILE_PAGE_COLLECTIBLE_DETAILS + ) if (match) { // Ignore needed bc typescript doesn't think that match.params has collectibleId property // @ts-ignore @@ -533,7 +538,8 @@ const CollectiblesPage = (props: CollectiblesPageProps) => { isUserOnTheirProfile, updateProfilePicture, onSave, - setIsEmbedModalOpen + setIsEmbedModalOpen, + history.location ]) const overflowMenuItems: PopupMenuItem[] = [ diff --git a/packages/web/src/components/cookie-banner/CookieBanner.tsx b/packages/web/src/components/cookie-banner/CookieBanner.tsx index 094de747b36..0de1b7edb1f 100644 --- a/packages/web/src/components/cookie-banner/CookieBanner.tsx +++ b/packages/web/src/components/cookie-banner/CookieBanner.tsx @@ -6,9 +6,9 @@ import { connect } from 'react-redux' import { Dispatch } from 'redux' import IconRemove from 'assets/img/iconRemove.svg' +import { useIsMobile } from 'hooks/useIsMobile' import { dismissCookieBanner } from 'store/application/ui/cookieBanner/actions' import { AppState } from 'store/types' -import { isMobile } from 'utils/clientUtil' import { COOKIE_POLICY } from 'utils/route' import styles from './CookieBanner.module.css' @@ -24,11 +24,8 @@ const messages = { export type CookieBannerProps = ReturnType & ReturnType -export const CookieBanner = ({ - isMobile, - isPlaying, - dismiss -}: CookieBannerProps) => { +export const CookieBanner = ({ isPlaying, dismiss }: CookieBannerProps) => { + const isMobile = useIsMobile() const goToCookiePolicy = () => { const win = window.open(COOKIE_POLICY, '_blank') if (win) win.focus() @@ -56,7 +53,6 @@ export const CookieBanner = ({ function mapStateToProps(state: AppState) { return { - isMobile: isMobile(), isPlaying: !!getUid(state) } } diff --git a/packages/web/src/components/cover-photo/CoverPhoto.tsx b/packages/web/src/components/cover-photo/CoverPhoto.tsx index a0a308cccf5..7676f8a7c34 100644 --- a/packages/web/src/components/cover-photo/CoverPhoto.tsx +++ b/packages/web/src/components/cover-photo/CoverPhoto.tsx @@ -6,6 +6,7 @@ import { FileWithPreview } from 'react-dropzone' import Lottie from 'react-lottie' import loadingSpinner from 'assets/animations/loadingSpinner.json' +import { ClientOnly } from 'components/client-only/ClientOnly' import DynamicImage from 'components/dynamic-image/DynamicImage' import ImageSelectionButton from 'components/image-selection/ImageSelectionButton' import { useCoverPhoto } from 'hooks/useCoverPhoto' @@ -81,11 +82,17 @@ const CoverPhoto = ({ } const loadingElement = ( -
- -
+ +
+ +
+
) return ( diff --git a/packages/web/src/components/data-entry/Input.jsx b/packages/web/src/components/data-entry/Input.jsx index affc9376764..977013b8788 100644 --- a/packages/web/src/components/data-entry/Input.jsx +++ b/packages/web/src/components/data-entry/Input.jsx @@ -153,11 +153,7 @@ Input.propTypes = { isRequired: PropTypes.bool, onChange: PropTypes.func, onFocus: PropTypes.func, - onBlur: PropTypes.func, - inputRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ current: PropTypes.instanceOf(Element) }) - ]) + onBlur: PropTypes.func } Input.defaultProps = { diff --git a/packages/web/src/components/dog-ear/DogEar.tsx b/packages/web/src/components/dog-ear/DogEar.tsx index b2cf7b752c0..9b5fa5d011e 100644 --- a/packages/web/src/components/dog-ear/DogEar.tsx +++ b/packages/web/src/components/dog-ear/DogEar.tsx @@ -10,7 +10,7 @@ import cn from 'classnames' import Rectangle from 'assets/img/dogEarRectangle.svg' import IconHidden from 'assets/img/iconHidden.svg' import IconStar from 'assets/img/iconStar.svg' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { isMatrix } from 'utils/theme/theme' import styles from './DogEar.module.css' @@ -41,13 +41,13 @@ const getIcon = (type: DogEarType) => { export const DogEar = (props: DogEarProps) => { const { type, className } = props const isMatrixMode = isMatrix() - const isMobileMode = isMobile() + const isMobile = useIsMobile() const Icon = getIcon(type) return (
diff --git a/packages/web/src/components/download-buttons/DownloadButtons.tsx b/packages/web/src/components/download-buttons/DownloadButtons.tsx index e7d0b5ae44a..3fea5fb6dec 100644 --- a/packages/web/src/components/download-buttons/DownloadButtons.tsx +++ b/packages/web/src/components/download-buttons/DownloadButtons.tsx @@ -20,7 +20,7 @@ import { } from 'common/store/pages/signon/actions' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import Tooltip from 'components/tooltip/Tooltip' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import styles from './DownloadButtons.module.css' const { toast } = toastActions diff --git a/packages/web/src/components/feature-flag-override-modal/FeatureFlagOverrideModal.tsx b/packages/web/src/components/feature-flag-override-modal/FeatureFlagOverrideModal.tsx index caa1758c194..a4ddeebed44 100644 --- a/packages/web/src/components/feature-flag-override-modal/FeatureFlagOverrideModal.tsx +++ b/packages/web/src/components/feature-flag-override-modal/FeatureFlagOverrideModal.tsx @@ -29,10 +29,12 @@ const messages = { title: 'Feature Flag Override Settings' } -const getOverrideSetting = (flag: string) => - localStorage.getItem( +const getOverrideSetting = (flag: string) => { + if (typeof localStorage === 'undefined') return null + return localStorage.getItem( `${FEATURE_FLAG_OVERRIDE_KEY}:${flag}` ) as OverrideSetting +} const setOverrideSetting = (flag: string, val: OverrideSetting) => { const flagKey = `${FEATURE_FLAG_OVERRIDE_KEY}:${flag}` diff --git a/packages/web/src/components/header/mobile/HeaderContextProvider.tsx b/packages/web/src/components/header/mobile/HeaderContextProvider.tsx index 2fdead99954..431015c4f81 100644 --- a/packages/web/src/components/header/mobile/HeaderContextProvider.tsx +++ b/packages/web/src/components/header/mobile/HeaderContextProvider.tsx @@ -10,7 +10,7 @@ import { import { useInstanceVar } from '@audius/common' import { useHistory } from 'react-router-dom' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' type HeaderContextProps = { header: ReactNode @@ -25,8 +25,9 @@ export const HeaderContext = createContext({ export const HeaderContextProvider = memo( (props: { children: JSX.Element }) => { const [header, setHeader] = useState(null) + const isMobile = useIsMobile() - if (useIsMobile()) { + if (isMobile) { return ( -type LineupProps = LineupWithoutTile & ReturnType +type LineupProps = LineupWithoutTile /** A lineup renders a LineupProvider, injecting different tiles * depending on the client state. */ const Lineup = (props: LineupProps) => { - const mobile = props.isMobile - const trackTile = mobile ? MobileTrackTile : DesktopTrackTile - const playlistTile = mobile ? MobilePlaylistTile : DesktopPlaylistTile + const isMobile = useIsMobile() + const trackTile = isMobile ? MobileTrackTile : DesktopTrackTile + const playlistTile = isMobile ? MobilePlaylistTile : DesktopPlaylistTile return ( number) ) => Math.ceil( - (window.innerHeight / totalTileHeight[variant]) * + (innerHeight / totalTileHeight[variant]) * (typeof multiplier === 'function' ? multiplier() : multiplier) ) // Call load more when the user is LOAD_MORE_PAGE_THRESHOLD of the view height // away from the bottom of the scrolling window. const getLoadMoreThreshold = () => - Math.ceil(window.innerHeight * LOAD_MORE_PAGE_THRESHOLD) + Math.ceil(innerHeight * LOAD_MORE_PAGE_THRESHOLD) const shouldLoadMore = ( scrollContainer: HTMLDivElement | null, @@ -230,6 +232,8 @@ type CombinedProps = LineupProviderProps & * is controlled by injecting tiles conforming to `Track/Playlist/SkeletonProps interfaces. */ class LineupProvider extends PureComponent { + static contextType = SsrContext + declare context: React.ContextType scrollContainer = createRef() constructor(props: any) { @@ -476,7 +480,6 @@ class LineupProvider extends PureComponent { extraPrecedingElement, endOfLineup, lineupContainerStyles, - isMobile, showLeadingElementArtistPick = true, lineup: { isMetadataLoading, page, entries = [] }, numPlaylistSkeletonRows, @@ -484,6 +487,7 @@ class LineupProvider extends PureComponent { showFeedTipTile = false, rankIconCount = 0 } = this.props + const isMobile = this.context.isMobile const status = lineup.status const { loadMoreThreshold, @@ -791,7 +795,6 @@ class LineupProvider extends PureComponent { function mapStateToProps(state: AppState) { return { - isMobile: isMobile(), showTip: getShowTip(state), playing: getPlaying(state), playingUid: getUid(state) diff --git a/packages/web/src/components/lineup/hooks.ts b/packages/web/src/components/lineup/hooks.ts index 7d7402c215a..66e48fe0f1b 100644 --- a/packages/web/src/components/lineup/hooks.ts +++ b/packages/web/src/components/lineup/hooks.ts @@ -9,8 +9,8 @@ import { import { useDispatch } from 'react-redux' import { LineupVariant } from 'components/lineup/types' +import { useIsMobile } from 'hooks/useIsMobile' import { AppState } from 'store/types' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' const { makeGetCurrent } = queueSelectors @@ -46,6 +46,7 @@ export const useLineupProps = ({ isOrdered }: useLineupPropsProps) => { const dispatch = useDispatch() + const isMobile = useIsMobile() // Create memoized selectors const getLineup = useMemo( @@ -83,7 +84,7 @@ export const useLineupProps = ({ loadMore, numPlaylistSkeletonRows, scrollParent, - isMobile: isMobile(), + isMobile, rankIconCount, isTrending, ordered: isOrdered diff --git a/packages/web/src/components/link/UserLink.tsx b/packages/web/src/components/link/UserLink.tsx index c81de623ea5..7ed2528826d 100644 --- a/packages/web/src/components/link/UserLink.tsx +++ b/packages/web/src/components/link/UserLink.tsx @@ -4,6 +4,7 @@ import cn from 'classnames' import { ArtistPopover } from 'components/artist/ArtistPopover' import { Text } from 'components/typography' import UserBadges from 'components/user-badges/UserBadges' +import { useSsrContext } from 'ssr/SsrContext' import { useSelector } from 'utils/reducer' import { profilePage } from 'utils/route' @@ -20,6 +21,7 @@ type UserLinkProps = Omit & { } export const UserLink = (props: UserLinkProps) => { + const { isServerSide } = useSsrContext() const { userId, textAs = 'span', @@ -50,7 +52,7 @@ export const UserLink = (props: UserLinkProps) => { ) - return popover && handle ? ( + return !isServerSide && popover && handle ? ( {linkElement} ) : ( linkElement diff --git a/packages/web/src/components/locked-content-modal/LockedContentModal.tsx b/packages/web/src/components/locked-content-modal/LockedContentModal.tsx index 0d3c08892e0..d74a31530f4 100644 --- a/packages/web/src/components/locked-content-modal/LockedContentModal.tsx +++ b/packages/web/src/components/locked-content-modal/LockedContentModal.tsx @@ -12,8 +12,8 @@ import { useDispatch } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import { GatedTrackSection } from 'components/track/GatedTrackSection' import { LockedTrackDetailsTile } from 'components/track/LockedTrackDetailsTile' +import { useIsMobile } from 'hooks/useIsMobile' import ModalDrawer from 'pages/audio-rewards-page/components/modals/ModalDrawer' -import { isMobile } from 'utils/clientUtil' import styles from './LockedContentModal.module.css' @@ -34,7 +34,7 @@ export const LockedContentModal = () => { dispatch(resetLockedContentId()) }, [setIsOpen, dispatch]) - const mobile = isMobile() + const isMobile = useIsMobile() return ( { useGradientTitle={false} > { + const { history } = useHistoryContext() const { getScrollForRoute, setScrollForRoute } = useContext(ScrollContext)! - const [getInitialPathname] = useInstanceVar(getPathname()) + const [getInitialPathname] = useInstanceVar(getPathname(history.location)) const [getLastScroll, setLastScroll] = useInstanceVar(0) // On mount, restore the last scroll position @@ -83,7 +85,7 @@ const MobilePageContainer = ({ useEffect(() => { // Store Y scroll in instance var as we scroll const onScroll = () => { - const path = getPathname() + const path = getPathname(history.location) // We can stay mounted after switching // paths, so check for this case if (path === getInitialPathname()) { @@ -98,7 +100,13 @@ const MobilePageContainer = ({ setScrollForRoute(getInitialPathname(), getLastScroll()) window.removeEventListener('scroll', onScroll) } - }, [setLastScroll, getInitialPathname, setScrollForRoute, getLastScroll]) + }, [ + setLastScroll, + getInitialPathname, + setScrollForRoute, + getLastScroll, + history + ]) const paddingBottom = `${ BOTTOM_BAR_HEIGHT + diff --git a/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx b/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx index 93d690de274..151fa93fae6 100644 --- a/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx +++ b/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx @@ -9,7 +9,7 @@ import { import { useDispatch } from 'react-redux' import { MusicConfetti } from 'components/background-animations/MusicConfetti' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { useSelector } from 'utils/reducer' import zIndex from 'utils/zIndex' diff --git a/packages/web/src/components/nav/Navigator.tsx b/packages/web/src/components/nav/Navigator.tsx index e153cb314fe..1c6bfd6e607 100644 --- a/packages/web/src/components/nav/Navigator.tsx +++ b/packages/web/src/components/nav/Navigator.tsx @@ -2,6 +2,7 @@ import { Client } from '@audius/common' import cn from 'classnames' import { RouteComponentProps, withRouter } from 'react-router-dom' +import { useIsMobile } from 'hooks/useIsMobile' import { getClient } from 'utils/clientUtil' import styles from './Navigator.module.css' @@ -18,8 +19,8 @@ type NavigatorProps = OwnProps & RouteComponentProps // and LeftNav for desktop const Navigator = ({ className }: NavigatorProps) => { const client = getClient() + const isMobile = useIsMobile() - const isMobile = client === Client.MOBILE const isElectron = client === Client.ELECTRON return ( diff --git a/packages/web/src/components/nav/desktop/LeftNav.tsx b/packages/web/src/components/nav/desktop/LeftNav.tsx index 03816282399..85c3105df5f 100644 --- a/packages/web/src/components/nav/desktop/LeftNav.tsx +++ b/packages/web/src/components/nav/desktop/LeftNav.tsx @@ -19,6 +19,7 @@ import { Dispatch } from 'redux' import { make, useRecord } from 'common/store/analytics/actions' import * as signOnActions from 'common/store/pages/signon/actions' +import { ClientOnly } from 'components/client-only/ClientOnly' import { DragAutoscroller } from 'components/drag-autoscroller/DragAutoscroller' import ConnectedProfileCompletionPane from 'components/profile-progress/ConnectedProfileCompletionPane' import { selectDraggingKind } from 'store/dragndrop/slice' @@ -124,76 +125,82 @@ const LeftNav = (props: NavColumnProps) => { ) } diff --git a/packages/web/src/components/nav/mobile/TopLevelPage.tsx b/packages/web/src/components/nav/mobile/TopLevelPage.tsx index 8dbe7971399..ff8565228cd 100644 --- a/packages/web/src/components/nav/mobile/TopLevelPage.tsx +++ b/packages/web/src/components/nav/mobile/TopLevelPage.tsx @@ -14,7 +14,8 @@ const { getModalVisibility } = modalsSelectors type TopLevelPageProps = ReturnType & ReturnType -const rootElement = document.querySelector('#root') +const rootElement = + typeof document !== 'undefined' ? document.querySelector('#root') : null const TopLevelPage = ({ showAddToCollection }: TopLevelPageProps) => { const { isOpen } = useEditPlaylistModal() diff --git a/packages/web/src/components/nav/store/context.tsx b/packages/web/src/components/nav/store/context.tsx index e64bd352d61..19a073d1175 100644 --- a/packages/web/src/components/nav/store/context.tsx +++ b/packages/web/src/components/nav/store/context.tsx @@ -9,7 +9,7 @@ import { useCallback } from 'react' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' type NavContextProps = { setLeft: (el: LeftElement) => void @@ -65,7 +65,8 @@ export const useNavContext = () => { export const NavProvider = memo((props: { children: JSX.Element }) => { const contextValue = useNavContext() - if (useIsMobile()) { + const isMobile = useIsMobile() + if (isMobile) { return ( {props.children} diff --git a/packages/web/src/components/notification/Notification/FavoriteNotification.tsx b/packages/web/src/components/notification/Notification/FavoriteNotification.tsx index d79a327066e..4bfc66ae334 100644 --- a/packages/web/src/components/notification/Notification/FavoriteNotification.tsx +++ b/packages/web/src/components/notification/Notification/FavoriteNotification.tsx @@ -8,12 +8,12 @@ import { import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { useIsMobile } from 'hooks/useIsMobile' import { setUsers as setUserListUsers, setVisibility as openUserListModal } from 'store/application/ui/userListModal/slice' import { UserListType } from 'store/application/ui/userListModal/types' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' import { EntityLink, useGoToEntity } from './components/EntityLink' @@ -55,6 +55,7 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { : entityType const dispatch = useDispatch() + const isMobile = useIsMobile() const handleGoToEntity = useGoToEntity(entity, entityType) @@ -68,7 +69,7 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { id: id as unknown as number }) ) - if (isMobile()) { + if (isMobile) { dispatch(push(`notification/${id}/users`)) } else { dispatch(openUserListModal(true)) @@ -77,7 +78,7 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { handleGoToEntity(event) } }, - [isMultiUser, dispatch, entityType, id, handleGoToEntity] + [isMultiUser, dispatch, entityType, id, handleGoToEntity, isMobile] ) if (!users || !firstUser || !entity) return null diff --git a/packages/web/src/components/notification/Notification/FollowNotification.tsx b/packages/web/src/components/notification/Notification/FollowNotification.tsx index 6b6f10307f6..e04e28bcc08 100644 --- a/packages/web/src/components/notification/Notification/FollowNotification.tsx +++ b/packages/web/src/components/notification/Notification/FollowNotification.tsx @@ -7,6 +7,7 @@ import { import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { useIsMobile } from 'hooks/useIsMobile' import { setUsers as setUserListUsers, setVisibility as openUserListModal @@ -15,7 +16,6 @@ import { UserListEntityType, UserListType } from 'store/application/ui/userListModal/types' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' import { profilePage } from 'utils/route' @@ -49,6 +49,7 @@ export const FollowNotification = (props: FollowNotificationProps) => { const otherUsersCount = userIds.length - 1 const isMultiUser = userIds.length > 1 const dispatch = useDispatch() + const isMobile = useIsMobile() const handleClick = useCallback(() => { if (isMultiUser) { @@ -59,7 +60,7 @@ export const FollowNotification = (props: FollowNotificationProps) => { id: id as unknown as number }) ) - if (isMobile()) { + if (isMobile) { dispatch(push(`notification/${id}/users`)) } else { dispatch(openUserListModal(true)) @@ -69,7 +70,7 @@ export const FollowNotification = (props: FollowNotificationProps) => { dispatch(push(profilePage(firstUser.handle))) } } - }, [isMultiUser, dispatch, id, firstUser]) + }, [isMultiUser, dispatch, id, firstUser, isMobile]) if (!users || !firstUser) return null diff --git a/packages/web/src/components/notification/Notification/RepostNotification.tsx b/packages/web/src/components/notification/Notification/RepostNotification.tsx index 4f66af162eb..9643135530c 100644 --- a/packages/web/src/components/notification/Notification/RepostNotification.tsx +++ b/packages/web/src/components/notification/Notification/RepostNotification.tsx @@ -8,12 +8,12 @@ import { import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { useIsMobile } from 'hooks/useIsMobile' import { setUsers as setUserListUsers, setVisibility as openUserListModal } from 'store/application/ui/userListModal/slice' import { UserListType } from 'store/application/ui/userListModal/types' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' import { EntityLink, useGoToEntity } from './components/EntityLink' @@ -56,6 +56,7 @@ export const RepostNotification = (props: RepostNotificationProps) => { : entityType const dispatch = useDispatch() + const isMobile = useIsMobile() const handleGoToEntity = useGoToEntity(entity, entityType) @@ -69,7 +70,7 @@ export const RepostNotification = (props: RepostNotificationProps) => { id: id as unknown as number }) ) - if (isMobile()) { + if (isMobile) { dispatch(push(`notification/${id}/users`)) } else { dispatch(openUserListModal(true)) @@ -78,7 +79,7 @@ export const RepostNotification = (props: RepostNotificationProps) => { handleGoToEntity(event) } }, - [isMultiUser, dispatch, entityType, id, handleGoToEntity] + [isMultiUser, dispatch, entityType, id, handleGoToEntity, isMobile] ) if (!users || !firstUser || !entity) return null diff --git a/packages/web/src/components/notification/Notification/components/UserNameLink.tsx b/packages/web/src/components/notification/Notification/components/UserNameLink.tsx index 435f4a289cc..f76ce3dd009 100644 --- a/packages/web/src/components/notification/Notification/components/UserNameLink.tsx +++ b/packages/web/src/components/notification/Notification/components/UserNameLink.tsx @@ -8,8 +8,8 @@ import { useDispatch } from 'react-redux' import { make, useRecord } from 'common/store/analytics/actions' import { ArtistPopover } from 'components/artist/ArtistPopover' import UserBadges from 'components/user-badges/UserBadges' +import { useIsMobile } from 'hooks/useIsMobile' import { closeNotificationPanel } from 'store/application/ui/notifications/notificationsUISlice' -import { isMobile } from 'utils/clientUtil' import { profilePage } from 'utils/route' import styles from './UserNameLink.module.css' @@ -27,6 +27,7 @@ type UserNameLinkProps = { export const UserNameLink = (props: UserNameLinkProps) => { const { className, notification, user } = props const dispatch = useDispatch() + const isMobile = useIsMobile() const record = useRecord() const { type } = notification @@ -79,7 +80,7 @@ export const UserNameLink = (props: UserNameLinkProps) => { ) - if (!isMobile()) { + if (!isMobile) { userNameElement = ( { onClose() if (track) { - goToRoute(track.permalink) + goToRoute(history.location, track.permalink) } else { - goToRoute(collectibleDetailsPage(user.handle, collectible?.id ?? '')) + goToRoute( + history.location, + collectibleDetailsPage(user.handle, collectible?.id ?? '') + ) } } const goToProfilePage = () => { onClose() - goToRoute(profilePage(handle)) + goToRoute(history.location, profilePage(handle)) } const onClickOverflow = useCallback(() => { @@ -656,7 +662,8 @@ function mapDispatchToProps(dispatch: Dispatch) { overflowActionCallbacks: callbacks }) ), - goToRoute: (route: string) => dispatch(pushRoute(route)) + goToRoute: (location: Location, route: string) => + dispatch(pushRoute(location, route)) } } diff --git a/packages/web/src/components/now-playing/NowPlayingDrawer.tsx b/packages/web/src/components/now-playing/NowPlayingDrawer.tsx index 15e02ed3b5a..fc609156a5c 100644 --- a/packages/web/src/components/now-playing/NowPlayingDrawer.tsx +++ b/packages/web/src/components/now-playing/NowPlayingDrawer.tsx @@ -14,7 +14,7 @@ import NowPlaying from './NowPlaying' import styles from './NowPlayingDrawer.module.css' const { setIsOpen: _setIsNowPlayingOpen } = nowPlayingUIActions -const DEFAULT_HEIGHT = window.innerHeight +const DEFAULT_HEIGHT = typeof window !== 'undefined' ? window.innerHeight : 0 // Translation values for a totally hidden drawer const DRAWER_HIDDEN_TRANSLATION = -48 diff --git a/packages/web/src/components/page/Page.jsx b/packages/web/src/components/page/Page.jsx deleted file mode 100644 index e3fee3e9a50..00000000000 --- a/packages/web/src/components/page/Page.jsx +++ /dev/null @@ -1,205 +0,0 @@ -import { cloneElement, useRef, useState, useEffect, useCallback } from 'react' - -import cn from 'classnames' -import PropTypes from 'prop-types' -import { Helmet } from 'react-helmet' -// eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { Spring } from 'react-spring/renderprops' -import calcScrollbarWidth from 'scrollbar-width' - -import SearchBar from 'components/search-bar/ConnectedSearchBar' - -import styles from './Page.module.css' - -const messages = { - dotAudius: '• Audius', - audius: 'Audius' -} - -const HEADER_MARGIN_PX = 32 -// Pixels on the right side of the header to account for potential scrollbars -const MIN_GUTTER_WIDTH = 20 - -// Responsible for positioning the header -const HeaderContainer = ({ header, containerRef, showSearch }) => { - // Need to offset the header on the right side - // the width of the scrollbar. - const [scrollBarWidth, setScrollbarWidth] = useState(0) - - const refreshScrollWidth = useCallback(() => { - const width = calcScrollbarWidth(true) - // For some odd reason, narrow windows ONLY in Firefox - // return 0 width for the scroll bars. - setScrollbarWidth(width > 0 ? width : MIN_GUTTER_WIDTH) - }, []) - - useEffect(() => { - refreshScrollWidth() - }, [refreshScrollWidth]) - - // Only Safari & Chrome support the CSS - // frosted glasss effect. - const [isChromeOrSafari, setIsChromeOrSafari] = useState(false) - useEffect(() => { - const chromeOrSafari = () => { - const userAgent = navigator.userAgent.toLowerCase() - return ( - userAgent.indexOf('chrome') > -1 || userAgent.indexOf('safari') > -1 - ) - } - setIsChromeOrSafari(chromeOrSafari) - }, []) - - const headerContainerRef = useRef() - - return ( -
-
- {cloneElement(header, { - isChromeOrSafari, - scrollBarWidth, - headerContainerRef, - topLeftElement: showSearch ? : null - })} -
- {/* We attach the box shadow as a separate element to - avoid overlapping the scroll bar. - */} -
-
- ) -} - -export const Page = (props) => { - const [headerHeight, setHeaderHeight] = useState(0) - - const calculateHeaderHeight = (element) => { - if (element) { - setHeaderHeight(element.offsetHeight) - } - } - - return ( - - {(animProps) => ( -
- - {props.title ? ( - {`${props.title} ${messages.dotAudius}`} - ) : ( - {messages.audius} - )} - {props.description ? ( - - ) : null} - {/* TODO: re-enable once we fix redirects and casing of canonicalUrls */} - {/* {props.canonicalUrl && ( - - )} */} - {props.structuredData && ( - - )} - - {props.header && ( - - )} -
- {/* Set an id so that nested components can mount in relation to page if needed, e.g. fixed menu popups. */} -
- {props.children} -
-
- {props.scrollableSearch && ( -
- -
- )} -
- )} -
- ) -} - -Page.propTypes = { - title: PropTypes.string, - description: PropTypes.string, - canonicalUrl: PropTypes.string, - structuredData: PropTypes.object, - variant: PropTypes.oneOf(['inset', 'flush']), - size: PropTypes.oneOf(['medium', 'large']), - containerRef: PropTypes.node, - contentClassName: PropTypes.string, - containerClassName: PropTypes.string, - fadeDuration: PropTypes.number, - header: PropTypes.node, - - // There are some pages which don't have a fixed header but still display - // a search bar that scrolls with the page. - scrollableSearch: PropTypes.bool, - children: PropTypes.node, - showSearch: PropTypes.bool -} - -Page.defaultProps = { - variant: 'inset', - size: 'medium', - fadeDuration: 200, - scrollableSearch: false, - showSearch: true -} - -export default Page diff --git a/packages/web/src/components/page/Page.tsx b/packages/web/src/components/page/Page.tsx new file mode 100644 index 00000000000..54d69689df9 --- /dev/null +++ b/packages/web/src/components/page/Page.tsx @@ -0,0 +1,272 @@ +import { + ReactNode, + cloneElement, + useRef, + useState, + useEffect, + useCallback, + MutableRefObject +} from 'react' + +import { Nullable } from '@audius/common' +import cn from 'classnames' +import { Helmet } from 'react-helmet' +// eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web +import { Spring } from 'react-spring/renderprops.cjs' +// @ts-ignore +import calcScrollbarWidth from 'scrollbar-width' + +import { ClientOnly } from 'components/client-only/ClientOnly' +import SearchBar from 'components/search-bar/ConnectedSearchBar' + +import styles from './Page.module.css' + +const messages = { + dotAudius: '• Audius', + audius: 'Audius' +} + +const HEADER_MARGIN_PX = 32 +// Pixels on the right side of the header to account for potential scrollbars +const MIN_GUTTER_WIDTH = 20 + +// Responsible for positioning the header +type HeaderContainerProps = Pick & { + containerRef: (element: Nullable) => void +} + +const HeaderContainer = (props: HeaderContainerProps) => { + const { header, containerRef, showSearch } = props + + // Need to offset the header on the right side + // the width of the scrollbar. + const [scrollBarWidth, setScrollbarWidth] = useState(0) + + const refreshScrollWidth = useCallback(() => { + const width = calcScrollbarWidth(true) + // For some odd reason, narrow windows ONLY in Firefox + // return 0 width for the scroll bars. + setScrollbarWidth(width > 0 ? width : MIN_GUTTER_WIDTH) + }, []) + + useEffect(() => { + refreshScrollWidth() + }, [refreshScrollWidth]) + + // Only Safari & Chrome support the CSS + // frosted glasss effect. + const [isChromeOrSafari, setIsChromeOrSafari] = useState(false) + useEffect(() => { + const chromeOrSafari = () => { + const userAgent = navigator.userAgent.toLowerCase() + return ( + userAgent.indexOf('chrome') > -1 || userAgent.indexOf('safari') > -1 + ) + } + setIsChromeOrSafari(chromeOrSafari) + }, []) + + const headerContainerRef = useRef(null) + + return ( +
+
+ {cloneElement(header as any, { + isChromeOrSafari, + scrollBarWidth, + headerContainerRef, + topLeftElement: showSearch ? : null + })} +
+ {/* We attach the box shadow as a separate element to + avoid overlapping the scroll bar. + */} +
+
+ ) +} + +type PageProps = { + title?: string + description?: string + ogDescription?: string + image?: string + canonicalUrl?: string + structuredData?: object + variant?: 'insert' | 'flush' + size?: 'medium' | 'large' + containerRef?: MutableRefObject + className?: string + contentClassName?: string + containerClassName?: string + fromOpacity?: number + fadeDuration?: number + header?: ReactNode + + // There are some pages which don't have a fixed header but still display + // a search bar that scrolls with the page. + scrollableSearch?: boolean + children?: ReactNode + showSearch?: boolean +} + +export const Page = (props: PageProps) => { + const { + title, + description, + ogDescription, + image, + canonicalUrl, + structuredData, + variant = 'inset', + size = 'medium', + containerRef, + contentClassName, + containerClassName, + fromOpacity = 0.2, + fadeDuration = 200, + header, + scrollableSearch = false, + children, + showSearch = true + } = props + + const [headerHeight, setHeaderHeight] = useState(0) + + const calculateHeaderHeight = (element: Nullable) => { + if (element) { + setHeaderHeight(element.offsetHeight) + } + } + + const formattedTitle = title + ? `${title} ${messages.dotAudius}` + : messages.audius + + return ( + <> + {/* Title */} + + {formattedTitle} + + + + + {/* Description */} + {description ? ( + + + + ) : null} + + {/* OG Description - This is the actual description of the content, for example a Track description */} + {ogDescription ? ( + + + + + ) : null} + + {/* Canonical URL */} + {canonicalUrl ? ( + + + + + ) : null} + + {/* Image */} + {image ? ( + + + + + ) : null} + + + + + {structuredData && ( + + )} + + {(animProps) => ( +
+ {header && ( + + )} +
+ {/* Set an id so that nested components can mount in relation to page if needed, e.g. fixed menu popups. */} +
+ {children} +
+
+ + {scrollableSearch && ( +
+ +
+ )} +
+
+ )} +
+ + ) +} + +export default Page diff --git a/packages/web/src/components/payment-method/PaymentMethod.tsx b/packages/web/src/components/payment-method/PaymentMethod.tsx index 3714292d69a..62073bc4ee9 100644 --- a/packages/web/src/components/payment-method/PaymentMethod.tsx +++ b/packages/web/src/components/payment-method/PaymentMethod.tsx @@ -21,7 +21,7 @@ import BN from 'bn.js' import { MobileFilterButton } from 'components/mobile-filter-button/MobileFilterButton' import { SummaryTable, SummaryTableItem } from 'components/summary-table' import { Text } from 'components/typography' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import zIndex from 'utils/zIndex' const messages = { @@ -52,7 +52,7 @@ export const PaymentMethod = ({ showExistingBalance, isCoinflowEnabled }: PaymentMethodProps) => { - const mobile = isMobile() + const isMobile = useIsMobile() const balanceCents = formatUSDCWeiToFloorCentsNumber( (balance ?? new BN(0)) as BNUSDC ) @@ -97,7 +97,7 @@ export const PaymentMethod = ({ icon: IconCreditCard, value: vendorOptions.length > 1 ? ( - mobile ? ( + isMobile ? ( { const getFlexProps = (id: PurchaseMethod) => { - if (mobile && id === PurchaseMethod.CARD) { + if (isMobile && id === PurchaseMethod.CARD) { return { direction: 'column' as CSSProperties['flexDirection'], justifyContent: 'center', @@ -179,7 +179,7 @@ export const PaymentMethod = ({ {value} diff --git a/packages/web/src/components/play-bar/PlayBarProvider.tsx b/packages/web/src/components/play-bar/PlayBarProvider.tsx index 930849afb5b..91bcbd8d172 100644 --- a/packages/web/src/components/play-bar/PlayBarProvider.tsx +++ b/packages/web/src/components/play-bar/PlayBarProvider.tsx @@ -3,9 +3,10 @@ import cn from 'classnames' import { connect } from 'react-redux' import { RouteComponentProps, withRouter } from 'react-router-dom' +import { ClientOnly } from 'components/client-only/ClientOnly' import NowPlayingDrawer from 'components/now-playing/NowPlayingDrawer' +import { useIsMobile } from 'hooks/useIsMobile' import { AppState } from 'store/types' -import { isMobile } from 'utils/clientUtil' import styles from './PlayBarProvider.module.css' import DesktopPlayBar from './desktop/PlayBar' @@ -21,11 +22,11 @@ type PlayBarProviderProps = OwnProps & RouteComponentProps const PlayBarProvider = ({ - isMobile, playingUid, collectible, addToCollectionOpen }: PlayBarProviderProps) => { + const isMobile = useIsMobile() return (
{isMobile ? ( - + + + ) : ( <>
@@ -51,7 +54,6 @@ function mapStateToProps(state: AppState) { return { playingUid: getPlayingUid(state), collectible: getCollectible(state), - isMobile: isMobile(), addToCollectionOpen: getModalVisibility(state, 'AddToCollection') } } diff --git a/packages/web/src/components/play-bar/VolumeBar.jsx b/packages/web/src/components/play-bar/VolumeBar.jsx index 81185c580e6..d59ea63b5da 100644 --- a/packages/web/src/components/play-bar/VolumeBar.jsx +++ b/packages/web/src/components/play-bar/VolumeBar.jsx @@ -19,6 +19,7 @@ const getVolumeIcon = (volumeLevel) => { } const getSavedVolume = (defaultVolume) => { + if (typeof window === 'undefined') return defaultVolume const localStorageVolume = window.localStorage.getItem('volume') if (localStorageVolume === null) { window.localStorage.setItem('volume', defaultVolume) diff --git a/packages/web/src/components/play-bar/desktop/PlayBar.jsx b/packages/web/src/components/play-bar/desktop/PlayBar.jsx index 924cf5a5cc3..d2e663d0d5b 100644 --- a/packages/web/src/components/play-bar/desktop/PlayBar.jsx +++ b/packages/web/src/components/play-bar/desktop/PlayBar.jsx @@ -25,6 +25,7 @@ import { push as pushRoute } from 'connected-react-router' import { connect } from 'react-redux' import { make } from 'common/store/analytics/actions' +import { ClientOnly } from 'components/client-only/ClientOnly' import PlayButton from 'components/play-bar/PlayButton' import VolumeBar from 'components/play-bar/VolumeBar' import NextButtonProvider from 'components/play-bar/next-button/NextButtonProvider' @@ -34,6 +35,7 @@ import ShuffleButtonProvider from 'components/play-bar/shuffle-button/ShuffleBut import { audioPlayer } from 'services/audio-player' import { getFeatureEnabled } from 'services/remote-config/featureFlagHelpers' import { getLineupSelectorForRoute } from 'store/lineup/lineupForRoute' +import { getLocation } from 'store/routing/selectors' import { setupHotkeys } from 'utils/hotkeyUtil' import { collectibleDetailsPage, profilePage } from 'utils/route' import { isMatrix, shouldShowDark } from 'utils/theme/theme' @@ -357,99 +359,101 @@ class PlayBar extends Component { return (
-
- -
- -
-
- +
+
-
-
- {isLongFormContent && isNewPodcastControlsEnabled ? null : ( - - )} -
-
- -
-
- +
+
-
- -
-
- {isLongFormContent && isNewPodcastControlsEnabled ? ( - - ) : ( - +
+ {isLongFormContent && isNewPodcastControlsEnabled ? null : ( + + )} +
+
+ +
+
+ - )} +
+
+ +
+
+ {isLongFormContent && isNewPodcastControlsEnabled ? ( + + ) : ( + + )} +
-
- -
- - -
+ +
+ + +
+
) @@ -460,8 +464,9 @@ const makeMapStateToProps = () => { const getCurrentQueueItem = makeGetCurrent() const mapStateToProps = (state) => { + const location = getLocation(state) const lineupEntries = - getLineupEntries(getLineupSelectorForRoute(state), state) ?? [] + getLineupEntries(getLineupSelectorForRoute(location), state) ?? [] // The lineup has accessible tracks when there is at least one track // the user has access to i.e. a non-gated track or an unlocked gated track. diff --git a/packages/web/src/components/play-bar/mobile/PlayBar.tsx b/packages/web/src/components/play-bar/mobile/PlayBar.tsx index d97f7cfd6a5..f839c9b90ad 100644 --- a/packages/web/src/components/play-bar/mobile/PlayBar.tsx +++ b/packages/web/src/components/play-bar/mobile/PlayBar.tsx @@ -66,6 +66,9 @@ const PlayBar = ({ useEffect(() => { const seekInterval = setInterval(async () => { + if (!audioPlayer) { + return + } const duration = await audioPlayer.getDuration() const pos = await audioPlayer.getPosition() if (duration === undefined || pos === undefined) return diff --git a/packages/web/src/components/play-bar/shuffle-button/ShuffleButtonProvider.tsx b/packages/web/src/components/play-bar/shuffle-button/ShuffleButtonProvider.tsx index 999489c3dc2..a2ef89dff23 100644 --- a/packages/web/src/components/play-bar/shuffle-button/ShuffleButtonProvider.tsx +++ b/packages/web/src/components/play-bar/shuffle-button/ShuffleButtonProvider.tsx @@ -45,8 +45,12 @@ const ShuffleButtonProvider = ({ setAnimations({ ...matrixAnimations.current }) } else if (darkMode) { if (!darkAnimations.current) { - const pbIconShuffleOff = require('../../../assets/animations/pbIconShuffleOffDark.json') - const pbIconShuffleOn = require('../../../assets/animations/pbIconShuffleOnDark.json') + const pbIconShuffleOff = (await import( + '../../../assets/animations/pbIconShuffleOffDark.json' + )) as any + const pbIconShuffleOn = (await import( + '../../../assets/animations/pbIconShuffleOnDark.json' + )) as any darkAnimations.current = { pbIconShuffleOff, pbIconShuffleOn diff --git a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx index f30eb2dc476..92590a448f6 100644 --- a/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx +++ b/packages/web/src/components/premium-content-purchase-modal/PremiumContentPurchaseModal.tsx @@ -31,13 +31,14 @@ import { Formik, useFormikContext } from 'formik' import { useDispatch, useSelector } from 'react-redux' import { toFormikValidationSchema } from 'zod-formik-adapter' +import { useHistoryContext } from 'app/HistoryProvider' import { Icon } from 'components/Icon' import { ModalForm } from 'components/modal-form/ModalForm' import { LockedTrackDetailsTile } from 'components/track/LockedTrackDetailsTile' import { Text } from 'components/typography' import { USDCManualTransfer } from 'components/usdc-manual-transfer/USDCManualTransfer' +import { useIsMobile } from 'hooks/useIsMobile' import ModalDrawer from 'pages/audio-rewards-page/components/modals/ModalDrawer' -import { isMobile } from 'utils/clientUtil' import { pushUniqueRoute } from 'utils/route' import zIndex from 'utils/zIndex' @@ -77,6 +78,7 @@ const RenderForm = ({ track: PurchaseableTrackMetadata }) => { const dispatch = useDispatch() + const isMobile = useIsMobile() const { permalink, stream_conditions: { @@ -88,6 +90,7 @@ const RenderForm = ({ const currentPageIndex = pageToPageIndex(page) const { resetForm } = useFormikContext() + const { history } = useHistoryContext() // Reset form on track change useEffect(() => resetForm, [track.track_id, resetForm]) @@ -95,22 +98,20 @@ const RenderForm = ({ // Navigate to track on successful purchase behind the modal useEffect(() => { if (stage === PurchaseContentStage.FINISH && permalink) { - dispatch(pushUniqueRoute(permalink)) + dispatch(pushUniqueRoute(history.location, permalink)) } - }, [stage, permalink, dispatch]) + }, [stage, permalink, dispatch, history]) const handleClose = useCallback(() => { dispatch(setPurchasePage({ page: PurchaseContentPage.PURCHASE })) }, [dispatch]) - const mobile = isMobile() - return ( - + ) : null} - + { const dispatch = useDispatch() + const isMobile = useIsMobile() const { isOpen, onClose, @@ -224,7 +226,6 @@ export const PremiumContentPurchaseModal = () => { if (track && !isValidTrack) { console.error('PremiumContentPurchaseModal: Track is not purchasable') } - const mobile = isMobile() return ( { useGradientTitle={false} dismissOnClickOutside zIndex={zIndex.PREMIUM_CONTENT_PURCHASE_MODAL} - wrapperClassName={mobile ? styles.mobileWrapper : undefined} + wrapperClassName={isMobile ? styles.mobileWrapper : undefined} > {isValidTrack && isCoinflowEnabledLoaded ? ( `Earn ${amount} $AUDIO when you buy this track!` diff --git a/packages/web/src/components/scroll-provider/ScrollProvider.tsx b/packages/web/src/components/scroll-provider/ScrollProvider.tsx index ac6e3071ea3..87887df1606 100644 --- a/packages/web/src/components/scroll-provider/ScrollProvider.tsx +++ b/packages/web/src/components/scroll-provider/ScrollProvider.tsx @@ -3,7 +3,7 @@ import { memo, createContext, useCallback } from 'react' import { useInstanceVar } from '@audius/common' import { matchPath } from 'react-router-dom' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { TRACK_PAGE, NOTIFICATION_PAGE } from 'utils/route' type ScrollRecords = { [route: string]: number } diff --git a/packages/web/src/components/search-bar/ConnectedSearchBar.jsx b/packages/web/src/components/search-bar/ConnectedSearchBar.jsx index fac38162a42..690bac79e5f 100644 --- a/packages/web/src/components/search-bar/ConnectedSearchBar.jsx +++ b/packages/web/src/components/search-bar/ConnectedSearchBar.jsx @@ -13,6 +13,7 @@ import { connect } from 'react-redux' import { matchPath } from 'react-router' import { withRouter } from 'react-router-dom' +import { HistoryContext } from 'app/HistoryProvider' import { make } from 'common/store/analytics/actions' import { fetchSearch, @@ -26,6 +27,7 @@ import { collectionPage, profilePage, getPathname } from 'utils/route' import styles from './ConnectedSearchBar.module.css' class ConnectedSearchBar extends Component { + static contextType = HistoryContext state = { value: '' } @@ -35,7 +37,7 @@ class ConnectedSearchBar extends Component { // Clear search when navigating away from the search results page. history.listen((location, action) => { - const match = matchPath(getPathname(), { + const match = matchPath(getPathname(this.context.history.location), { path: '/search/:query' }) if (!match) { @@ -44,7 +46,7 @@ class ConnectedSearchBar extends Component { }) // Set the initial search bar value if we loaded into a search page. - const match = matchPath(getPathname(), { + const match = matchPath(getPathname(this.context.history.location), { path: '/search/:query' }) if (has(match, 'params.query')) { diff --git a/packages/web/src/components/search/SearchBar.jsx b/packages/web/src/components/search/SearchBar.jsx index c4bb355d279..5735b022dfa 100644 --- a/packages/web/src/components/search/SearchBar.jsx +++ b/packages/web/src/components/search/SearchBar.jsx @@ -8,7 +8,7 @@ import { isEqual } from 'lodash' import PropTypes from 'prop-types' import Lottie from 'react-lottie' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { Transition } from 'react-spring/renderprops' +import { Transition } from 'react-spring/renderprops.cjs' import loadingSpinner from 'assets/animations/loadingSpinner.json' import IconArrow from 'assets/img/iconArrowGrey.svg' diff --git a/packages/web/src/components/selectable-pill/SelectablePill.tsx b/packages/web/src/components/selectable-pill/SelectablePill.tsx index 2aa4e1fccf1..f95fb8951cf 100644 --- a/packages/web/src/components/selectable-pill/SelectablePill.tsx +++ b/packages/web/src/components/selectable-pill/SelectablePill.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react' import cn from 'classnames' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import styles from './SelectablePill.module.css' @@ -19,6 +19,7 @@ const SelectablePill = ({ onClick, className }: SelectablePillProps) => { + const isMobile = useIsMobile() const wrappedOnClick = () => { !isSelected && onClick() } @@ -29,7 +30,7 @@ const SelectablePill = ({ className={cn(styles.pill, { [styles.selectedPill]: isSelected, [className!]: !!className, - [styles.isMobile]: isMobile() + [styles.isMobile]: isMobile })} > {content} diff --git a/packages/web/src/components/share-modal/ShareModal.tsx b/packages/web/src/components/share-modal/ShareModal.tsx index 2a87e4f4fad..841175c6f2f 100644 --- a/packages/web/src/components/share-modal/ShareModal.tsx +++ b/packages/web/src/components/share-modal/ShareModal.tsx @@ -18,9 +18,9 @@ import { useDispatch } from 'react-redux' import { make, useRecord } from 'common/store/analytics/actions' import * as embedModalActions from 'components/embed-modal/store/actions' import { ToastContext } from 'components/toast/ToastContext' +import { useIsMobile } from 'hooks/useIsMobile' import { useFlag } from 'hooks/useRemoteConfig' import { useModalState } from 'pages/modals/useModalState' -import { isMobile } from 'utils/clientUtil' import { SHARE_TOAST_TIMEOUT_MILLIS } from 'utils/constants' import { useSelector } from 'utils/reducer' import { openTwitterLink } from 'utils/tweet' @@ -42,6 +42,7 @@ export const ShareModal = () => { const { toast } = useContext(ToastContext) const dispatch = useDispatch() + const isMobile = useIsMobile() const record = useRecord() const { content, source } = useSelector(getShareState) const account = useSelector(getAccountUser) @@ -158,6 +159,6 @@ export const ShareModal = () => { content?.type === 'playlist' ? content.playlist.is_private : false } - if (isMobile()) return + if (isMobile) return return } diff --git a/packages/web/src/components/share-sound-to-tiktok-modal/ShareSoundToTikTokModal.tsx b/packages/web/src/components/share-sound-to-tiktok-modal/ShareSoundToTikTokModal.tsx index 7e21b734de4..19674670596 100644 --- a/packages/web/src/components/share-sound-to-tiktok-modal/ShareSoundToTikTokModal.tsx +++ b/packages/web/src/components/share-sound-to-tiktok-modal/ShareSoundToTikTokModal.tsx @@ -19,8 +19,8 @@ import { useDispatch, useSelector } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import Drawer from 'components/drawer/Drawer' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' +import { useIsMobile } from 'hooks/useIsMobile' import { useTikTokAuth } from 'hooks/useTikTokAuth' -import { isMobile } from 'utils/clientUtil' import styles from './ShareSoundToTikTokModal.module.css' const { getStatus, getTrack } = shareSoundToTiktokModalSelectors @@ -49,7 +49,7 @@ const fileRequirementErrorMessages = { } const ShareSoundToTikTokModal = () => { - const mobile = isMobile() + const isMobile = useIsMobile() const [isOpen, setIsOpen] = useModalState('ShareSoundToTikTok') const dispatch = useDispatch() @@ -148,7 +148,7 @@ const ShareSoundToTikTokModal = () => { } } - return mobile ? ( + return isMobile ? (
diff --git a/packages/web/src/components/subscribe-button/SubscribeButton.tsx b/packages/web/src/components/subscribe-button/SubscribeButton.tsx index da61bf5dfda..f9544863a4f 100644 --- a/packages/web/src/components/subscribe-button/SubscribeButton.tsx +++ b/packages/web/src/components/subscribe-button/SubscribeButton.tsx @@ -4,7 +4,7 @@ import cn from 'classnames' import IconNotification from 'assets/img/iconNotification.svg' import IconNotificationOff from 'assets/img/iconNotificationOff.svg' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import { isMatrix } from 'utils/theme/theme' import styles from './SubscribeButton.module.css' @@ -24,6 +24,7 @@ const SubscribeButton = ({ }: SubscribeButtonProps) => { const [isHovering, setIsHovering] = useState(false) const [isHoveringClicked, setIsHoveringClicked] = useState(false) + const isMobile = useIsMobile() const onClick = useCallback(() => { onToggleSubscribe() setIsHoveringClicked(true) @@ -39,7 +40,7 @@ const SubscribeButton = ({ [className as string]: !!className, [styles.notFollowing]: !isFollowing, [styles.isSubscribed]: isSubscribed, - [styles.isMobile]: isMobile(), + [styles.isMobile]: isMobile, [styles.isMatrix]: isMatrix() })} onMouseEnter={() => setIsHovering(true)} diff --git a/packages/web/src/components/tipping/tip-audio/ConfirmSendTip.tsx b/packages/web/src/components/tipping/tip-audio/ConfirmSendTip.tsx index 62fdd6feafe..dac7463e94b 100644 --- a/packages/web/src/components/tipping/tip-audio/ConfirmSendTip.tsx +++ b/packages/web/src/components/tipping/tip-audio/ConfirmSendTip.tsx @@ -5,7 +5,7 @@ import { Button, ButtonType, IconCheck } from '@audius/stems' import cn from 'classnames' import { useDispatch, useSelector } from 'react-redux' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { Transition, animated } from 'react-spring/renderprops' +import { Transition, animated } from 'react-spring/renderprops.cjs' import IconCaretLeft from 'assets/img/iconCaretLeft.svg' import IconSend from 'assets/img/iconSend.svg' diff --git a/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx b/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx index 208d8abffef..755fe2aeb2e 100644 --- a/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx +++ b/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx @@ -13,7 +13,7 @@ import { Modal, ModalHeader, ModalTitle } from '@audius/stems' import cn from 'classnames' import { useDispatch } from 'react-redux' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { animated, Transition } from 'react-spring/renderprops' +import { animated, Transition } from 'react-spring/renderprops.cjs' import { usePrevious } from 'react-use' import IconSuccess from 'assets/img/iconVerified.svg' diff --git a/packages/web/src/components/toast/Toast.tsx b/packages/web/src/components/toast/Toast.tsx index 1dbfa7bc7cd..fb1461c3687 100644 --- a/packages/web/src/components/toast/Toast.tsx +++ b/packages/web/src/components/toast/Toast.tsx @@ -122,13 +122,17 @@ class Toast extends PureComponent { triggerNode.parentNode as HTMLElement break case MountPlacement.PAGE: { - const page = document.getElementById('page') + const page = + typeof document !== 'undefined' && document.getElementById('page') if (page) popupContainer = () => page || undefined break } case MountPlacement.BODY: default: - popupContainer = () => document.body as HTMLElement + popupContainer = + typeof document !== 'undefined' + ? () => document.body as HTMLElement + : undefined } return ( diff --git a/packages/web/src/components/tooltip/Tooltip.tsx b/packages/web/src/components/tooltip/Tooltip.tsx index ca29670f927..8119c5905a7 100644 --- a/packages/web/src/components/tooltip/Tooltip.tsx +++ b/packages/web/src/components/tooltip/Tooltip.tsx @@ -63,7 +63,8 @@ export const Tooltip = ({ ) let popupContainer - const page = document.getElementById('page') + const page = + typeof document !== 'undefined' ? document.getElementById('page') : null switch (mount) { case 'parent': popupContainer = (triggerNode: HTMLElement) => diff --git a/packages/web/src/components/track/CardTitle.tsx b/packages/web/src/components/track/CardTitle.tsx index f0d5da53875..b55c37e1026 100644 --- a/packages/web/src/components/track/CardTitle.tsx +++ b/packages/web/src/components/track/CardTitle.tsx @@ -26,7 +26,7 @@ const messages = { } type CardTitleProps = { - className: string + className?: string isUnlisted: boolean isScheduledRelease: boolean isRemix: boolean diff --git a/packages/web/src/components/track/DownloadRow.tsx b/packages/web/src/components/track/DownloadRow.tsx index 756d473fbf1..4b61d182dc6 100644 --- a/packages/web/src/components/track/DownloadRow.tsx +++ b/packages/web/src/components/track/DownloadRow.tsx @@ -13,7 +13,7 @@ import { useDispatch, shallowEqual, useSelector } from 'react-redux' import { Icon } from 'components/Icon' import Tooltip from 'components/tooltip/Tooltip' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' const { toast } = toastActions const { getTrack } = cacheTracksSelectors diff --git a/packages/web/src/components/track/GiantTrackTile.tsx b/packages/web/src/components/track/GiantTrackTile.tsx index 9f99cc9541e..db3f9460094 100644 --- a/packages/web/src/components/track/GiantTrackTile.tsx +++ b/packages/web/src/components/track/GiantTrackTile.tsx @@ -34,6 +34,7 @@ import moment from 'moment' import { useDispatch, shallowEqual, useSelector } from 'react-redux' import IconRobot from 'assets/img/robot.svg' +import { ClientOnly } from 'components/client-only/ClientOnly' import DownloadButtons from 'components/download-buttons/DownloadButtons' import { EntityActionButton } from 'components/entity-page/EntityActionButton' import { Link, UserLink } from 'components/link' @@ -49,6 +50,7 @@ import Tooltip from 'components/tooltip/Tooltip' import { ComponentPlacement } from 'components/types' import { UserGeneratedText } from 'components/user-generated-text' import { getFeatureEnabled } from 'services/remote-config/featureFlagHelpers' +import { useSsrContext } from 'ssr/SsrContext' import { moodMap } from 'utils/Moods' import { trpc } from 'utils/trpcClientWeb' @@ -190,7 +192,8 @@ export const GiantTrackTile = ({ userId }: GiantTrackTileProps) => { const dispatch = useDispatch() - const [artworkLoading, setArtworkLoading] = useState(true) + const { isSsrEnabled } = useSsrContext() + const [artworkLoading, setArtworkLoading] = useState(!isSsrEnabled) const onArtworkLoad = useCallback( () => setArtworkLoading(false), [setArtworkLoading] @@ -608,66 +611,73 @@ export const GiantTrackTile = ({
-
- {showPlay ? ( - - ) : null} - {showPreview ? ( - - ) : null} - {isLongFormContent && isNewPodcastControlsEnabled ? ( - - ) : ( - renderListenCount() - )} -
+ +
+ {showPlay ? ( + + ) : null} + {showPreview ? ( + + ) : null} + {isLongFormContent && isNewPodcastControlsEnabled ? ( + + ) : ( + renderListenCount() + )} +
+
{renderStatsRow()} {renderScheduledReleaseRow()}
-
- {renderShareButton()} - {renderMakePublicButton()} - {hasStreamAccess && renderRepostButton()} - {hasStreamAccess && renderFavoriteButton()} - - {/* prop types for overflow menu don't work correctly + +
+ {renderShareButton()} + {renderMakePublicButton()} + {hasStreamAccess && renderRepostButton()} + {hasStreamAccess && renderFavoriteButton()} + + {/* prop types for overflow menu don't work correctly so we need to cast here */} - - {(ref, triggerPopup) => ( -
-
- )} -
-
-
+ + {(ref, triggerPopup) => ( +
+
+ )} +
+
+
+
{aiAttributionUserId ? ( @@ -683,20 +693,24 @@ export const GiantTrackTile = ({
- {isStreamGated && streamConditions ? ( - - ) : null} + + {isStreamGated && streamConditions ? ( + + ) : null} + - {aiAttributionUserId ? ( - - ) : null} + + {aiAttributionUserId ? ( + + ) : null} +
@@ -726,13 +740,15 @@ export const GiantTrackTile = ({ {description} ) : null} - {renderTags()} - {!isLosslessDownloadsEnabled ? renderDownloadButtons() : null} - {isLosslessDownloadsEnabled && hasDownloadableAssets ? ( - - - - ) : null} + + {renderTags()} + {!isLosslessDownloadsEnabled ? renderDownloadButtons() : null} + {isLosslessDownloadsEnabled && hasDownloadableAssets ? ( + + + + ) : null} +
) diff --git a/packages/web/src/components/transition-container/TransitionContainer.tsx b/packages/web/src/components/transition-container/TransitionContainer.tsx index de372611e81..ccb79a60a6e 100644 --- a/packages/web/src/components/transition-container/TransitionContainer.tsx +++ b/packages/web/src/components/transition-container/TransitionContainer.tsx @@ -1,7 +1,7 @@ import { ReactElement } from 'react' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { animated, Transition } from 'react-spring/renderprops' +import { animated, Transition } from 'react-spring/renderprops.cjs' type TransitionContainerProps = { render: (item: any, style: object) => ReactElement diff --git a/packages/web/src/components/update-dot/UpdateDot.tsx b/packages/web/src/components/update-dot/UpdateDot.tsx index d7117531604..e0759f798e2 100644 --- a/packages/web/src/components/update-dot/UpdateDot.tsx +++ b/packages/web/src/components/update-dot/UpdateDot.tsx @@ -2,7 +2,7 @@ import { ComponentProps, forwardRef } from 'react' import cn from 'classnames' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import styles from './UpdateDot.module.css' diff --git a/packages/web/src/components/upload/InvalidFileType.jsx b/packages/web/src/components/upload/InvalidFileType.jsx index 574d06051f2..a6a737b4b09 100644 --- a/packages/web/src/components/upload/InvalidFileType.jsx +++ b/packages/web/src/components/upload/InvalidFileType.jsx @@ -1,7 +1,7 @@ import cn from 'classnames' import PropTypes from 'prop-types' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web -import { Spring } from 'react-spring/renderprops' +import { Spring } from 'react-spring/renderprops.cjs' import { Text } from 'components/typography' diff --git a/packages/web/src/components/usdc-manual-transfer/USDCManualTransfer.tsx b/packages/web/src/components/usdc-manual-transfer/USDCManualTransfer.tsx index fa6cd5fe1e7..638c6623ade 100644 --- a/packages/web/src/components/usdc-manual-transfer/USDCManualTransfer.tsx +++ b/packages/web/src/components/usdc-manual-transfer/USDCManualTransfer.tsx @@ -21,10 +21,10 @@ import { AddressTile } from 'components/address-tile' import { ToastContext } from 'components/toast/ToastContext' import { Text } from 'components/typography' import { Hint } from 'components/withdraw-usdc-modal/components/Hint' +import { useIsMobile } from 'hooks/useIsMobile' import { track as trackAnalytics, make } from 'services/analytics' import { audiusBackendInstance } from 'services/audius-backend/audius-backend-instance' import { getUSDCUserBank } from 'services/solana/solana' -import { isMobile } from 'utils/clientUtil' import { copyToClipboard } from 'utils/clipboardUtil' import styles from './USDCManualTransfer.module.css' @@ -67,7 +67,7 @@ export const USDCManualTransfer = ({ mint: 'usdc' }) const { toast } = useContext(ToastContext) - const mobile = isMobile() + const isMobile = useIsMobile() const { value: USDCUserBank } = useAsync(async () => { const USDCUserBankPubKey = await getUSDCUserBank() @@ -87,13 +87,13 @@ export const USDCManualTransfer = ({ return ( - - {mobile ? {messages.explainer} : null} + + {isMobile ? {messages.explainer} : null}
{USDCUserBank ? : null}
- {!mobile ? {messages.explainer} : null} + {!isMobile ? {messages.explainer} : null}
{amountInCents === undefined ? ( @@ -113,7 +113,7 @@ export const USDCManualTransfer = ({ - {mobile ? null : ( + {isMobile ? null : ( @@ -121,7 +121,7 @@ export const USDCManualTransfer = ({ ) : ( <> - {mobile ? null : ( + {isMobile ? null : ( diff --git a/packages/web/src/components/user-list/UserList.tsx b/packages/web/src/components/user-list/UserList.tsx index 274f96447c5..fee10ff4be1 100644 --- a/packages/web/src/components/user-list/UserList.tsx +++ b/packages/web/src/components/user-list/UserList.tsx @@ -17,8 +17,8 @@ import { connect } from 'react-redux' import { Dispatch } from 'redux' import * as unfollowConfirmationActions from 'components/unfollow-confirmation-modal/store/actions' +import { useIsMobile } from 'hooks/useIsMobile' import { AppState } from 'store/types' -import { isMobile } from 'utils/clientUtil' import { profilePage } from 'utils/route' import UserList from './components/UserList' @@ -53,13 +53,14 @@ type ConnectedUserListProps = ConnectedUserListOwnProps & ReturnType const ConnectedUserList = (props: ConnectedUserListProps) => { + const isMobile = useIsMobile() const onFollow = (userId: ID) => { props.onFollow(userId) if (!props.loggedIn && props.afterFollow) props.afterFollow() } const onUnfollow = (userId: ID) => { - props.onUnfollow(userId) + props.onUnfollow(userId, isMobile) if (!props.loggedIn && props.afterUnfollow) props.afterUnfollow() } @@ -104,7 +105,7 @@ const ConnectedUserList = (props: ConnectedUserListProps) => { userId={props.userId} onClickArtistName={onClickArtistName} loadMore={props.loadMore} - isMobile={props.isMobile} + isMobile={isMobile} getScrollParent={props.getScrollParent} tag={props.tag} otherUserId={props.otherUserId} @@ -135,7 +136,6 @@ function mapStateToProps(state: AppState, ownProps: ConnectedUserListOwnProps) { users, hasMore, loading, - isMobile: isMobile(), otherUserId } } @@ -144,12 +144,11 @@ function mapDispatchToProps( dispatch: Dispatch, ownProps: ConnectedUserListOwnProps ) { - const mobile = isMobile() return { onFollow: (userId: ID) => dispatch(socialActions.followUser(userId, FollowSource.USER_LIST)), - onUnfollow: (userId: ID) => { - if (mobile) { + onUnfollow: (userId: ID, isMobile: boolean) => { + if (isMobile) { dispatch(unfollowConfirmationActions.setOpen(userId)) } else { dispatch(socialActions.unfollowUser(userId, FollowSource.USER_LIST)) diff --git a/packages/web/src/hooks/useIsMobile.ts b/packages/web/src/hooks/useIsMobile.ts new file mode 100644 index 00000000000..69c8e9a878e --- /dev/null +++ b/packages/web/src/hooks/useIsMobile.ts @@ -0,0 +1,11 @@ +import { useSsrContext } from 'ssr/SsrContext' + +/** + * Returns true if the current device is a mobile device, based on the user agent + * + * This supports SSR by pulling the value from SsrContext + */ +export const useIsMobile = () => { + const { isMobile } = useSsrContext() + return isMobile +} diff --git a/packages/web/src/hooks/useWithMobileStyle.ts b/packages/web/src/hooks/useWithMobileStyle.ts index d3183d6d389..1a9840b8ea1 100644 --- a/packages/web/src/hooks/useWithMobileStyle.ts +++ b/packages/web/src/hooks/useWithMobileStyle.ts @@ -5,7 +5,7 @@ import cn from 'classnames' // eslint-disable-next-line import { ClassValue } from 'classnames/types' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' /** * Wraps classnames and applies a mobile class as needed. @@ -17,12 +17,12 @@ import { isMobile } from 'utils/clientUtil' * ``` */ export const useWithMobileStyle = (mobileClassName: string) => { - const mobile = isMobile() + const isMobile = useIsMobile() const withMobile = useMemo(() => { - const mobileStyle = { [mobileClassName]: mobile } + const mobileStyle = { [mobileClassName]: isMobile } return (...classnames: ClassValue[]) => cn(...classnames, mobileStyle) - }, [mobile, mobileClassName]) + }, [isMobile, mobileClassName]) return withMobile } diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx index eac6ef17d57..5ea23b0d962 100644 --- a/packages/web/src/index.tsx +++ b/packages/web/src/index.tsx @@ -3,6 +3,8 @@ import 'setimmediate' import { createRoot } from 'react-dom/client' import './index.css' +import { SsrContextProvider } from 'ssr/SsrContext' + import { Root } from './Root' // @ts-ignore @@ -11,5 +13,17 @@ window.global ||= window const container = document.getElementById('root') if (container) { const root = createRoot(container) - root.render() + root.render( + + + + ) } diff --git a/packages/web/src/pages/ai-attributed-tracks-page/AiPage.tsx b/packages/web/src/pages/ai-attributed-tracks-page/AiPage.tsx index 0bb19ca9fff..c139a164f49 100644 --- a/packages/web/src/pages/ai-attributed-tracks-page/AiPage.tsx +++ b/packages/web/src/pages/ai-attributed-tracks-page/AiPage.tsx @@ -1,6 +1,6 @@ import { RefObject } from 'react' -import { useIsMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import AiPageProvider from './AiPageProvider' import AiPageDesktopContent from './components/desktop/AiPage' diff --git a/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx b/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx index 4c444f9fab1..a311122b175 100644 --- a/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx +++ b/packages/web/src/pages/audio-rewards-page/AudioRewardsPage.tsx @@ -16,10 +16,10 @@ import NavContext, { RightPreset } from 'components/nav/store/context' import Page from 'components/page/Page' +import { useIsMobile } from 'hooks/useIsMobile' import { useFlag, useRemoteVar } from 'hooks/useRemoteConfig' import { useRequiresAccount } from 'hooks/useRequiresAccount' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { isMobile } from 'utils/clientUtil' import { AUDIO_PAGE, BASE_URL, TRENDING_PAGE } from 'utils/route' import styles from './AudioRewardsPage.module.css' @@ -135,10 +135,11 @@ const MobilePage = ({ children }: { children: ReactNode }) => { export const AudioRewardsPage = () => { const dispatch = useDispatch() + const isMobile = useIsMobile() useEffect(() => { dispatch(getBalance()) }, [dispatch]) - const Page = isMobile() ? MobilePage : DesktopPage + const Page = isMobile ? MobilePage : DesktopPage return ( diff --git a/packages/web/src/pages/audio-rewards-page/Tiers.tsx b/packages/web/src/pages/audio-rewards-page/Tiers.tsx index 05e0c600301..7c1d78e2c10 100644 --- a/packages/web/src/pages/audio-rewards-page/Tiers.tsx +++ b/packages/web/src/pages/audio-rewards-page/Tiers.tsx @@ -20,8 +20,8 @@ import IconGoldBadge from 'assets/img/tokenBadgeGold108@2x.png' import IconPlatinumBadge from 'assets/img/tokenBadgePlatinum108@2x.png' import IconSilverBadge from 'assets/img/tokenBadgeSilver108@2x.png' import { BadgeTierText } from 'components/user-badges/ProfilePageBadge' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' import styles from './Tiers.module.css' @@ -228,7 +228,7 @@ const Tiers = () => { const wm = useWithMobileStyle(styles.mobile) - const mobile = isMobile() + const isMobile = useIsMobile() return (
@@ -245,7 +245,7 @@ const Tiers = () => { isActive={tier === t} key={t} onClickDiscord={onClickDiscord} - isCompact={mobile} + isCompact={isMobile} /> ))}
diff --git a/packages/web/src/pages/audio-rewards-page/Tiles.tsx b/packages/web/src/pages/audio-rewards-page/Tiles.tsx index dbb551e60f2..7efd7916022 100644 --- a/packages/web/src/pages/audio-rewards-page/Tiles.tsx +++ b/packages/web/src/pages/audio-rewards-page/Tiles.tsx @@ -16,8 +16,8 @@ import IconSend from 'assets/img/iconSend.svg' import { useModalState } from 'common/hooks/useModalState' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import MobileConnectWalletsDrawer from 'components/mobile-connect-wallets-drawer/MobileConnectWalletsDrawer' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { isMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' import styles from './Tiles.module.css' @@ -95,31 +95,31 @@ export const WalletTile = ({ className }: { className?: string }) => { const dispatch = useDispatch() const [, openTransferDrawer] = useModalState('TransferAudioMobileWarning') - const mobile = isMobile() + const isMobile = useIsMobile() const onClickReceive = useCallback(() => { - if (mobile) { + if (isMobile) { openTransferDrawer(true) } else { dispatch(pressReceive()) } - }, [dispatch, mobile, openTransferDrawer]) + }, [dispatch, isMobile, openTransferDrawer]) const onClickSend = useCallback(() => { - if (mobile) { + if (isMobile) { openTransferDrawer(true) } else { dispatch(pressSend()) } - }, [mobile, dispatch, openTransferDrawer]) + }, [isMobile, dispatch, openTransferDrawer]) const [, setOpen] = useModalState('MobileConnectWalletsDrawer') const onClickConnectWallets = useCallback(() => { - if (mobile) { + if (isMobile) { setOpen(true) } else { dispatch(pressConnectWallets()) } - }, [mobile, setOpen, dispatch]) + }, [isMobile, setOpen, dispatch]) const onCloseConnectWalletsDrawer = useCallback(() => { setOpen(false) @@ -157,7 +157,7 @@ export const WalletTile = ({ className }: { className?: string }) => { onClick={onClickConnectWallets} type={ButtonType.GLASS} /> - {mobile && ( + {isMobile && ( )}
diff --git a/packages/web/src/pages/audio-rewards-page/components/ExplainerTile.tsx b/packages/web/src/pages/audio-rewards-page/components/ExplainerTile.tsx index 6a30f1dbaee..309b839f72e 100644 --- a/packages/web/src/pages/audio-rewards-page/components/ExplainerTile.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/ExplainerTile.tsx @@ -4,8 +4,8 @@ import { Theme } from '@audius/common' import cn from 'classnames' import TokenStill from 'assets/img/tokenSpinStill.png' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { isMobile } from 'utils/clientUtil' import { getTheme, isDarkMode as getIsDarkMode } from 'utils/theme/theme' import styles from './ExplainerTile.module.css' @@ -43,6 +43,7 @@ export const ExplainerTile = ({ className }: { className?: string }) => { const [mouseOver, setMouseOver] = useState(false) const videoRef = useRef(null) const [initialPlaysRemaining, setInitialPlays] = useState(1) + const isMobile = useIsMobile() const handleOnEnded = useCallback(() => { setInitialPlays((p) => p - 1) @@ -59,7 +60,7 @@ export const ExplainerTile = ({ className }: { className?: string }) => { const isDarkMode = getIsDarkMode() const isMatrixMode = getTheme() === Theme.MATRIX - const showSvgToken = isDarkMode || isMatrixMode || isMobile() + const showSvgToken = isDarkMode || isMatrixMode || isMobile const wm = useWithMobileStyle(styles.mobile) return ( diff --git a/packages/web/src/pages/audio-rewards-page/components/WalletManagementTile.tsx b/packages/web/src/pages/audio-rewards-page/components/WalletManagementTile.tsx index 59198ba6589..ee3b73bd340 100644 --- a/packages/web/src/pages/audio-rewards-page/components/WalletManagementTile.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/WalletManagementTile.tsx @@ -21,6 +21,7 @@ import { push as pushRoute } from 'connected-react-router' import { useDispatch, useSelector } from 'react-redux' import { useAsync } from 'react-use' +import { useHistoryContext } from 'app/HistoryProvider' import IconReceive from 'assets/img/iconReceive.svg' import IconSend from 'assets/img/iconSend.svg' import IconSettings from 'assets/img/iconSettings.svg' @@ -34,9 +35,10 @@ import MobileConnectWalletsDrawer from 'components/mobile-connect-wallets-drawer import { OnRampButton } from 'components/on-ramp-button/OnRampButton' import { ToastContext } from 'components/toast/ToastContext' import Tooltip from 'components/tooltip/Tooltip' +import { useIsMobile } from 'hooks/useIsMobile' import { useFlag, useRemoteVar } from 'hooks/useRemoteConfig' import { getLocation } from 'services/Location' -import { isMobile, getClient } from 'utils/clientUtil' +import { getClient } from 'utils/clientUtil' import { AUDIO_TRANSACTIONS_PAGE, pushUniqueRoute, @@ -86,25 +88,25 @@ const AdvancedWalletActions = () => { const dispatch = useDispatch() const [, openTransferDrawer] = useModalState('TransferAudioMobileWarning') - const mobile = isMobile() + const isMobile = useIsMobile() const { isEnabled: isTransactionsEnabled } = useFlag( FeatureFlags.AUDIO_TRANSACTIONS_HISTORY ) const onClickReceive = useCallback(() => { - if (mobile) { + if (isMobile) { openTransferDrawer(true) } else { dispatch(pressReceive()) } - }, [dispatch, mobile, openTransferDrawer]) + }, [dispatch, isMobile, openTransferDrawer]) const onClickSend = useCallback(() => { - if (mobile) { + if (isMobile) { openTransferDrawer(true) } else { dispatch(pressSend()) } - }, [mobile, dispatch, openTransferDrawer]) + }, [isMobile, dispatch, openTransferDrawer]) const [, setOpen] = useModalState('MobileConnectWalletsDrawer') const onClickTransactions = useCallback(() => { @@ -112,12 +114,12 @@ const AdvancedWalletActions = () => { }, [dispatch]) const onClickConnectWallets = useCallback(() => { - if (mobile) { + if (isMobile) { setOpen(true) } else { dispatch(pressConnectWallets()) } - }, [mobile, setOpen, dispatch]) + }, [isMobile, setOpen, dispatch]) const onCloseConnectWalletsDrawer = useCallback(() => { setOpen(false) @@ -149,7 +151,7 @@ const AdvancedWalletActions = () => { type={ButtonType.GLASS} minWidth={200} /> - {!mobile && isTransactionsEnabled && ( + {!isMobile && isTransactionsEnabled && (
@@ -190,17 +192,19 @@ const OnRampTooltipButton = ({ bannedState }: OnRampTooltipButtonProps) => { const dispatch = useDispatch() + const { history } = useHistoryContext() + const onClick = useCallback(() => { dispatch( startBuyAudioFlow({ provider, onSuccess: { - action: pushUniqueRoute(TRENDING_PAGE), + action: pushUniqueRoute(history.location, TRENDING_PAGE), message: messages.findArtists } }) ) - }, [dispatch, provider]) + }, [dispatch, provider, history]) const bannedRegionText = provider === OnRampProvider.COINBASE ? messages.coinbasePayRegionNotSupported diff --git a/packages/web/src/pages/audio-rewards-page/components/WalletsTable.tsx b/packages/web/src/pages/audio-rewards-page/components/WalletsTable.tsx index 58a02efe795..b06151c3a9a 100644 --- a/packages/web/src/pages/audio-rewards-page/components/WalletsTable.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/WalletsTable.tsx @@ -18,8 +18,8 @@ import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import Toast from 'components/toast/Toast' import { ToastContext } from 'components/toast/ToastContext' import { ComponentPlacement, MountPlacement } from 'components/types' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { useIsMobile } from 'utils/clientUtil' import { copyToClipboard } from 'utils/clipboardUtil' import { NEW_WALLET_CONNECTED_TOAST_TIMEOUT_MILLIS } from 'utils/constants' import { useSelector } from 'utils/reducer' diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx index 747367dbee3..eedd2dbb9e0 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx @@ -19,9 +19,9 @@ import { useSelector } from 'react-redux' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import { SummaryTable } from 'components/summary-table' +import { useIsMobile } from 'hooks/useIsMobile' import { useNavigateToPage } from 'hooks/useNavigateToPage' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import { isMobile } from 'utils/clientUtil' import { EXPLORE_PREMIUM_TRACKS_PAGE, UPLOAD_PAGE } from 'utils/route' import { ProgressDescription } from './ProgressDescription' @@ -90,6 +90,7 @@ export const AudioMatchingRewardsModalContent = ({ errorContent }: AudioMatchingRewardsModalContentProps) => { const wm = useWithMobileStyle(styles.mobile) + const isMobile = useIsMobile() const navigateToPage = useNavigateToPage() const { fullDescription } = challengeRewardsConfig[challengeName] const { @@ -141,7 +142,7 @@ export const AudioMatchingRewardsModalContent = ({ return (
- {isMobile() ? ( + {isMobile ? ( <> {progressDescription}
diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx index 7988a979a05..725312965ae 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx @@ -36,9 +36,9 @@ import Toast from 'components/toast/Toast' import { ToastContext } from 'components/toast/ToastContext' import Tooltip from 'components/tooltip/Tooltip' import { ComponentPlacement, MountPlacement } from 'components/types' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' import { getChallengeConfig } from 'pages/audio-rewards-page/config' -import { isMobile } from 'utils/clientUtil' import { copyToClipboard, getCopyableLink } from 'utils/clipboardUtil' import { CLAIM_REWARD_TOAST_TIMEOUT_MILLIS } from 'utils/constants' import { openTwitterLink } from 'utils/tweet' @@ -226,7 +226,7 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { const userHandle = useSelector(getUserHandle) const dispatch = useDispatch() const wm = useWithMobileStyle(styles.mobile) - const displayMobileContent = isMobile() + const isMobile = useIsMobile() const userChallenges = useSelector(getOptimisticUserChallenges) const challenge = userChallenges[modalType] @@ -388,7 +388,7 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { /> ) : (
- {displayMobileContent ? ( + {isMobile ? ( <> {progressDescription}
diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ModalDrawer.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ModalDrawer.tsx index d9d90cf1a1d..9f744cee664 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ModalDrawer.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ModalDrawer.tsx @@ -2,7 +2,7 @@ import { Modal, ModalProps } from '@audius/stems' import cn from 'classnames' import Drawer, { DrawerProps } from 'components/drawer/Drawer' -import { isMobile } from 'utils/clientUtil' +import { useIsMobile } from 'hooks/useIsMobile' import styles from './ModalDrawer.module.css' @@ -16,7 +16,8 @@ type ModalDrawerProps = ModalProps & */ const ModalDrawer = (props: ModalDrawerProps) => { const gradientTitle = props.useGradientTitle ?? true - if (isMobile()) { + const isMobile = useIsMobile() + if (isMobile) { return (