浏览代码

Merge branch 'main' into ml-alpha

Abhinav 2 年之前
父节点
当前提交
3b468cb154
共有 100 个文件被更改,包括 2766 次插入1401 次删除
  1. 0 13
      .babelrc
  2. 0 1
      .eslintignore
  3. 42 37
      .eslintrc.json
  4. 1 1
      .husky/pre-commit
  5. 13 0
      .lintstagedrc.js
  6. 1 0
      .yarnrc
  7. 13 5
      README.md
  8. 1 6
      configUtil.js
  9. 31 31
      next.config.js
  10. 12 25
      package.json
  11. 1 1
      public/_headers
  12. 6 1
      sentry.client.config.js
  13. 8 0
      sentry.server.config.js
  14. 7 6
      sentryConfigUtil.js
  15. 3 2
      src/components/Badge.tsx
  16. 10 0
      src/components/Chip.tsx
  17. 32 9
      src/components/CodeBlock/CopyButton.tsx
  18. 5 15
      src/components/CodeBlock/index.tsx
  19. 0 3
      src/components/Collections/AllCollections/CollectionSort/options.tsx
  20. 2 2
      src/components/Collections/CollectionInfoWithOptions.tsx
  21. 4 2
      src/components/Collections/CollectionListBar/CollectionCard.tsx
  22. 4 4
      src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx
  23. 25 0
      src/components/Collections/CollectionOptions/SharedCollectionOption.tsx
  24. 35 0
      src/components/Collections/CollectionOptions/index.tsx
  25. 8 4
      src/components/Collections/CollectionSelector/index.tsx
  26. 1 1
      src/components/Collections/CollectionShare/emailShare.tsx
  27. 2 3
      src/components/Collections/CollectionShare/publicShare/control.tsx
  28. 8 1
      src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx
  29. 11 4
      src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx
  30. 18 16
      src/components/Collections/CollectionShare/publicShare/manage/index.tsx
  31. 10 3
      src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx
  32. 0 49
      src/components/Collections/CollectionShare/publicShare/manage/linkPassword.tsx
  33. 72 0
      src/components/Collections/CollectionShare/publicShare/manage/linkPassword/index.tsx
  34. 3 3
      src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx
  35. 34 0
      src/components/Collections/CollectionShare/publicShare/manage/publicCollect.tsx
  36. 1 1
      src/components/Collections/index.tsx
  37. 0 39
      src/components/DeleteBtn.tsx
  38. 18 0
      src/components/DialogBox/DialogIcon.tsx
  39. 11 0
      src/components/DialogBox/base.tsx
  40. 2 0
      src/components/DialogBox/index.tsx
  41. 1 9
      src/components/EnteDateTimePicker.tsx
  42. 11 0
      src/components/EnteDrawer.tsx
  43. 1 1
      src/components/ExportFinished.tsx
  44. 0 1
      src/components/FixCreationTime/index.tsx
  45. 0 1
      src/components/FixCreationTime/options.tsx
  46. 0 19
      src/components/IconWithMessage.tsx
  47. 22 0
      src/components/Navbar/EnteLinkLogo.tsx
  48. 30 25
      src/components/Notification.tsx
  49. 172 108
      src/components/PhotoFrame.tsx
  50. 84 50
      src/components/PhotoList.tsx
  51. 0 73
      src/components/PhotoSwipe/InfoDialog/ExifData.tsx
  52. 0 100
      src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx
  53. 0 98
      src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx
  54. 0 175
      src/components/PhotoSwipe/InfoDialog/index.tsx
  55. 175 0
      src/components/PhotoSwipe/infoDialog.tsx
  56. 92 0
      src/components/PhotoViewer/FileInfo/ExifData.tsx
  57. 47 0
      src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx
  58. 61 0
      src/components/PhotoViewer/FileInfo/InfoItem.tsx
  59. 126 0
      src/components/PhotoViewer/FileInfo/RenderCaption.tsx
  60. 21 38
      src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
  61. 122 0
      src/components/PhotoViewer/FileInfo/RenderFileName.tsx
  62. 0 0
      src/components/PhotoViewer/FileInfo/RenderInfoItem.tsx
  63. 329 0
      src/components/PhotoViewer/FileInfo/index.tsx
  64. 0 0
      src/components/PhotoViewer/PhotoSwipe-old.tsx
  65. 287 80
      src/components/PhotoViewer/index.tsx
  66. 0 0
      src/components/PhotoViewer/styledComponents/Legend.tsx
  67. 0 0
      src/components/PhotoViewer/styledComponents/LegendContainer.tsx
  68. 0 0
      src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx
  69. 0 0
      src/components/PhotoViewer/styledComponents/Pre.tsx
  70. 0 0
      src/components/PhotoViewer/styledComponents/SmallLoadingSpinner.tsx
  71. 6 1
      src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx
  72. 3 1
      src/components/Search/SearchBar/searchInput/index.tsx
  73. 0 47
      src/components/Sidebar/DebugLogs.tsx
  74. 84 0
      src/components/Sidebar/DebugSection.tsx
  75. 2 2
      src/components/Sidebar/HelpSection.tsx
  76. 4 4
      src/components/Sidebar/ShortcutSection.tsx
  77. 2 2
      src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx
  78. 1 1
      src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx
  79. 17 17
      src/components/Sidebar/SubscriptionStatus/index.tsx
  80. 2 2
      src/components/Sidebar/index.tsx
  81. 3 5
      src/components/Sidebar/styledComponents.tsx
  82. 20 23
      src/components/SignUp.tsx
  83. 36 5
      src/components/SingleInputForm.tsx
  84. 9 4
      src/components/SubmitButton.tsx
  85. 57 0
      src/components/Titlebar.tsx
  86. 21 8
      src/components/Upload/UploadButton.tsx
  87. 78 61
      src/components/Upload/UploadProgress/dialog.tsx
  88. 14 7
      src/components/Upload/UploadProgress/inProgressSection.tsx
  89. 2 0
      src/components/Upload/UploadProgress/title.tsx
  90. 45 12
      src/components/Upload/UploadTypeSelector/index.tsx
  91. 245 84
      src/components/Upload/Uploader.tsx
  92. 43 0
      src/components/UserNameInputDialog.tsx
  93. 3 3
      src/components/VerifyMasterPasswordForm.tsx
  94. 1 4
      src/components/WatchFolder/styledComponents.tsx
  95. 10 0
      src/components/icons/ente.tsx
  96. 3 4
      src/components/pages/dedupe/SelectedFileOptions.tsx
  97. 2 23
      src/components/pages/gallery/LinkButton.tsx
  98. 1 2
      src/components/pages/gallery/Navbar.tsx
  99. 14 12
      src/components/pages/gallery/PlanSelector/card/index.tsx
  100. 2 1
      src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx

+ 0 - 13
.babelrc

@@ -1,13 +0,0 @@
-{
-    "presets": ["next/babel"],
-    "plugins": [
-        [
-            "styled-components",
-            {
-                "ssr": true,
-                "displayName": true,
-                "preprocess": false
-            }
-        ]
-    ]
-}

+ 0 - 1
.eslintignore

@@ -1 +0,0 @@
-thirdparty

+ 42 - 37
.eslintrc.json

@@ -1,60 +1,65 @@
 {
     "root": true,
-    "env": {
-        "browser": true,
-        "es2021": true,
-        "node": true
+    "parserOptions": {
+        "project": ["./tsconfig.json"]
     },
     "extends": [
-        "plugin:react/recommended",
+        "next/core-web-vitals",
         "eslint:recommended",
-        "plugin:@typescript-eslint/eslint-recommended",
-        "google",
+        "plugin:@typescript-eslint/recommended",
+        "plugin:@typescript-eslint/recommended-requiring-type-checking",
         "prettier"
     ],
-    "parser": "@typescript-eslint/parser",
-    "parserOptions": {
-        "ecmaFeatures": {
-            "jsx": true
-        },
-        "ecmaVersion": 12,
-        "sourceType": "module"
-    },
-    "plugins": [
-        "react",
-        "@typescript-eslint"
-    ],
+    "plugins": ["@typescript-eslint"],
+
     "rules": {
-        "indent":"off",
+        "indent": "off",
         "class-methods-use-this": "off",
         "react/prop-types": "off",
         "react/display-name": "off",
         "react/no-unescaped-entities": "off",
         "no-unused-vars": "off",
-        "@typescript-eslint/no-unused-vars": [
-            "error"
-        ],
+        "@typescript-eslint/no-unused-vars": ["error"],
         "require-jsdoc": "off",
         "valid-jsdoc": "off",
         "max-len": "off",
         "new-cap": "off",
         "no-invalid-this": "off",
         "eqeqeq": "error",
-        "object-curly-spacing": [
+        "object-curly-spacing": ["error", "always"],
+        "space-before-function-paren": "off",
+        "operator-linebreak": [
             "error",
-            "always"
+            "after",
+            { "overrides": { "?": "before", ":": "before" } }
         ],
-        "space-before-function-paren": "off",
-        "operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
-    },
-    "settings": {
-        "react": {
-            "version": "detect"
-        }
-    },
-    "globals": {
-        "JSX": "readonly",
-        "NodeJS": "readonly",
-        "ReadableStreamDefaultController": "readonly"
+        "import/no-anonymous-default-export": [
+            "error",
+            {
+                "allowNew": true
+            }
+        ],
+        "@typescript-eslint/no-unsafe-member-access": "off",
+        "@typescript-eslint/no-unsafe-return": "off",
+        "@typescript-eslint/no-unsafe-assignment": "off",
+        "@typescript-eslint/no-inferrable-types": "off",
+        "@typescript-eslint/restrict-template-expressions": "off",
+        "@typescript-eslint/ban-types": "off",
+        "@typescript-eslint/no-floating-promises": "off",
+        "@typescript-eslint/no-unsafe-call": "off",
+        "@typescript-eslint/require-await": "off",
+        "@typescript-eslint/restrict-plus-operands": "off",
+        "@typescript-eslint/no-var-requires": "off",
+        "@typescript-eslint/no-empty-interface": "off",
+        "@typescript-eslint/no-misused-promises": "off",
+        "@typescript-eslint/no-empty-function": "off",
+        "@typescript-eslint/explicit-module-boundary-types": "off",
+        "@typescript-eslint/no-explicit-any": "off",
+        "@typescript-eslint/no-unnecessary-type-assertion": "off",
+        "react-hooks/rules-of-hooks": "off",
+        "react-hooks/exhaustive-deps": "off",
+        "@next/next/no-img-element": "off",
+        "@typescript-eslint/no-unsafe-argument": "off",
+        "jsx-a11y/alt-text": "off"
     }
 }

+ 1 - 1
.husky/pre-commit

@@ -1,4 +1,4 @@
 #!/bin/sh
 . "$(dirname "$0")/_/husky.sh"
 
-npx lint-staged
+yarn lint-staged

+ 13 - 0
.lintstagedrc.js

@@ -0,0 +1,13 @@
+const path = require('path');
+
+const buildEslintCommand = (filenames) =>
+    `next lint --fix --file ${filenames
+        .map((f) => path.relative(process.cwd(), f))
+        .join(' --file ')}`;
+
+const buildPrettierCommand = (filenames) =>
+    `yarn prettier --write --ignore-unknown ${filenames.join(' ')}`;
+
+module.exports = {
+    '*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand],
+};

+ 1 - 0
.yarnrc

@@ -0,0 +1 @@
+network-timeout 500000

+ 13 - 5
README.md

@@ -2,9 +2,16 @@
 
 **ente** is a cloud storage provider that provides end-to-end encryption for your data.
 
-We have open-source apps across [Android](https://github.com/ente-io/frame), [iOS](https://github.com/ente-io/frame), [web](https://github.com/ente-io/bada-frame) and [desktop](https://github.com/ente-io/bhari-frame) that automatically backup your photos and videos.
+We have open-source apps across
+[Android](https://github.com/ente-io/photos-app),
+[iOS](https://github.com/ente-io/photos-app),
+[web](https://github.com/ente-io/photos-web) and
+[desktop](https://github.com/ente-io/photos-desktop) that automatically backup
+your photos and videos.
+
+This repository contains the code for our web app, built with a lot of ❤️, and a
+little bit of JavaScript.
 
-This repository contains the code for our web app, built with a lot of ❤️, and a little bit of JavaScript.
 <br/><br/><br/>
 
 ![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png)
@@ -30,7 +37,7 @@ The deployed application is accessible @ [web.ente.io](https://web.ente.io).
 
 ## 🧑‍💻 Building from source
 
-1. Clone this repository with `git clone git@github.com:ente-io/bada-frame.git` 
+1. Clone this repository with `git clone https://github.com/ente-io/photos-web.git`
 2. Pull in all submodules with `git submodule update --init --recursive`
 3. Install dependencies with `yarn install`
 4. Finally, run the development server with `yarn dev`
@@ -55,7 +62,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
 
 If you like this project, please consider upgrading to a paid subscription.
 
-If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/bada-frame/stargazers) this project.
+If you would like to motivate us to keep building, you can do so by
+[starring](https://github.com/ente-io/photos-web/stargazers) this project.
 
 <br/>
 
@@ -69,5 +77,5 @@ An important part of our journey is to build better software by consistently lis
 
 ---
 
-Cross-browser testing provided by 
+Cross-browser testing provided by
 [<img src="https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780" width="115" height="25">](https://www.browserstack.com/open-source)

+ 1 - 6
configUtil.js

@@ -26,7 +26,7 @@ module.exports = {
         'style-src': "'self' 'unsafe-inline'",
         'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
         'connect-src':
-            "'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
+            "'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/",
         'base-uri ': "'self'",
         // to allow worker
         'child-src': "'self' blob:",
@@ -37,11 +37,6 @@ module.exports = {
         'report-to': ' https://csp-reporter.ente.io/local',
     },
 
-    WORKBOX_CONFIG: {
-        swSrc: 'src/serviceWorker.js',
-        exclude: [/manifest\.json$/i],
-    },
-
     ALL_ROUTES: '/(.*)',
 
     buildCSPHeader: (directives) => ({

+ 31 - 31
next.config.js

@@ -1,7 +1,6 @@
 const withBundleAnalyzer = require('@next/bundle-analyzer')({
     enabled: process.env.ANALYZE === 'true',
 });
-const withWorkbox = require('@ente-io/next-with-workbox');
 
 const { withSentryConfig } = require('@sentry/nextjs');
 const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
@@ -19,7 +18,6 @@ const {
     COOP_COEP_HEADERS,
     WEB_SECURITY_HEADERS,
     CSP_DIRECTIVES,
-    WORKBOX_CONFIG,
     ALL_ROUTES,
     getIsSentryEnabled,
 } = require('./configUtil');
@@ -30,37 +28,39 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
 
 module.exports = (phase) =>
     withSentryConfig(
-        withWorkbox(
-            withBundleAnalyzer(
-                withTM({
-                    env: {
-                        SENTRY_RELEASE: GIT_SHA,
-                        NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA,
+        withBundleAnalyzer(
+            withTM({
+                compiler: {
+                    styledComponents: {
+                        ssr: true,
+                        displayName: true,
                     },
-                    workbox: WORKBOX_CONFIG,
+                },
+                env: {
+                    SENTRY_RELEASE: GIT_SHA,
+                },
 
-                    headers() {
-                        return [
-                            {
-                                // Apply these headers to all routes in your application....
-                                source: ALL_ROUTES,
-                                headers: convertToNextHeaderFormat({
-                                    ...COOP_COEP_HEADERS,
-                                    ...WEB_SECURITY_HEADERS,
-                                    ...buildCSPHeader(CSP_DIRECTIVES),
-                                }),
-                            },
-                        ];
-                    },
-                    // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
-                    webpack: (config, { isServer }) => {
-                        if (!isServer) {
-                            config.resolve.fallback.fs = false;
-                        }
-                        return config;
-                    },
-                })
-            )
+                headers() {
+                    return [
+                        {
+                            // Apply these headers to all routes in your application....
+                            source: ALL_ROUTES,
+                            headers: convertToNextHeaderFormat({
+                                ...COOP_COEP_HEADERS,
+                                ...WEB_SECURITY_HEADERS,
+                                ...buildCSPHeader(CSP_DIRECTIVES),
+                            }),
+                        },
+                    ];
+                },
+                // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
+                webpack: (config, { isServer }) => {
+                    if (!isServer) {
+                        config.resolve.fallback.fs = false;
+                    }
+                    return config;
+                },
+            })
         ),
         {
             release: GIT_SHA,

+ 12 - 25
package.json

@@ -5,8 +5,7 @@
   "scripts": {
     "dev": "next dev",
     "albums": "next dev -p 3002",
-    "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
-    "prebuild": "yarn lint",
+    "lint": "next lint",
     "build": "next build",
     "postbuild": "next export",
     "build-analyze": "ANALYZE=true next build",
@@ -15,7 +14,6 @@
   },
   "dependencies": {
     "@date-io/date-fns": "^2.14.0",
-    "@ente-io/next-with-workbox": "^1.0.3",
     "@mui/icons-material": "^5.6.2",
     "@mui/material": "^5.6.2",
     "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
@@ -53,16 +51,16 @@
     "libsodium-wrappers": "^0.7.8",
     "localforage": "^1.9.0",
     "ml-matrix": "^6.8.2",
-    "next": "^12.1.0",
-    "next-transpile-modules": "^9.0.0",
+    "next": "^13.0.6",
+    "next-transpile-modules": "^10.0.0",
     "p-queue": "^7.1.0",
     "photoswipe": "file:./thirdparty/photoswipe",
     "piexifjs": "^1.0.6",
-    "react": "^17.0.2",
+    "react": "^18.2.0",
     "react-bootstrap": "^1.3.0",
     "react-d3-tree": "^3.1.1",
     "react-datepicker": "^4.3.0",
-    "react-dom": "^17.0.2",
+    "react-dom": "^18.2.0",
     "react-dropzone": "^11.2.4",
     "react-otp-input": "^2.3.1",
     "react-select": "^4.3.1",
@@ -70,6 +68,7 @@
     "react-top-loading-bar": "^2.0.1",
     "react-virtualized-auto-sizer": "^1.0.2",
     "react-window": "^1.8.6",
+    "sanitize-filename": "^1.6.3",
     "similarity-transformation": "^0.0.1",
     "styled-components": "^5.3.5",
     "tesseract.js": "file:./thirdparty/tesseract",
@@ -85,6 +84,7 @@
   },
   "devDependencies": {
     "@next/bundle-analyzer": "^9.5.3",
+    "@types/bs58": "^4.0.1",
     "@types/debounce-promise": "^3.1.3",
     "@types/libsodium-wrappers": "^0.7.8",
     "@types/node": "^14.6.4",
@@ -98,17 +98,10 @@
     "@types/styled-components": "^5.1.25",
     "@types/wicg-file-system-access": "^2020.9.5",
     "@types/yup": "^0.29.7",
-    "@typescript-eslint/eslint-plugin": "^4.25.0",
-    "@typescript-eslint/parser": "^4.25.0",
-    "babel-plugin-styled-components": "^1.11.1",
-    "eslint": "^7.27.0",
-    "eslint-config-airbnb": "^18.2.1",
-    "eslint-config-google": "^0.14.0",
-    "eslint-config-prettier": "^8.3.0",
-    "eslint-plugin-import": "^2.23.3",
-    "eslint-plugin-jsx-a11y": "^6.4.1",
-    "eslint-plugin-react": "^7.23.2",
-    "eslint-plugin-react-hooks": "^4.2.0",
+    "@typescript-eslint/eslint-plugin": "^5.43.0",
+    "eslint": "^8.28.0",
+    "eslint-config-next": "^13.0.6",
+    "eslint-config-prettier": "^8.5.0",
     "husky": "^7.0.1",
     "lint-staged": "^11.1.2",
     "prettier": "2.3.2",
@@ -117,13 +110,7 @@
   "standard": {
     "parser": "babel-eslint"
   },
-  "lint-staged": {
-    "src/**/*.{js,jsx,ts,tsx}": [
-      "eslint --fix",
-      "prettier --write --ignore-unknown"
-    ]
-  },
   "resolutions": {
     "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
   }
-}
+}

+ 1 - 1
public/_headers

@@ -8,5 +8,5 @@
     X-Frame-Options: deny
     X-XSS-Protection: 1; mode=block
     Referrer-Policy: same-origin
-    Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
+    Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
     

+ 6 - 1
sentry.client.config.js

@@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV();
 const SENTRY_RELEASE = getSentryRelease();
 const IS_ENABLED = getIsSentryEnabled();
 
-Sentry.setUser({ id: getSentryUserID() });
 Sentry.init({
     dsn: SENTRY_DSN,
     enabled: IS_ENABLED,
@@ -39,3 +38,9 @@ Sentry.init({
     // `release` value here - use the environment variable `SENTRY_RELEASE`, so
     // that it will also get attached to your source maps
 });
+
+const main = async () => {
+    Sentry.setUser({ id: await getSentryUserID() });
+};
+
+main();

+ 8 - 0
sentry.server.config.js

@@ -6,6 +6,8 @@ import {
     getIsSentryEnabled,
 } from 'constants/sentry';
 
+import { getSentryUserID } from 'utils/user';
+
 const SENTRY_DSN = getSentryDSN();
 const SENTRY_ENV = getSentryENV();
 const SENTRY_RELEASE = getSentryRelease();
@@ -18,3 +20,9 @@ Sentry.init({
     release: SENTRY_RELEASE,
     autoSessionTracking: false,
 });
+
+const main = async () => {
+    Sentry.setUser({ id: await getSentryUserID() });
+};
+
+main();

+ 7 - 6
sentryConfigUtil.js

@@ -1,10 +1,11 @@
+const ENV_DEVELOPMENT = 'development';
+
 module.exports.getIsSentryEnabled = () => {
-    if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) {
-        return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes';
+    if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
+        return false;
+    } else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
+        return false;
     } else {
-        if (process.env.NEXT_PUBLIC_SENTRY_ENV) {
-            return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development';
-        }
+        return true;
     }
-    return false;
 };

+ 3 - 2
src/components/Badge.tsx

@@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
 
 export const Badge = styled(Paper)(({ theme }) => ({
     padding: '2px 4px',
-    backgroundColor: theme.palette.glass.main,
-    color: theme.palette.glass.contrastText,
+    backgroundColor: theme.palette.backdrop.main,
+    backdropFilter: `blur(${theme.palette.blur.muted})`,
+    color: theme.palette.primary.contrastText,
     textTransform: 'uppercase',
     ...(theme.typography.mini as CSSProperties),
 }));

+ 10 - 0
src/components/Chip.tsx

@@ -0,0 +1,10 @@
+import { Box, styled } from '@mui/material';
+import { CSSProperties } from 'react';
+
+export const Chip = styled(Box)(({ theme }) => ({
+    ...(theme.typography.body2 as CSSProperties),
+    padding: '8px 12px',
+    borderRadius: '4px',
+    backgroundColor: theme.palette.fill.dark,
+    fontWeight: 'bold',
+}));

+ 32 - 9
src/components/CodeBlock/CopyButton.tsx

@@ -1,20 +1,43 @@
-import React from 'react';
+import React, { useState } from 'react';
 import constants from 'utils/strings/constants';
-import { CopyButtonWrapper } from './styledComponents';
 import DoneIcon from '@mui/icons-material/Done';
 import ContentCopyIcon from '@mui/icons-material/ContentCopy';
-import { Tooltip } from '@mui/material';
+import {
+    IconButton,
+    IconButtonProps,
+    SvgIconProps,
+    Tooltip,
+} from '@mui/material';
 
-export default function CopyButton({ code, copied, copyToClipboardHelper }) {
+export default function CopyButton({
+    code,
+    color,
+    size,
+}: {
+    code: string;
+    color?: IconButtonProps['color'];
+    size?: SvgIconProps['fontSize'];
+}) {
+    const [copied, setCopied] = useState<boolean>(false);
+
+    const copyToClipboardHelper = (text: string) => () => {
+        navigator.clipboard.writeText(text);
+        setCopied(true);
+        setTimeout(() => setCopied(false), 1000);
+    };
     return (
-        <Tooltip arrow open={copied} title={constants.COPIED}>
-            <CopyButtonWrapper onClick={copyToClipboardHelper(code)}>
+        <Tooltip
+            arrow
+            open={copied}
+            title={constants.COPIED}
+            PopperProps={{ sx: { zIndex: 2000 } }}>
+            <IconButton onClick={copyToClipboardHelper(code)} color={color}>
                 {copied ? (
-                    <DoneIcon fontSize="small" />
+                    <DoneIcon fontSize={size ?? 'small'} />
                 ) : (
-                    <ContentCopyIcon fontSize="small" />
+                    <ContentCopyIcon fontSize={size ?? 'small'} />
                 )}
-            </CopyButtonWrapper>
+            </IconButton>
         </Tooltip>
     );
 }

+ 5 - 15
src/components/CodeBlock/index.tsx

@@ -1,7 +1,7 @@
 import { FreeFlowText } from '../Container';
-import React, { useState } from 'react';
+import React from 'react';
 import EnteSpinner from '../EnteSpinner';
-import { Wrapper, CodeWrapper } from './styledComponents';
+import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
 import CopyButton from './CopyButton';
 import { BoxProps } from '@mui/material';
 
@@ -15,14 +15,6 @@ export default function CodeBlock({
     wordBreak,
     ...props
 }: BoxProps<'div', Iprops>) {
-    const [copied, setCopied] = useState<boolean>(false);
-
-    const copyToClipboardHelper = (text: string) => () => {
-        navigator.clipboard.writeText(text);
-        setCopied(true);
-        setTimeout(() => setCopied(false), 1000);
-    };
-
     if (!code) {
         return (
             <Wrapper>
@@ -37,11 +29,9 @@ export default function CodeBlock({
                     {code}
                 </FreeFlowText>
             </CodeWrapper>
-            <CopyButton
-                code={code}
-                copied={copied}
-                copyToClipboardHelper={copyToClipboardHelper}
-            />
+            <CopyButtonWrapper>
+                <CopyButton code={code} />
+            </CopyButtonWrapper>
         </Wrapper>
     );
 }

+ 0 - 3
src/components/Collections/AllCollections/CollectionSort/options.tsx

@@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) {
             <SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
                 {constants.SORT_BY_NAME}
             </SortByOption>
-            <SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
-                {constants.SORT_BY_CREATION_TIME_DESCENDING}
-            </SortByOption>
             <SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
                 {constants.SORT_BY_CREATION_TIME_ASCENDING}
             </SortByOption>

+ 2 - 2
src/components/Collections/CollectionInfoWithOptions.tsx

@@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents';
 import { shouldShowOptions } from 'utils/collection';
 import { CollectionSummaryType } from 'constants/collection';
 import Favorite from '@mui/icons-material/FavoriteRounded';
-import VisibilityOff from '@mui/icons-material/VisibilityOff';
 import Delete from '@mui/icons-material/Delete';
+import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
 
 interface Iprops {
     activeCollection: Collection;
@@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({
                 return <Favorite />;
             case CollectionSummaryType.archived:
             case CollectionSummaryType.archive:
-                return <VisibilityOff />;
+                return <ArchiveOutlined />;
             case CollectionSummaryType.trash:
                 return <Delete />;
             default:

+ 4 - 2
src/components/Collections/CollectionListBar/CollectionCard.tsx

@@ -11,7 +11,8 @@ import TruncateText from 'components/TruncateText';
 import { Box } from '@mui/material';
 import { CollectionSummaryType } from 'constants/collection';
 import Favorite from '@mui/icons-material/FavoriteRounded';
-import VisibilityOff from '@mui/icons-material/VisibilityOff';
+import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
+import PeopleIcon from '@mui/icons-material/People';
 
 interface Iprops {
     active: boolean;
@@ -50,8 +51,9 @@ function CollectionCardIcon({ collectionType }) {
         <CollectionBarTileIcon>
             {collectionType === CollectionSummaryType.favorites && <Favorite />}
             {collectionType === CollectionSummaryType.archived && (
-                <VisibilityOff />
+                <ArchiveOutlined />
             )}
+            {collectionType === CollectionSummaryType.shared && <PeopleIcon />}
         </CollectionBarTileIcon>
     );
 }

+ 4 - 4
src/components/Collections/CollectionOptions/AlbumCollectionOption.tsx

@@ -4,11 +4,11 @@ import React from 'react';
 import EditIcon from '@mui/icons-material/Edit';
 import IosShareIcon from '@mui/icons-material/IosShare';
 import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
-import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
-import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
 import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
 import constants from 'utils/strings/constants';
 import { CollectionActions } from '.';
+import Unarchive from '@mui/icons-material/Unarchive';
+import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
 
 interface Iprops {
     IsArchived: boolean;
@@ -53,13 +53,13 @@ export function AlbumCollectionOption({
                     onClick={handleCollectionAction(
                         CollectionActions.UNARCHIVE
                     )}
-                    startIcon={<VisibilityOnOutlinedIcon />}>
+                    startIcon={<Unarchive />}>
                     {constants.UNARCHIVE}
                 </OverflowMenuOption>
             ) : (
                 <OverflowMenuOption
                     onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
-                    startIcon={<VisibilityOffOutlinedIcon />}>
+                    startIcon={<ArchiveOutlined />}>
                     {constants.ARCHIVE}
                 </OverflowMenuOption>
             )}

+ 25 - 0
src/components/Collections/CollectionOptions/SharedCollectionOption.tsx

@@ -0,0 +1,25 @@
+import { OverflowMenuOption } from 'components/OverflowMenu/option';
+import React from 'react';
+import LogoutIcon from '@mui/icons-material/Logout';
+import constants from 'utils/strings/constants';
+import { CollectionActions } from '.';
+
+interface Iprops {
+    handleCollectionAction: (
+        action: CollectionActions,
+        loader?: boolean
+    ) => (...args: any[]) => Promise<void>;
+}
+
+export function SharedCollectionOption({ handleCollectionAction }: Iprops) {
+    return (
+        <OverflowMenuOption
+            startIcon={<LogoutIcon />}
+            onClick={handleCollectionAction(
+                CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM,
+                false
+            )}>
+            {constants.LEAVE_ALBUM}
+        </OverflowMenuOption>
+    );
+}

+ 35 - 0
src/components/Collections/CollectionOptions/index.tsx

@@ -17,6 +17,7 @@ import { AppContext } from 'pages/_app';
 import OverflowMenu from 'components/OverflowMenu/menu';
 import { CollectionSummaryType } from 'constants/collection';
 import { TrashCollectionOption } from './TrashCollectionOption';
+import { SharedCollectionOption } from './SharedCollectionOption';
 import MoreHoriz from '@mui/icons-material/MoreHoriz';
 
 interface CollectionOptionsProps {
@@ -39,6 +40,8 @@ export enum CollectionActions {
     SHOW_SHARE_DIALOG,
     CONFIRM_EMPTY_TRASH,
     EMPTY_TRASH,
+    CONFIRM_LEAVE_SHARED_ALBUM,
+    LEAVE_SHARED_ALBUM,
 }
 
 const CollectionOptions = (props: CollectionOptionsProps) => {
@@ -93,6 +96,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
             case CollectionActions.EMPTY_TRASH:
                 callback = emptyTrash;
                 break;
+            case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
+                callback = confirmLeaveSharedAlbum;
+                break;
+            case CollectionActions.LEAVE_SHARED_ALBUM:
+                callback = leaveSharedAlbum;
+                break;
             default:
                 logError(
                     Error('invalid collection action '),
@@ -130,6 +139,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
         redirectToAll();
     };
 
+    const leaveSharedAlbum = async () => {
+        await CollectionAPI.leaveSharedAlbum(activeCollection.id);
+        redirectToAll();
+    };
+
     const archiveCollection = () => {
         changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
     };
@@ -200,6 +214,23 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
             close: { text: constants.CANCEL },
         });
 
+    const confirmLeaveSharedAlbum = () => {
+        setDialogMessage({
+            title: constants.LEAVE_SHARED_ALBUM_TITLE,
+            content: constants.LEAVE_SHARED_ALBUM_MESSAGE,
+            proceed: {
+                text: constants.LEAVE_SHARED_ALBUM,
+                action: handleCollectionAction(
+                    CollectionActions.LEAVE_SHARED_ALBUM
+                ),
+                variant: 'danger',
+            },
+            close: {
+                text: constants.CANCEL,
+            },
+        });
+    };
+
     return (
         <OverflowMenu
             ariaControls={'collection-options'}
@@ -213,6 +244,10 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
                 <TrashCollectionOption
                     handleCollectionAction={handleCollectionAction}
                 />
+            ) : collectionSummaryType === CollectionSummaryType.shared ? (
+                <SharedCollectionOption
+                    handleCollectionAction={handleCollectionAction}
+                />
             ) : (
                 <AlbumCollectionOption
                     IsArchived={IsArchived(activeCollection)}

+ 8 - 4
src/components/Collections/CollectionSelector/index.tsx

@@ -14,11 +14,12 @@ export interface CollectionSelectorAttributes {
     showNextModal: () => void;
     title: string;
     fromCollection?: number;
+    onCancel?: () => void;
 }
 
 interface Props {
     open: boolean;
-    onClose: (closeBtnClick?: boolean) => void;
+    onClose: () => void;
     attributes: CollectionSelectorAttributes;
     collections: Collection[];
     collectionSummaries: CollectionSummaries;
@@ -61,15 +62,18 @@ function CollectionSelector({
         props.onClose();
     };
 
-    const onCloseButtonClick = () => props.onClose(true);
+    const onUserTriggeredClose = () => {
+        attributes.onCancel?.();
+        props.onClose();
+    };
 
     return (
         <AllCollectionDialog
-            onClose={props.onClose}
+            onClose={onUserTriggeredClose}
             open={props.open}
             position="center"
             fullScreen={appContext.isMobile}>
-            <DialogTitleWithCloseButton onClose={onCloseButtonClick}>
+            <DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
                 {attributes.title}
             </DialogTitleWithCloseButton>
             <DialogContent>

+ 1 - 1
src/components/Collections/CollectionShare/emailShare.tsx

@@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
 import React, { useContext } from 'react';
 import { shareCollection } from 'services/collectionService';
 import { User } from 'types/user';
-import { handleSharingErrors } from 'utils/error';
+import { handleSharingErrors } from 'utils/error/ui';
 import { getData, LS_KEYS } from 'utils/storage/localStorage';
 import constants from 'utils/strings/constants';
 import { CollectionShareSharees } from './sharees';

+ 2 - 3
src/components/Collections/CollectionShare/publicShare/control.tsx

@@ -1,6 +1,5 @@
 import { Box, Typography } from '@mui/material';
 import { FlexWrapper } from 'components/Container';
-import { ButtonVariant } from 'components/pages/gallery/LinkButton';
 import { AppContext } from 'pages/_app';
 import React, { useContext, useState } from 'react';
 import {
@@ -8,7 +7,7 @@ import {
     deleteShareableURL,
 } from 'services/collectionService';
 import { Collection, PublicURL } from 'types/collection';
-import { handleSharingErrors } from 'utils/error';
+import { handleSharingErrors } from 'utils/error/ui';
 import constants from 'utils/strings/constants';
 import PublicShareSwitch from './switch';
 interface Iprops {
@@ -60,7 +59,7 @@ export default function PublicShareControl({
             proceed: {
                 text: constants.DISABLE,
                 action: disablePublicSharing,
-                variant: ButtonVariant.danger,
+                variant: 'danger',
             },
         });
     };

+ 8 - 1
src/components/Collections/CollectionShare/publicShare/manage/deviceLimit.tsx

@@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material';
 import React from 'react';
 import Select from 'react-select';
 import { DropdownStyle } from 'styles/dropdown';
+import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
 import { getDeviceLimitOptions } from 'utils/collection';
 import constants from 'utils/strings/constants';
 import { OptionWithDivider } from './selectComponents/OptionWithDivider';
 
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
+}
+
 export function ManageDeviceLimit({
     publicShareProp,
     collection,
     updatePublicShareURLHelper,
-}) {
+}: Iprops) {
     const updateDeviceLimit = async (newLimit: number) => {
         return updatePublicShareURLHelper({
             collectionID: collection.id,

+ 11 - 4
src/components/Collections/CollectionShare/publicShare/manage/downloadAccess.tsx

@@ -1,14 +1,21 @@
 import { Box, Typography } from '@mui/material';
-import { ButtonVariant } from 'components/pages/gallery/LinkButton';
 import { AppContext } from 'pages/_app';
 import React, { useContext } from 'react';
+import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
 import constants from 'utils/strings/constants';
 import PublicShareSwitch from '../switch';
+
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
+}
+
 export function ManageDownloadAccess({
     publicShareProp,
     updatePublicShareURLHelper,
     collection,
-}) {
+}: Iprops) {
     const appContext = useContext(AppContext);
 
     const handleFileDownloadSetting = () => {
@@ -34,7 +41,7 @@ export function ManageDownloadAccess({
                         collectionID: collection.id,
                         enableDownload: false,
                     }),
-                variant: ButtonVariant.danger,
+                variant: 'danger',
             },
         });
     };
@@ -42,7 +49,7 @@ export function ManageDownloadAccess({
         <Box>
             <Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
             <PublicShareSwitch
-                checked={publicShareProp?.enableDownload ?? false}
+                checked={publicShareProp?.enableDownload ?? true}
                 onChange={handleFileDownloadSetting}
             />
         </Box>

+ 18 - 16
src/components/Collections/CollectionShare/publicShare/manage/index.tsx

@@ -1,33 +1,37 @@
 import { ManageLinkPassword } from './linkPassword';
 import { ManageDeviceLimit } from './deviceLimit';
 import { ManageLinkExpiry } from './linkExpiry';
-import { PublicLinkSetPassword } from '../setPassword';
 import { Stack, Typography } from '@mui/material';
 import { GalleryContext } from 'pages/gallery';
 import React, { useContext, useState } from 'react';
 import { updateShareableURL } from 'services/collectionService';
-import { UpdatePublicURL } from 'types/collection';
+import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
 import { sleep } from 'utils/common';
-import { handleSharingErrors } from 'utils/error';
 import constants from 'utils/strings/constants';
 import {
     ManageSectionLabel,
     ManageSectionOptions,
 } from '../../styledComponents';
 import { ManageDownloadAccess } from './downloadAccess';
+import { handleSharingErrors } from 'utils/error/ui';
+import { SetPublicShareProp } from 'types/publicCollection';
+import { ManagePublicCollect } from './publicCollect';
+
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    setPublicShareProp: SetPublicShareProp;
+}
 
 export default function PublicShareManage({
     publicShareProp,
     collection,
     setPublicShareProp,
-}) {
+}: Iprops) {
     const galleryContext = useContext(GalleryContext);
 
-    const [changePasswordView, setChangePasswordView] = useState(false);
     const [sharableLinkError, setSharableLinkError] = useState(null);
 
-    const closeConfigurePassword = () => setChangePasswordView(false);
-
     const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
         try {
             galleryContext.setBlockingLoad(true);
@@ -73,6 +77,13 @@ export default function PublicShareManage({
                                 updatePublicShareURLHelper
                             }
                         />
+                        <ManagePublicCollect
+                            collection={collection}
+                            publicShareProp={publicShareProp}
+                            updatePublicShareURLHelper={
+                                updatePublicShareURLHelper
+                            }
+                        />
                         <ManageDownloadAccess
                             collection={collection}
                             publicShareProp={publicShareProp}
@@ -81,7 +92,6 @@ export default function PublicShareManage({
                             }
                         />
                         <ManageLinkPassword
-                            setChangePasswordView={setChangePasswordView}
                             collection={collection}
                             publicShareProp={publicShareProp}
                             updatePublicShareURLHelper={
@@ -102,14 +112,6 @@ export default function PublicShareManage({
                     )}
                 </ManageSectionOptions>
             </details>
-            <PublicLinkSetPassword
-                open={changePasswordView}
-                onClose={closeConfigurePassword}
-                collection={collection}
-                publicShareProp={publicShareProp}
-                updatePublicShareURLHelper={updatePublicShareURLHelper}
-                setChangePasswordView={setChangePasswordView}
-            />
         </>
     );
 }

+ 10 - 3
src/components/Collections/CollectionShare/publicShare/manage/linkExpiry.tsx

@@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material';
 import React from 'react';
 import Select from 'react-select';
 import { linkExpiryStyle } from 'styles/linkExpiry';
+import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
 import { shareExpiryOptions } from 'utils/collection';
 import constants from 'utils/strings/constants';
-import { dateStringWithMMH } from 'utils/time';
+import { formatDateTime } from 'utils/time/format';
 import { OptionWithDivider } from './selectComponents/OptionWithDivider';
 
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
+}
+
 export function ManageLinkExpiry({
     publicShareProp,
     collection,
     updatePublicShareURLHelper,
-}) {
+}: Iprops) {
     const updateDeviceExpiry = async (optionFn) => {
         return updatePublicShareURLHelper({
             collectionID: collection.id,
@@ -31,7 +38,7 @@ export function ManageLinkExpiry({
                 }}
                 placeholder={
                     publicShareProp?.validTill
-                        ? dateStringWithMMH(publicShareProp?.validTill)
+                        ? formatDateTime(publicShareProp?.validTill / 1000)
                         : 'never'
                 }
                 onChange={(e) => {

+ 0 - 49
src/components/Collections/CollectionShare/publicShare/manage/linkPassword.tsx

@@ -1,49 +0,0 @@
-import { Box, Typography } from '@mui/material';
-import { ButtonVariant } from 'components/pages/gallery/LinkButton';
-import { AppContext } from 'pages/_app';
-import React, { useContext } from 'react';
-import constants from 'utils/strings/constants';
-import PublicShareSwitch from '../switch';
-export function ManageLinkPassword({
-    collection,
-    publicShareProp,
-    updatePublicShareURLHelper,
-    setChangePasswordView,
-}) {
-    const appContext = useContext(AppContext);
-
-    const handlePasswordChangeSetting = async () => {
-        if (publicShareProp.passwordEnabled) {
-            await confirmDisablePublicUrlPassword();
-        } else {
-            setChangePasswordView(true);
-        }
-    };
-
-    const confirmDisablePublicUrlPassword = async () => {
-        appContext.setDialogMessage({
-            title: constants.DISABLE_PASSWORD,
-            content: constants.DISABLE_PASSWORD_MESSAGE,
-            close: { text: constants.CANCEL },
-            proceed: {
-                text: constants.DISABLE,
-                action: () =>
-                    updatePublicShareURLHelper({
-                        collectionID: collection.id,
-                        disablePassword: true,
-                    }),
-                variant: ButtonVariant.danger,
-            },
-        });
-    };
-
-    return (
-        <Box>
-            <Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
-            <PublicShareSwitch
-                checked={!!publicShareProp?.passwordEnabled}
-                onChange={handlePasswordChangeSetting}
-            />
-        </Box>
-    );
-}

+ 72 - 0
src/components/Collections/CollectionShare/publicShare/manage/linkPassword/index.tsx

@@ -0,0 +1,72 @@
+import { Box, Typography } from '@mui/material';
+import { AppContext } from 'pages/_app';
+import React, { useContext, useState } from 'react';
+import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
+import constants from 'utils/strings/constants';
+import { PublicLinkSetPassword } from './setPassword';
+import PublicShareSwitch from '../../switch';
+
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
+}
+
+export function ManageLinkPassword({
+    collection,
+    publicShareProp,
+    updatePublicShareURLHelper,
+}: Iprops) {
+    const appContext = useContext(AppContext);
+    const [changePasswordView, setChangePasswordView] = useState(false);
+
+    const closeConfigurePassword = () => setChangePasswordView(false);
+
+    const handlePasswordChangeSetting = async () => {
+        if (publicShareProp.passwordEnabled) {
+            await confirmDisablePublicUrlPassword();
+        } else {
+            setChangePasswordView(true);
+        }
+    };
+
+    const confirmDisablePublicUrlPassword = async () => {
+        appContext.setDialogMessage({
+            title: constants.DISABLE_PASSWORD,
+            content: constants.DISABLE_PASSWORD_MESSAGE,
+            close: { text: constants.CANCEL },
+            proceed: {
+                text: constants.DISABLE,
+                action: () =>
+                    updatePublicShareURLHelper({
+                        collectionID: collection.id,
+                        disablePassword: true,
+                    }),
+                variant: 'danger',
+            },
+        });
+    };
+
+    return (
+        <>
+            <Box>
+                <Typography mb={0.5}>
+                    {' '}
+                    {constants.LINK_PASSWORD_LOCK}
+                </Typography>
+                <PublicShareSwitch
+                    checked={!!publicShareProp?.passwordEnabled}
+                    onChange={handlePasswordChangeSetting}
+                />
+            </Box>
+            <PublicLinkSetPassword
+                open={changePasswordView}
+                onClose={closeConfigurePassword}
+                collection={collection}
+                publicShareProp={publicShareProp}
+                updatePublicShareURLHelper={updatePublicShareURLHelper}
+                setChangePasswordView={setChangePasswordView}
+            />
+        </>
+    );
+}

+ 3 - 3
src/components/Collections/CollectionShare/publicShare/setPassword.tsx → src/components/Collections/CollectionShare/publicShare/manage/linkPassword/setPassword.tsx

@@ -3,7 +3,7 @@ import SingleInputForm, {
     SingleInputFormProps,
 } from 'components/SingleInputForm';
 import React from 'react';
-import CryptoWorker from 'utils/crypto';
+import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
 import constants from 'utils/strings/constants';
 
 export function PublicLinkSetPassword({
@@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
     };
 
     const enablePublicUrlPassword = async (password: string) => {
-        const cryptoWorker = await new CryptoWorker();
-        const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
+        const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+        const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
         const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
 
         return updatePublicShareURLHelper({

+ 34 - 0
src/components/Collections/CollectionShare/publicShare/manage/publicCollect.tsx

@@ -0,0 +1,34 @@
+import { Box, Typography } from '@mui/material';
+import React from 'react';
+import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
+import constants from 'utils/strings/constants';
+import PublicShareSwitch from '../switch';
+
+interface Iprops {
+    publicShareProp: PublicURL;
+    collection: Collection;
+    updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
+}
+
+export function ManagePublicCollect({
+    publicShareProp,
+    updatePublicShareURLHelper,
+    collection,
+}: Iprops) {
+    const handleFileDownloadSetting = () => {
+        updatePublicShareURLHelper({
+            collectionID: collection.id,
+            enableCollect: !publicShareProp.enableCollect,
+        });
+    };
+
+    return (
+        <Box>
+            <Typography mb={0.5}>{constants.PUBLIC_COLLECT}</Typography>
+            <PublicShareSwitch
+                checked={publicShareProp?.enableCollect}
+                onChange={handleFileDownloadSetting}
+            />
+        </Box>
+    );
+}

+ 1 - 1
src/components/Collections/index.tsx

@@ -96,7 +96,7 @@ export default function Collections(props: Iprops) {
             itemType: ITEM_TYPE.OTHER,
             height: 68,
         });
-    }, [collectionSummaries, activeCollectionID]);
+    }, [collectionSummaries, activeCollectionID, isInSearchMode]);
 
     if (shouldBeHidden) {
         return <></>;

+ 0 - 39
src/components/DeleteBtn.tsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import { styled } from '@mui/material';
-import constants from 'utils/strings/constants';
-import { IconWithMessage } from './IconWithMessage';
-
-const Wrapper = styled('button')`
-    border: none;
-    background-color: #ff6666;
-    position: fixed;
-    z-index: 1;
-    bottom: 30px;
-    right: 30px;
-    width: 60px;
-    height: 60px;
-    border-radius: 50%;
-    color: #fff;
-`;
-export default function DeleteBtn(props) {
-    return (
-        <IconWithMessage message={constants.EMPTY_TRASH}>
-            <Wrapper onClick={props.onClick}>
-                <svg
-                    xmlns="http://www.w3.org/2000/svg"
-                    height={props.height}
-                    viewBox={props.viewBox}
-                    width={props.width}>
-                    <path d="M0 0h24v24H0z" fill="none" />
-                    <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
-                </svg>
-            </Wrapper>
-        </IconWithMessage>
-    );
-}
-
-DeleteBtn.defaultProps = {
-    height: 24,
-    width: 24,
-    viewBox: '0 0 24 24',
-};

+ 18 - 0
src/components/DialogBox/DialogIcon.tsx

@@ -0,0 +1,18 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
+    return (
+        <Box
+            className="DialogIcon"
+            sx={{
+                svg: {
+                    width: '48px',
+                    height: '48px',
+                },
+                color: 'stroke.secondary',
+            }}>
+            {icon}
+        </Box>
+    );
+}

+ 11 - 0
src/components/DialogBox/base.tsx

@@ -5,6 +5,12 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
         padding: theme.spacing(1, 1.5),
         maxWidth: '346px',
     },
+
+    '& .DialogIcon': {
+        padding: theme.spacing(2),
+        paddingBottom: theme.spacing(1),
+    },
+
     '& .MuiDialogTitle-root': {
         padding: theme.spacing(2),
         paddingBottom: theme.spacing(1),
@@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
     '& .MuiDialogContent-root': {
         padding: theme.spacing(2),
     },
+
+    '.DialogIcon + .MuiDialogTitle-root': {
+        paddingTop: 0,
+    },
+
     '.MuiDialogTitle-root + .MuiDialogContent-root': {
         paddingTop: 0,
     },

+ 2 - 0
src/components/DialogBox/index.tsx

@@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, {
 } from './TitleWithCloseButton';
 import DialogBoxBase from './base';
 import { DialogBoxAttributes } from 'types/dialogBox';
+import DialogIcon from './DialogIcon';
 
 type IProps = React.PropsWithChildren<
     Omit<DialogProps, 'onClose' | 'maxSize'> & {
@@ -48,6 +49,7 @@ export default function DialogBox({
             maxWidth={size}
             onClose={handleClose}
             {...props}>
+            {attributes.icon && <DialogIcon icon={attributes.icon} />}
             {attributes.title && (
                 <DialogTitleWithCloseButton
                     onClose={

+ 1 - 9
src/components/EnteDateTimePicker.tsx

@@ -4,7 +4,6 @@ import {
     MIN_EDITED_CREATION_TIME,
     MAX_EDITED_CREATION_TIME,
 } from 'constants/file';
-import { TextField } from '@mui/material';
 import {
     LocalizationProvider,
     MobileDateTimePicker,
@@ -60,14 +59,7 @@ const EnteDateTimePicker = ({
                         },
                     },
                 }}
-                renderInput={(params) => (
-                    <TextField
-                        {...params}
-                        hiddenLabel
-                        margin="none"
-                        variant="standard"
-                    />
-                )}
+                renderInput={() => <></>}
             />
         </LocalizationProvider>
     );

+ 11 - 0
src/components/EnteDrawer.tsx

@@ -0,0 +1,11 @@
+import { Drawer } from '@mui/material';
+import styled from 'styled-components';
+
+export const EnteDrawer = styled(Drawer)(({ theme }) => ({
+    '& .MuiPaper-root': {
+        maxWidth: '375px',
+        width: '100%',
+        scrollbarWidth: 'thin',
+        padding: theme.spacing(1),
+    },
+}));

+ 1 - 1
src/components/ExportFinished.tsx

@@ -1,8 +1,8 @@
 import { Button, DialogActions, DialogContent, Stack } from '@mui/material';
 import React from 'react';
 import { ExportStats } from 'types/export';
-import { formatDateTime } from 'utils/time';
 import constants from 'utils/strings/constants';
+import { formatDateTime } from 'utils/time/format';
 import { FlexWrapper, Label, Value } from './Container';
 import { ComfySpan } from './ExportInProgress';
 

+ 0 - 1
src/components/FixCreationTime/index.tsx

@@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
             <div
                 style={{
                     marginBottom: '10px',
-                    padding: '0 5%',
                     display: 'flex',
                     flexDirection: 'column',
                     ...(fixState === FIX_STATE.RUNNING

+ 0 - 1
src/components/FixCreationTime/options.tsx

@@ -23,7 +23,6 @@ const Option = ({
             color: value !== Number(selected) ? '#aaa' : '#fff',
         }}>
         <Form.Check.Input
-            style={{ marginTop: '6px' }}
             id={value.toString()}
             type="radio"
             value={value}

+ 0 - 19
src/components/IconWithMessage.tsx

@@ -1,19 +0,0 @@
-import { OverlayTrigger, Tooltip } from 'react-bootstrap';
-import React from 'react';
-
-interface IconWithMessageProps {
-    children?: any;
-    message: string;
-}
-
-export const IconWithMessage = (props: IconWithMessageProps) => (
-    <OverlayTrigger
-        placement="bottom"
-        overlay={
-            <Tooltip id="on-hover-info" style={{ zIndex: 1002 }}>
-                {props.message}
-            </Tooltip>
-        }>
-        {props.children}
-    </OverlayTrigger>
-);

+ 22 - 0
src/components/Navbar/EnteLinkLogo.tsx

@@ -0,0 +1,22 @@
+import { Box } from '@mui/material';
+import Ente from 'components/icons/ente';
+import Link from 'next/link';
+import { ENTE_WEBSITE_LINK } from 'constants/urls';
+
+export function EnteLinkLogo() {
+    return (
+        <Link href={ENTE_WEBSITE_LINK}>
+            <Box
+                sx={(theme) => ({
+                    ':hover': {
+                        cursor: 'pointer',
+                        svg: {
+                            fill: theme.palette.text.secondary,
+                        },
+                    },
+                })}>
+                <Ente />
+            </Box>
+        </Link>
+    );
+}

+ 30 - 25
src/components/Notification.tsx

@@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
     };
 
     const handleClick = () => {
-        attributes.action?.callback();
+        attributes.onClick();
         onClose();
     };
     return (
@@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
             anchorOrigin={{
                 horizontal: 'right',
                 vertical: 'bottom',
-            }}>
+            }}
+            sx={{ backgroundColor: '#000', width: '320px' }}>
             <Paper
                 component={Button}
                 color={attributes.variant}
                 onClick={handleClick}
                 sx={{
                     textAlign: 'left',
-                    width: '320px',
+                    flex: '1',
                     padding: (theme) => theme.spacing(1.5, 2),
                 }}>
                 <Stack
@@ -55,34 +56,38 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
                     spacing={2}
                     direction="row"
                     alignItems={'center'}>
-                    <Box>
-                        {attributes?.icon ?? <InfoIcon fontSize="large" />}
+                    <Box sx={{ svg: { fontSize: '36px' } }}>
+                        {attributes.startIcon ?? <InfoIcon />}
                     </Box>
-                    <Box sx={{ flex: 1 }}>
-                        <Typography
-                            variant="body2"
-                            color="rgba(255, 255, 255, 0.7)"
-                            mb={0.5}>
-                            {attributes.message}{' '}
-                        </Typography>
-                        {attributes?.action && (
-                            <Typography
-                                mb={0.5}
-                                variant="button"
-                                fontWeight={'bold'}>
-                                {attributes?.action.text}
+
+                    <Stack
+                        direction={'column'}
+                        spacing={0.5}
+                        flex={1}
+                        textAlign="left">
+                        {attributes.subtext && (
+                            <Typography variant="body2">
+                                {attributes.subtext}
                             </Typography>
                         )}
-                    </Box>
-                    <Box>
+                        {attributes.message && (
+                            <Typography variant="button">
+                                {attributes.message}
+                            </Typography>
+                        )}
+                    </Stack>
+
+                    {attributes.endIcon ? (
                         <IconButton
-                            onClick={handleClose}
-                            sx={{
-                                backgroundColor: 'rgba(255, 255, 255, 0.1)',
-                            }}>
+                            onClick={attributes.onClick}
+                            sx={{ fontSize: '36px' }}>
+                            {attributes?.endIcon}
+                        </IconButton>
+                    ) : (
+                        <IconButton onClick={handleClose}>
                             <CloseIcon />
                         </IconButton>
-                    </Box>
+                    )}
                 </Stack>
             </Paper>
         </Snackbar>

+ 172 - 108
src/components/PhotoFrame.tsx

@@ -1,12 +1,12 @@
 import { GalleryContext } from 'pages/gallery';
 import PreviewCard from './pages/gallery/PreviewCard';
-import React, { useContext, useEffect, useRef, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
 import { EnteFile } from 'types/file';
 import { styled } from '@mui/material';
 import DownloadManager from 'services/downloadManager';
 import constants from 'utils/strings/constants';
 import AutoSizer from 'react-virtualized-auto-sizer';
-import PhotoSwipe from 'components/PhotoSwipe';
+import PhotoViewer from 'components/PhotoViewer';
 import {
     ALL_SECTION,
     ARCHIVE_SECTION,
@@ -15,7 +15,7 @@ import {
 import { isSharedFile } from 'utils/file';
 import { isPlaybackPossible } from 'utils/photoFrame';
 import { PhotoList } from './PhotoList';
-import { SetFiles, SelectedState } from 'types/gallery';
+import { SelectedState } from 'types/gallery';
 import { FILE_TYPE } from 'constants/file';
 import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
 import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
@@ -30,6 +30,8 @@ import { logError } from 'utils/sentry';
 import { CustomError } from 'utils/error';
 import { User } from 'types/user';
 import { getData, LS_KEYS } from 'utils/storage/localStorage';
+import { useMemo } from 'react';
+import { Collection } from 'types/collection';
 
 const Container = styled('div')`
     display: block;
@@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
 
 interface Props {
     files: EnteFile[];
-    setFiles: SetFiles;
+    collections?: Collection[];
     syncWithRemote: () => Promise<void>;
     favItemIds?: Set<number>;
     archivedCollections?: Set<number>;
@@ -60,7 +62,8 @@ interface Props {
     openUploader?;
     isInSearchMode?: boolean;
     search?: Search;
-    deleted?: number[];
+    deletedFileIds?: Set<number>;
+    setDeletedFileIds?: (value: Set<number>) => void;
     activeCollection: number;
     isSharedCollection?: boolean;
     enableDownload?: boolean;
@@ -69,13 +72,15 @@ interface Props {
 }
 
 type SourceURL = {
-    imageURL?: string;
-    videoURL?: string;
+    originalImageURL?: string;
+    originalVideoURL?: string;
+    convertedImageURL?: string;
+    convertedVideoURL?: string;
 };
 
 const PhotoFrame = ({
     files,
-    setFiles,
+    collections,
     syncWithRemote,
     favItemIds,
     archivedCollections,
@@ -86,7 +91,8 @@ const PhotoFrame = ({
     isInSearchMode,
     search,
     resetSearch,
-    deleted,
+    deletedFileIds,
+    setDeletedFileIds,
     activeCollection,
     isSharedCollection,
     enableDownload,
@@ -104,75 +110,26 @@ const PhotoFrame = ({
     const [rangeStart, setRangeStart] = useState(null);
     const [currentHover, setCurrentHover] = useState(null);
     const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
-    const filteredDataRef = useRef<EnteFile[]>([]);
-    const filteredData = filteredDataRef?.current ?? [];
     const router = useRouter();
     const [isSourceLoaded, setIsSourceLoaded] = useState(false);
-    useEffect(() => {
-        const handleKeyDown = (e: KeyboardEvent) => {
-            if (e.key === 'Shift') {
-                setIsShiftKeyPressed(true);
-            }
-        };
-        const handleKeyUp = (e: KeyboardEvent) => {
-            if (e.key === 'Shift') {
-                setIsShiftKeyPressed(false);
-            }
-        };
-        document.addEventListener('keydown', handleKeyDown, false);
-        document.addEventListener('keyup', handleKeyUp, false);
-        router.events.on('hashChangeComplete', (url: string) => {
-            const start = url.indexOf('#');
-            const hash = url.slice(start !== -1 ? start : url.length);
-            const shouldPhotoSwipeBeOpened = hash.endsWith(
-                PHOTOSWIPE_HASH_SUFFIX
-            );
-            if (shouldPhotoSwipeBeOpened) {
-                setOpen(true);
-            } else {
-                setOpen(false);
-            }
-        });
-        return () => {
-            document.addEventListener('keydown', handleKeyDown, false);
-            document.addEventListener('keyup', handleKeyUp, false);
-        };
-    }, []);
 
-    useEffect(() => {
-        if (!isNaN(search?.file)) {
-            const filteredDataIdx = filteredData.findIndex((file) => {
-                return file.id === search.file;
-            });
-            if (!isNaN(filteredDataIdx)) {
-                onThumbnailClick(filteredDataIdx)();
-            }
-            resetSearch();
-        }
-    }, [search, filteredData]);
-
-    const resetFetching = () => {
-        setFetching({});
-    };
-
-    useEffect(() => {
-        if (selected.count === 0) {
-            setRangeStart(null);
-        }
-    }, [selected]);
-
-    useEffect(() => {
+    const filteredData = useMemo(() => {
         const idSet = new Set();
         const user: User = getData(LS_KEYS.USER);
-        filteredDataRef.current = files
+
+        return files
             .map((item, index) => ({
                 ...item,
                 dataIndex: index,
                 w: window.innerWidth,
                 h: window.innerHeight,
+                title: item.pubMagicMetadata?.data.caption,
             }))
             .filter((item) => {
-                if (deleted?.includes(item.id)) {
+                if (
+                    deletedFileIds?.has(item.id) &&
+                    activeCollection !== TRASH_SECTION
+                ) {
                     return false;
                 }
                 if (
@@ -236,7 +193,8 @@ const PhotoFrame = ({
                         activeCollection === ALL_SECTION ||
                         activeCollection === ARCHIVE_SECTION ||
                         activeCollection === TRASH_SECTION ||
-                        activeCollection === item.collectionID
+                        activeCollection === item.collectionID ||
+                        isInSearchMode
                     ) {
                         idSet.add(item.id);
                         return true;
@@ -245,7 +203,37 @@ const PhotoFrame = ({
                 }
                 return false;
             });
-    }, [files, deleted, search, activeCollection]);
+    }, [
+        files,
+        deletedFileIds,
+        search?.date,
+        search?.location,
+        activeCollection,
+    ]);
+
+    const fileToCollectionsMap = useMemo(() => {
+        const fileToCollectionsMap = new Map<number, number[]>();
+        files.forEach((file) => {
+            if (!fileToCollectionsMap.get(file.id)) {
+                fileToCollectionsMap.set(file.id, []);
+            }
+            fileToCollectionsMap.get(file.id).push(file.collectionID);
+        });
+        return fileToCollectionsMap;
+    }, [files]);
+
+    const collectionNameMap = useMemo(() => {
+        if (collections) {
+            return new Map<number, string>(
+                collections.map((collection) => [
+                    collection.id,
+                    collection.name,
+                ])
+            );
+        } else {
+            return new Map();
+        }
+    }, [collections]);
 
     useEffect(() => {
         const currentURL = new URL(window.location.href);
@@ -262,6 +250,59 @@ const PhotoFrame = ({
         }
     }, [open]);
 
+    useEffect(() => {
+        const handleKeyDown = (e: KeyboardEvent) => {
+            if (e.key === 'Shift') {
+                setIsShiftKeyPressed(true);
+            }
+        };
+        const handleKeyUp = (e: KeyboardEvent) => {
+            if (e.key === 'Shift') {
+                setIsShiftKeyPressed(false);
+            }
+        };
+        document.addEventListener('keydown', handleKeyDown, false);
+        document.addEventListener('keyup', handleKeyUp, false);
+        router.events.on('hashChangeComplete', (url: string) => {
+            const start = url.indexOf('#');
+            const hash = url.slice(start !== -1 ? start : url.length);
+            const shouldPhotoSwipeBeOpened = hash.endsWith(
+                PHOTOSWIPE_HASH_SUFFIX
+            );
+            if (shouldPhotoSwipeBeOpened) {
+                setOpen(true);
+            } else {
+                setOpen(false);
+            }
+        });
+        return () => {
+            document.addEventListener('keydown', handleKeyDown, false);
+            document.addEventListener('keyup', handleKeyUp, false);
+        };
+    }, []);
+
+    useEffect(() => {
+        if (!isNaN(search?.file)) {
+            const filteredDataIdx = filteredData.findIndex((file) => {
+                return file.id === search.file;
+            });
+            if (!isNaN(filteredDataIdx)) {
+                onThumbnailClick(filteredDataIdx)();
+            }
+            resetSearch();
+        }
+    }, [search, filteredData]);
+
+    const resetFetching = () => {
+        setFetching({});
+    };
+
+    useEffect(() => {
+        if (selected.count === 0) {
+            setRangeStart(null);
+        }
+    }, [selected]);
+
     const getFileIndexFromID = (files: EnteFile[], id: number) => {
         const index = files.findIndex((file) => file.id === id);
         if (index === -1) {
@@ -272,12 +313,10 @@ const PhotoFrame = ({
 
     const updateURL = (id: number) => (url: string) => {
         const updateFile = (file: EnteFile) => {
-            file = {
-                ...file,
-                msrc: url,
-                w: window.innerWidth,
-                h: window.innerHeight,
-            };
+            file.msrc = url;
+            file.w = window.innerWidth;
+            file.h = window.innerHeight;
+
             if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
                 file.html = `
                 <div class="pswp-item-container">
@@ -307,29 +346,30 @@ const PhotoFrame = ({
             }
             return file;
         };
-        setFiles((files) => {
-            const index = getFileIndexFromID(files, id);
-            files[index] = updateFile(files[index]);
-            return files;
-        });
         const index = getFileIndexFromID(files, id);
         return updateFile(files[index]);
     };
 
     const updateSrcURL = async (id: number, srcURL: SourceURL) => {
-        const { videoURL, imageURL } = srcURL;
-        const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
+        const {
+            originalImageURL,
+            convertedImageURL,
+            originalVideoURL,
+            convertedVideoURL,
+        } = srcURL;
+        const isPlayable =
+            convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
         const updateFile = (file: EnteFile) => {
-            file = {
-                ...file,
-                w: window.innerWidth,
-                h: window.innerHeight,
-            };
+            file.w = window.innerWidth;
+            file.h = window.innerHeight;
+            file.isSourceLoaded = true;
+            file.originalImageURL = originalImageURL;
+            file.originalVideoURL = originalVideoURL;
             if (file.metadata.fileType === FILE_TYPE.VIDEO) {
                 if (isPlayable) {
                     file.html = `
             <video controls onContextMenu="return false;">
-                <source src="${videoURL}" />
+                <source src="${convertedVideoURL}" />
                 Your browser does not support the video tag.
             </video>
         `;
@@ -339,7 +379,7 @@ const PhotoFrame = ({
                 <img src="${file.msrc}" onContextMenu="return false;"/>
                 <div class="download-banner" >
                     ${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
-                    <a class="btn btn-outline-success" href=${videoURL} download="${file.metadata.title}"">Download</a>
+                    <a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
                 </div>
             </div>
             `;
@@ -348,9 +388,9 @@ const PhotoFrame = ({
                 if (isPlayable) {
                     file.html = `
                 <div class = 'pswp-item-container'>
-                    <img id = "live-photo-image-${file.id}" src="${imageURL}" onContextMenu="return false;"/>
+                    <img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
                     <video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
-                        <source src="${videoURL}" />
+                        <source src="${convertedVideoURL}" />
                         Your browser does not support the video tag.
                     </video>
                 </div>
@@ -367,15 +407,10 @@ const PhotoFrame = ({
                 `;
                 }
             } else {
-                file.src = imageURL;
+                file.src = convertedImageURL;
             }
             return file;
         };
-        setFiles((files) => {
-            const index = getFileIndexFromID(files, id);
-            files[index] = updateFile(files[index]);
-            return files;
-        });
         setIsSourceLoaded(true);
         const index = getFileIndexFromID(files, id);
         return updateFile(files[index]);
@@ -441,7 +476,11 @@ const PhotoFrame = ({
             handleSelect(filteredData[index].id, index)(!checked);
         }
     };
-    const getThumbnail = (files: EnteFile[], index: number) =>
+    const getThumbnail = (
+        files: EnteFile[],
+        index: number,
+        isScrolling: boolean
+    ) =>
         files[index] ? (
             <PreviewCard
                 key={`tile-${files[index].id}-selected-${
@@ -465,6 +504,7 @@ const PhotoFrame = ({
                     (index >= currentHover && index <= rangeStart)
                 }
                 activeCollection={activeCollection}
+                showPlaceholder={isScrolling}
             />
         ) : (
             <></>
@@ -499,6 +539,9 @@ const PhotoFrame = ({
                 item.msrc = newFile.msrc;
                 item.html = newFile.html;
                 item.src = newFile.src;
+                item.isSourceLoaded = newFile.isSourceLoaded;
+                item.originalImageURL = newFile.originalImageURL;
+                item.originalVideoURL = newFile.originalVideoURL;
                 item.w = newFile.w;
                 item.h = newFile.h;
 
@@ -521,10 +564,13 @@ const PhotoFrame = ({
         if (!fetching[item.id]) {
             try {
                 fetching[item.id] = true;
-                let urls: string[];
+                let urls: { original: string[]; converted: string[] };
                 if (galleryContext.files.has(item.id)) {
                     const mergedURL = galleryContext.files.get(item.id);
-                    urls = mergedURL.split(',');
+                    urls = {
+                        original: mergedURL.original.split(','),
+                        converted: mergedURL.converted.split(','),
+                    };
                 } else {
                     appContext.startLoading();
                     if (
@@ -540,26 +586,40 @@ const PhotoFrame = ({
                         urls = await DownloadManager.getFile(item, true);
                     }
                     appContext.finishLoading();
-                    const mergedURL = urls.join(',');
+                    const mergedURL = {
+                        original: urls.original.join(','),
+                        converted: urls.converted.join(','),
+                    };
                     galleryContext.files.set(item.id, mergedURL);
                 }
-                let imageURL;
-                let videoURL;
+                let originalImageURL;
+                let originalVideoURL;
+                let convertedImageURL;
+                let convertedVideoURL;
+
                 if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
-                    [imageURL, videoURL] = urls;
+                    [originalImageURL, originalVideoURL] = urls.original;
+                    [convertedImageURL, convertedVideoURL] = urls.converted;
                 } else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
-                    [videoURL] = urls;
+                    [originalVideoURL] = urls.original;
+                    [convertedVideoURL] = urls.converted;
                 } else {
-                    [imageURL] = urls;
+                    [originalImageURL] = urls.original;
+                    [convertedImageURL] = urls.converted;
                 }
                 setIsSourceLoaded(false);
                 const newFile = await updateSrcURL(item.id, {
-                    imageURL,
-                    videoURL,
+                    originalImageURL,
+                    originalVideoURL,
+                    convertedImageURL,
+                    convertedVideoURL,
                 });
                 item.msrc = newFile.msrc;
                 item.html = newFile.html;
                 item.src = newFile.src;
+                item.isSourceLoaded = newFile.isSourceLoaded;
+                item.originalImageURL = newFile.originalImageURL;
+                item.originalVideoURL = newFile.originalVideoURL;
                 item.w = newFile.w;
                 item.h = newFile.h;
                 try {
@@ -606,17 +666,21 @@ const PhotoFrame = ({
                             />
                         )}
                     </AutoSizer>
-                    <PhotoSwipe
+                    <PhotoViewer
                         isOpen={open}
                         items={filteredData}
                         currentIndex={currentIndex}
                         onClose={handleClose}
                         gettingData={getSlideData}
                         favItemIds={favItemIds}
+                        deletedFileIds={deletedFileIds}
+                        setDeletedFileIds={setDeletedFileIds}
                         isSharedCollection={isSharedCollection}
                         isTrashCollection={activeCollection === TRASH_SECTION}
                         enableDownload={enableDownload}
                         isSourceLoaded={isSourceLoaded}
+                        fileToCollectionsMap={fileToCollectionsMap}
+                        collectionNameMap={collectionNameMap}
                     />
                 </Container>
             )}

+ 84 - 50
src/components/PhotoList.tsx

@@ -1,6 +1,6 @@
 import React, { useRef, useEffect, useContext } from 'react';
 import { VariableSizeList as List } from 'react-window';
-import { Box, styled } from '@mui/material';
+import { Box, Link, styled } from '@mui/material';
 import { EnteFile } from 'types/file';
 import {
     IMAGE_CONTAINER_MAX_HEIGHT,
@@ -15,17 +15,17 @@ import {
 import constants from 'utils/strings/constants';
 import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
 import { ENTE_WEBSITE_LINK } from 'constants/urls';
-import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
 import { convertBytesToHumanReadable } from 'utils/file/size';
 import { DeduplicateContext } from 'pages/deduplicate';
 import { FlexWrapper } from './Container';
 import { Typography } from '@mui/material';
 import { GalleryContext } from 'pages/gallery';
 import { SpecialPadding } from 'styles/SpecialPadding';
+import { formatDate } from 'utils/time/format';
 
 const A_DAY = 24 * 60 * 60 * 1000;
-const NO_OF_PAGES = 2;
 const FOOTER_HEIGHT = 90;
+const ALBUM_FOOTER_HEIGHT = 75;
 
 export enum ITEM_TYPE {
     TIME = 'TIME',
@@ -129,7 +129,6 @@ const SizeAndCountContainer = styled(DateContainer)`
 `;
 
 const FooterContainer = styled(ListItemContainer)`
-    font-size: 14px;
     margin-bottom: 0.75rem;
     @media (max-width: 540px) {
         font-size: 12px;
@@ -142,6 +141,13 @@ const FooterContainer = styled(ListItemContainer)`
     margin-top: calc(2rem + 20px);
 `;
 
+const AlbumFooterContainer = styled(ListItemContainer)`
+    margin-top: 48px;
+    margin-bottom: 10px;
+    text-align: center;
+    justify-content: center;
+`;
+
 const NothingContainer = styled(ListItemContainer)`
     color: #979797;
     text-align: center;
@@ -153,7 +159,11 @@ interface Props {
     width: number;
     filteredData: EnteFile[];
     showAppDownloadBanner: boolean;
-    getThumbnail: (files: EnteFile[], index: number) => JSX.Element;
+    getThumbnail: (
+        files: EnteFile[],
+        index: number,
+        isScrolling?: boolean
+    ) => JSX.Element;
     activeCollection: number;
     resetFetching: () => void;
 }
@@ -223,11 +233,18 @@ export function PhotoList({
         if (timeStampList.length === 1) {
             timeStampList.push(getEmptyListItem());
         }
+        timeStampList.push(getVacuumItem(timeStampList));
+        if (publicCollectionGalleryContext.photoListFooter) {
+            timeStampList.push(
+                getPhotoListFooter(
+                    publicCollectionGalleryContext.photoListFooter
+                )
+            );
+        }
         if (
             showAppDownloadBanner ||
             publicCollectionGalleryContext.accessedThroughSharedURL
         ) {
-            timeStampList.push(getVacuumItem(timeStampList));
             if (publicCollectionGalleryContext.accessedThroughSharedURL) {
                 timeStampList.push(getAlbumsFooter());
             } else {
@@ -244,6 +261,11 @@ export function PhotoList({
         filteredData,
         showAppDownloadBanner,
         publicCollectionGalleryContext.accessedThroughSharedURL,
+        galleryContext.photoListHeader,
+        publicCollectionGalleryContext.photoListFooter,
+        publicCollectionGalleryContext.photoListHeader,
+        deduplicateContext.isOnDeduplicatePage,
+        deduplicateContext.fileSizeMap,
     ]);
 
     const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
@@ -288,32 +310,27 @@ export function PhotoList({
 
     const groupByTime = (timeStampList: TimeStampListItem[]) => {
         let listItemIndex = 0;
-        let currentDate = -1;
-
+        let currentDate;
         filteredData.forEach((item, index) => {
             if (
+                !currentDate ||
                 !isSameDay(
                     new Date(item.metadata.creationTime / 1000),
                     new Date(currentDate)
                 )
             ) {
                 currentDate = item.metadata.creationTime / 1000;
-                const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
-                    weekday: 'short',
-                    year: 'numeric',
-                    month: 'short',
-                    day: 'numeric',
-                });
+
                 timeStampList.push({
                     itemType: ITEM_TYPE.TIME,
                     date: isSameDay(new Date(currentDate), new Date())
-                        ? 'Today'
+                        ? constants.TODAY
                         : isSameDay(
                               new Date(currentDate),
                               new Date(Date.now() - A_DAY)
                           )
-                        ? 'Yesterday'
-                        : dateTimeFormat.format(currentDate),
+                        ? constants.YESTERDAY
+                        : formatDate(currentDate),
                     id: currentDate.toString(),
                 });
                 timeStampList.push({
@@ -336,10 +353,13 @@ export function PhotoList({
         });
     };
 
-    const isSameDay = (first, second) =>
-        first.getFullYear() === second.getFullYear() &&
-        first.getMonth() === second.getMonth() &&
-        first.getDate() === second.getDate();
+    const isSameDay = (first, second) => {
+        return (
+            first.getFullYear() === second.getFullYear() &&
+            first.getMonth() === second.getMonth() &&
+            first.getDate() === second.getDate()
+        );
+    };
 
     const getPhotoListHeader = (photoListHeader) => {
         return {
@@ -352,6 +372,17 @@ export function PhotoList({
         };
     };
 
+    const getPhotoListFooter = (photoListFooter) => {
+        return {
+            ...photoListFooter,
+            item: (
+                <ListItemContainer span={columns}>
+                    {photoListFooter.item}
+                </ListItemContainer>
+            ),
+        };
+    };
+
     const getEmptyListItem = () => {
         return {
             itemType: ITEM_TYPE.OTHER,
@@ -365,12 +396,17 @@ export function PhotoList({
         };
     };
     const getVacuumItem = (timeStampList) => {
+        const footerHeight =
+            publicCollectionGalleryContext.accessedThroughSharedURL
+                ? ALBUM_FOOTER_HEIGHT +
+                  (publicCollectionGalleryContext.photoListFooter?.height ?? 0)
+                : FOOTER_HEIGHT;
         const photoFrameHeight = (() => {
             let sum = 0;
             const getCurrentItemSize = getItemSize(timeStampList);
             for (let i = 0; i < timeStampList.length; i++) {
                 sum += getCurrentItemSize(i);
-                if (height - sum <= FOOTER_HEIGHT) {
+                if (height - sum <= footerHeight) {
                     break;
                 }
             }
@@ -379,7 +415,7 @@ export function PhotoList({
         return {
             itemType: ITEM_TYPE.OTHER,
             item: <></>,
-            height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
+            height: Math.max(height - photoFrameHeight - footerHeight, 0),
         };
     };
 
@@ -389,7 +425,9 @@ export function PhotoList({
             height: FOOTER_HEIGHT,
             item: (
                 <FooterContainer span={columns}>
-                    <Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
+                    <Typography variant="body2">
+                        {constants.INSTALL_MOBILE_APP()}
+                    </Typography>
                 </FooterContainer>
             ),
         };
@@ -398,22 +436,16 @@ export function PhotoList({
     const getAlbumsFooter = () => {
         return {
             itemType: ITEM_TYPE.OTHER,
-            height: FOOTER_HEIGHT,
+            height: ALBUM_FOOTER_HEIGHT,
             item: (
-                <FooterContainer span={columns}>
-                    <p>
-                        {constants.PRESERVED_BY}{' '}
-                        <a
-                            target="_blank"
-                            style={{
-                                color: getVariantColor(ButtonVariant.success),
-                            }}
-                            href={ENTE_WEBSITE_LINK}
-                            rel="noreferrer">
+                <AlbumFooterContainer span={columns}>
+                    <Typography variant="body2">
+                        {constants.SHARED_USING}{' '}
+                        <Link target="_blank" href={ENTE_WEBSITE_LINK}>
                             {constants.ENTE_IO}
-                        </a>
-                    </p>
-                </FooterContainer>
+                        </Link>
+                    </Typography>
+                </AlbumFooterContainer>
             ),
         };
     };
@@ -453,9 +485,10 @@ export function PhotoList({
                             date: currItem.date,
                             span: items[index + 1].items.length,
                         });
-                        newList[newIndex + 1].items = newList[
-                            newIndex + 1
-                        ].items.concat(items[index + 1].items);
+                        newList[newIndex + 1].items = [
+                            ...newList[newIndex + 1].items,
+                            ...items[index + 1].items,
+                        ];
                         index += 2;
                     } else {
                         // Adding items would exceed the number of columns.
@@ -512,10 +545,6 @@ export function PhotoList({
         }
     };
 
-    const extraRowsToRender = Math.ceil(
-        (NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
-    );
-
     const generateKey = (index) => {
         switch (timeStampList[index].itemType) {
             case ITEM_TYPE.FILE:
@@ -527,7 +556,10 @@ export function PhotoList({
         }
     };
 
-    const renderListItem = (listItem: TimeStampListItem) => {
+    const renderListItem = (
+        listItem: TimeStampListItem,
+        isScrolling: boolean
+    ) => {
         switch (listItem.itemType) {
             case ITEM_TYPE.TIME:
                 return listItem.dates ? (
@@ -553,7 +585,8 @@ export function PhotoList({
                 const ret = listItem.items.map((item, idx) =>
                     getThumbnail(
                         filteredDataCopy,
-                        listItem.itemStartIndex + idx
+                        listItem.itemStartIndex + idx,
+                        isScrolling
                     )
                 );
                 if (listItem.groups) {
@@ -584,14 +617,15 @@ export function PhotoList({
             width={width}
             itemCount={timeStampList.length}
             itemKey={generateKey}
-            overscanCount={extraRowsToRender}>
-            {({ index, style }) => (
+            overscanCount={0}
+            useIsScrolling>
+            {({ index, style, isScrolling }) => (
                 <ListItem style={style}>
                     <ListContainer
                         columns={columns}
                         shrinkRatio={shrinkRatio}
                         groups={timeStampList[index].groups}>
-                        {renderListItem(timeStampList[index])}
+                        {renderListItem(timeStampList[index], isScrolling)}
                     </ListContainer>
                 </ListItem>
             )}

+ 0 - 73
src/components/PhotoSwipe/InfoDialog/ExifData.tsx

@@ -1,73 +0,0 @@
-import React, { useState } from 'react';
-import constants from 'utils/strings/constants';
-
-import { RenderInfoItem } from './RenderInfoItem';
-import { LegendContainer } from '../styledComponents/LegendContainer';
-import { Pre } from '../styledComponents/Pre';
-import {
-    Checkbox,
-    FormControlLabel,
-    FormGroup,
-    Typography,
-} from '@mui/material';
-
-export function ExifData(props: { exif: any }) {
-    const { exif } = props;
-    const [showAll, setShowAll] = useState(false);
-
-    const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
-        setShowAll(e.target.checked);
-    };
-
-    const renderAllValues = () => <Pre>{exif.raw}</Pre>;
-
-    const renderSelectedValues = () => (
-        <>
-            {exif?.Make &&
-                exif?.Model &&
-                RenderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
-            {exif?.ImageWidth &&
-                exif?.ImageHeight &&
-                RenderInfoItem(
-                    constants.IMAGE_SIZE,
-                    `${exif.ImageWidth} x ${exif.ImageHeight}`
-                )}
-            {exif?.Flash && RenderInfoItem(constants.FLASH, exif.Flash)}
-            {exif?.FocalLength &&
-                RenderInfoItem(
-                    constants.FOCAL_LENGTH,
-                    exif.FocalLength.toString()
-                )}
-            {exif?.ApertureValue &&
-                RenderInfoItem(
-                    constants.APERTURE,
-                    exif.ApertureValue.toString()
-                )}
-            {exif?.ISOSpeedRatings &&
-                RenderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
-        </>
-    );
-
-    return (
-        <>
-            <LegendContainer>
-                <Typography variant="subtitle" mb={1}>
-                    {constants.EXIF}
-                </Typography>
-                <FormGroup>
-                    <FormControlLabel
-                        control={
-                            <Checkbox
-                                size="small"
-                                onChange={changeHandler}
-                                color="accent"
-                            />
-                        }
-                        label={constants.SHOW_ALL}
-                    />
-                </FormGroup>
-            </LegendContainer>
-            {showAll ? renderAllValues() : renderSelectedValues()}
-        </>
-    );
-}

+ 0 - 100
src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx

@@ -1,100 +0,0 @@
-import React, { useState } from 'react';
-import constants from 'utils/strings/constants';
-import { Col, Form, FormControl } from 'react-bootstrap';
-import { FlexWrapper, Value } from 'components/Container';
-import CloseIcon from '@mui/icons-material/Close';
-import TickIcon from '@mui/icons-material/Done';
-import { Formik } from 'formik';
-import * as Yup from 'yup';
-import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
-import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
-import { IconButton } from '@mui/material';
-
-export interface formValues {
-    filename: string;
-}
-
-export const FileNameEditForm = ({
-    filename,
-    saveEdits,
-    discardEdits,
-    extension,
-}) => {
-    const [loading, setLoading] = useState(false);
-
-    const onSubmit = async (values: formValues) => {
-        try {
-            setLoading(true);
-            await saveEdits(values.filename);
-        } finally {
-            setLoading(false);
-        }
-    };
-    return (
-        <Formik<formValues>
-            initialValues={{ filename }}
-            validationSchema={Yup.object().shape({
-                filename: Yup.string()
-                    .required(constants.REQUIRED)
-                    .max(
-                        MAX_EDITED_FILE_NAME_LENGTH,
-                        constants.FILE_NAME_CHARACTER_LIMIT
-                    ),
-            })}
-            validateOnBlur={false}
-            onSubmit={onSubmit}>
-            {({ values, errors, handleChange, handleSubmit }) => (
-                <Form noValidate onSubmit={handleSubmit}>
-                    <Form.Row>
-                        <Form.Group
-                            bsPrefix="ente-form-group"
-                            as={Col}
-                            xs={extension ? 8 : 9}>
-                            <Form.Control
-                                as="textarea"
-                                placeholder={constants.FILE_NAME}
-                                value={values.filename}
-                                onChange={handleChange('filename')}
-                                isInvalid={Boolean(errors.filename)}
-                                autoFocus
-                                disabled={loading}
-                            />
-                            <FormControl.Feedback
-                                type="invalid"
-                                style={{ textAlign: 'center' }}>
-                                {errors.filename}
-                            </FormControl.Feedback>
-                        </Form.Group>
-                        {extension && (
-                            <Form.Group
-                                bsPrefix="ente-form-group"
-                                as={Col}
-                                xs={1}
-                                controlId="formHorizontalFileName">
-                                <FlexWrapper style={{ padding: '5px' }}>
-                                    {`.${extension}`}
-                                </FlexWrapper>
-                            </Form.Group>
-                        )}
-                        <Form.Group bsPrefix="ente-form-group" as={Col} xs={3}>
-                            <Value width={'16.67%'}>
-                                <IconButton type="submit" disabled={loading}>
-                                    {loading ? (
-                                        <SmallLoadingSpinner />
-                                    ) : (
-                                        <TickIcon />
-                                    )}
-                                </IconButton>
-                                <IconButton
-                                    onClick={discardEdits}
-                                    disabled={loading}>
-                                    <CloseIcon />
-                                </IconButton>
-                            </Value>
-                        </Form.Group>
-                    </Form.Row>
-                </Form>
-            )}
-        </Formik>
-    );
-};

+ 0 - 98
src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx

@@ -1,98 +0,0 @@
-import React, { useState } from 'react';
-import { updateFilePublicMagicMetadata } from 'services/fileService';
-import { EnteFile } from 'types/file';
-import constants from 'utils/strings/constants';
-import {
-    changeFileName,
-    splitFilenameAndExtension,
-    updateExistingFilePubMetadata,
-} from 'utils/file';
-import EditIcon from '@mui/icons-material/Edit';
-import { FreeFlowText, Label, Row, Value } from 'components/Container';
-import { logError } from 'utils/sentry';
-import { FileNameEditForm } from './FileNameEditForm';
-import { IconButton } from '@mui/material';
-
-export const getFileTitle = (filename, extension) => {
-    if (extension) {
-        return filename + '.' + extension;
-    } else {
-        return filename;
-    }
-};
-
-export function RenderFileName({
-    shouldDisableEdits,
-    file,
-    scheduleUpdate,
-}: {
-    shouldDisableEdits: boolean;
-    file: EnteFile;
-    scheduleUpdate: () => void;
-}) {
-    const originalTitle = file?.metadata.title;
-    const [isInEditMode, setIsInEditMode] = useState(false);
-    const [originalFileName, extension] =
-        splitFilenameAndExtension(originalTitle);
-    const [filename, setFilename] = useState(originalFileName);
-    const openEditMode = () => setIsInEditMode(true);
-    const closeEditMode = () => setIsInEditMode(false);
-
-    const saveEdits = async (newFilename: string) => {
-        try {
-            if (file) {
-                if (filename === newFilename) {
-                    closeEditMode();
-                    return;
-                }
-                setFilename(newFilename);
-                const newTitle = getFileTitle(newFilename, extension);
-                let updatedFile = await changeFileName(file, newTitle);
-                updatedFile = (
-                    await updateFilePublicMagicMetadata([updatedFile])
-                )[0];
-                updateExistingFilePubMetadata(file, updatedFile);
-                scheduleUpdate();
-            }
-        } catch (e) {
-            logError(e, 'failed to update file name');
-        } finally {
-            closeEditMode();
-        }
-    };
-    return (
-        <>
-            <Row>
-                <Label width="30%">{constants.FILE_NAME}</Label>
-                {!isInEditMode ? (
-                    <>
-                        <Value width={!shouldDisableEdits ? '60%' : '70%'}>
-                            <FreeFlowText>
-                                {getFileTitle(filename, extension)}
-                            </FreeFlowText>
-                        </Value>
-                        {!shouldDisableEdits && (
-                            <Value
-                                width="10%"
-                                style={{
-                                    cursor: 'pointer',
-                                    marginLeft: '10px',
-                                }}>
-                                <IconButton onClick={openEditMode}>
-                                    <EditIcon />
-                                </IconButton>
-                            </Value>
-                        )}
-                    </>
-                ) : (
-                    <FileNameEditForm
-                        extension={extension}
-                        filename={filename}
-                        saveEdits={saveEdits}
-                        discardEdits={closeEditMode}
-                    />
-                )}
-            </Row>
-        </>
-    );
-}

+ 0 - 175
src/components/PhotoSwipe/InfoDialog/index.tsx

@@ -1,175 +0,0 @@
-import React, { useContext, useEffect, useState } from 'react';
-import constants from 'utils/strings/constants';
-import { formatDateTime } from 'utils/time';
-import { RenderFileName } from './RenderFileName';
-import { ExifData } from './ExifData';
-import { RenderCreationTime } from './RenderCreationTime';
-import { RenderInfoItem } from './RenderInfoItem';
-import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
-import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
-import { AppContext } from 'pages/_app';
-import { Location, Metadata } from 'types/upload';
-import Photoswipe from 'photoswipe';
-import { getEXIFLocation } from 'services/upload/exifService';
-import {
-    PhotoPeopleList,
-    UnidentifiedFaces,
-} from 'components/MachineLearning/PeopleList';
-
-import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
-import { WordList } from 'components/MachineLearning/WordList';
-import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
-
-const FileInfoDialog = styled(Dialog)(({ theme }) => ({
-    zIndex: 1501,
-    '& .MuiDialog-container': {
-        alignItems: 'flex-start',
-    },
-    '& .MuiDialog-paper': {
-        padding: theme.spacing(2),
-    },
-}));
-
-const Legend = styled('span')`
-    font-size: 20px;
-    color: #ddd;
-    display: inline;
-`;
-
-interface Iprops {
-    shouldDisableEdits: boolean;
-    showInfo: boolean;
-    handleCloseInfo: () => void;
-    items: any[];
-    photoSwipe: Photoswipe<Photoswipe.Options>;
-    metadata: Metadata;
-    exif: any;
-    scheduleUpdate: () => void;
-}
-
-export function FileInfo({
-    shouldDisableEdits,
-    showInfo,
-    handleCloseInfo,
-    items,
-    photoSwipe,
-    metadata,
-    exif,
-    scheduleUpdate,
-}: Iprops) {
-    const appContext = useContext(AppContext);
-    const [location, setLocation] = useState<Location>(null);
-    const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
-
-    useEffect(() => {
-        if (!location && metadata) {
-            if (metadata.longitude || metadata.longitude === 0) {
-                setLocation({
-                    latitude: metadata.latitude,
-                    longitude: metadata.longitude,
-                });
-            }
-        }
-    }, [metadata]);
-
-    useEffect(() => {
-        if (!location && exif) {
-            const exifLocation = getEXIFLocation(exif);
-            if (exifLocation.latitude || exifLocation.latitude === 0) {
-                setLocation(exifLocation);
-            }
-        }
-    }, [exif]);
-
-    return (
-        <FileInfoDialog
-            open={showInfo}
-            onClose={handleCloseInfo}
-            fullScreen={appContext.isMobile}>
-            <DialogTitleWithCloseButton onClose={handleCloseInfo}>
-                {constants.INFO}
-            </DialogTitleWithCloseButton>
-            <DialogContent>
-                <Typography variant="subtitle" mb={1}>
-                    {constants.METADATA}
-                </Typography>
-
-                {RenderInfoItem(
-                    constants.FILE_ID,
-                    items[photoSwipe?.getCurrentIndex()]?.id
-                )}
-                {metadata?.title && (
-                    <RenderFileName
-                        shouldDisableEdits={shouldDisableEdits}
-                        file={items[photoSwipe?.getCurrentIndex()]}
-                        scheduleUpdate={scheduleUpdate}
-                    />
-                )}
-                {metadata?.creationTime && (
-                    <RenderCreationTime
-                        shouldDisableEdits={shouldDisableEdits}
-                        file={items[photoSwipe?.getCurrentIndex()]}
-                        scheduleUpdate={scheduleUpdate}
-                    />
-                )}
-                {metadata?.modificationTime &&
-                    RenderInfoItem(
-                        constants.UPDATED_ON,
-                        formatDateTime(metadata.modificationTime / 1000)
-                    )}
-                {location &&
-                    RenderInfoItem(
-                        constants.LOCATION,
-                        <Link
-                            href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
-                            target="_blank"
-                            rel="noopener noreferrer">
-                            {constants.SHOW_MAP}
-                        </Link>
-                    )}
-                {appContext.mlSearchEnabled && (
-                    <>
-                        <div>
-                            <Legend>{constants.PEOPLE}</Legend>
-                        </div>
-                        <PhotoPeopleList
-                            file={items[photoSwipe?.getCurrentIndex()]}
-                            updateMLDataIndex={updateMLDataIndex}
-                        />
-                        <div>
-                            <Legend>{constants.UNIDENTIFIED_FACES}</Legend>
-                        </div>
-                        <UnidentifiedFaces
-                            file={items[photoSwipe?.getCurrentIndex()]}
-                            updateMLDataIndex={updateMLDataIndex}
-                        />
-                        <div>
-                            <Legend>{constants.OBJECTS}</Legend>
-                            <ObjectLabelList
-                                file={items[photoSwipe?.getCurrentIndex()]}
-                                updateMLDataIndex={updateMLDataIndex}
-                            />
-                        </div>
-                        <div>
-                            <Legend>{constants.TEXT}</Legend>
-                            <WordList
-                                file={items[photoSwipe?.getCurrentIndex()]}
-                                updateMLDataIndex={updateMLDataIndex}
-                            />
-                        </div>
-                        <MLServiceFileInfoButton
-                            file={items[photoSwipe?.getCurrentIndex()]}
-                            updateMLDataIndex={updateMLDataIndex}
-                            setUpdateMLDataIndex={setUpdateMLDataIndex}
-                        />
-                    </>
-                )}
-                {exif && (
-                    <>
-                        <ExifData exif={exif} />
-                    </>
-                )}
-            </DialogContent>
-        </FileInfoDialog>
-    );
-}

+ 175 - 0
src/components/PhotoSwipe/infoDialog.tsx

@@ -0,0 +1,175 @@
+export {}; // import React, { useContext, useEffect, useState } from 'react';
+// import constants from 'utils/strings/constants';
+// import { formatDateTime } from 'utils/time';
+// import { RenderFileName } from './RenderFileName';
+// import { ExifData } from './ExifData';
+// import { RenderCreationTime } from './RenderCreationTime';
+// import { RenderInfoItem } from './RenderInfoItem';
+// import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
+// import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
+// import { AppContext } from 'pages/_app';
+// import { Location, Metadata } from 'types/upload';
+// import Photoswipe from 'photoswipe';
+// import { getEXIFLocation } from 'services/upload/exifService';
+// import {
+//     PhotoPeopleList,
+//     UnidentifiedFaces,
+// } from 'components/MachineLearning/PeopleList';
+
+// import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
+// import { WordList } from 'components/MachineLearning/WordList';
+// import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
+
+// const FileInfoDialog = styled(Dialog)(({ theme }) => ({
+//     zIndex: 1501,
+//     '& .MuiDialog-container': {
+//         alignItems: 'flex-start',
+//     },
+//     '& .MuiDialog-paper': {
+//         padding: theme.spacing(2),
+//     },
+// }));
+
+// const Legend = styled('span')`
+//     font-size: 20px;
+//     color: #ddd;
+//     display: inline;
+// `;
+
+// interface Iprops {
+//     shouldDisableEdits: boolean;
+//     showInfo: boolean;
+//     handleCloseInfo: () => void;
+//     items: any[];
+//     photoSwipe: Photoswipe<Photoswipe.Options>;
+//     metadata: Metadata;
+//     exif: any;
+//     scheduleUpdate: () => void;
+// }
+
+// export function FileInfo({
+//     shouldDisableEdits,
+//     showInfo,
+//     handleCloseInfo,
+//     items,
+//     photoSwipe,
+//     metadata,
+//     exif,
+//     scheduleUpdate,
+// }: Iprops) {
+//     const appContext = useContext(AppContext);
+//     const [location, setLocation] = useState<Location>(null);
+//     const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
+
+//     useEffect(() => {
+//         if (!location && metadata) {
+//             if (metadata.longitude || metadata.longitude === 0) {
+//                 setLocation({
+//                     latitude: metadata.latitude,
+//                     longitude: metadata.longitude,
+//                 });
+//             }
+//         }
+//     }, [metadata]);
+
+//     useEffect(() => {
+//         if (!location && exif) {
+//             const exifLocation = getEXIFLocation(exif);
+//             if (exifLocation.latitude || exifLocation.latitude === 0) {
+//                 setLocation(exifLocation);
+//             }
+//         }
+//     }, [exif]);
+
+//     return (
+//         <FileInfoDialog
+//             open={showInfo}
+//             onClose={handleCloseInfo}
+//             fullScreen={appContext.isMobile}>
+//             <DialogTitleWithCloseButton onClose={handleCloseInfo}>
+//                 {constants.INFO}
+//             </DialogTitleWithCloseButton>
+//             <DialogContent>
+//                 <Typography variant="subtitle" mb={1}>
+//                     {constants.METADATA}
+//                 </Typography>
+
+//                 {RenderInfoItem(
+//                     constants.FILE_ID,
+//                     items[photoSwipe?.getCurrentIndex()]?.id
+//                 )}
+//                 {metadata?.title && (
+//                     <RenderFileName
+//                         shouldDisableEdits={shouldDisableEdits}
+//                         file={items[photoSwipe?.getCurrentIndex()]}
+//                         scheduleUpdate={scheduleUpdate}
+//                     />
+//                 )}
+//                 {metadata?.creationTime && (
+//                     <RenderCreationTime
+//                         shouldDisableEdits={shouldDisableEdits}
+//                         file={items[photoSwipe?.getCurrentIndex()]}
+//                         scheduleUpdate={scheduleUpdate}
+//                     />
+//                 )}
+//                 {metadata?.modificationTime &&
+//                     RenderInfoItem(
+//                         constants.UPDATED_ON,
+//                         formatDateTime(metadata.modificationTime / 1000)
+//                     )}
+//                 {location &&
+//                     RenderInfoItem(
+//                         constants.LOCATION,
+//                         <Link
+//                             href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
+//                             target="_blank"
+//                             rel="noopener noreferrer">
+//                             {constants.SHOW_MAP}
+//                         </Link>
+//                     )}
+//                 {appContext.mlSearchEnabled && (
+//                     <>
+//                         <div>
+//                             <Legend>{constants.PEOPLE}</Legend>
+//                         </div>
+//                         <PhotoPeopleList
+//                             file={items[photoSwipe?.getCurrentIndex()]}
+//                             updateMLDataIndex={updateMLDataIndex}
+//                         />
+//                         <div>
+//                             <Legend>{constants.UNIDENTIFIED_FACES}</Legend>
+//                         </div>
+//                         <UnidentifiedFaces
+//                             file={items[photoSwipe?.getCurrentIndex()]}
+//                             updateMLDataIndex={updateMLDataIndex}
+//                         />
+//                         <div>
+//                             <Legend>{constants.OBJECTS}</Legend>
+//                             <ObjectLabelList
+//                                 file={items[photoSwipe?.getCurrentIndex()]}
+//                                 updateMLDataIndex={updateMLDataIndex}
+//                             />
+//                         </div>
+//                         <div>
+//                             <Legend>{constants.TEXT}</Legend>
+//                             <WordList
+//                                 file={items[photoSwipe?.getCurrentIndex()]}
+//                                 updateMLDataIndex={updateMLDataIndex}
+//                             />
+//                         </div>
+//                         <MLServiceFileInfoButton
+//                             file={items[photoSwipe?.getCurrentIndex()]}
+//                             updateMLDataIndex={updateMLDataIndex}
+//                             setUpdateMLDataIndex={setUpdateMLDataIndex}
+//                         />
+//                     </>
+//                 )}
+//                 {exif && (
+//                     <>
+//                         <ExifData exif={exif} />
+//                     </>
+//                 )}
+//             </DialogContent>
+//         </FileInfoDialog>
+//     );
+// }

+ 92 - 0
src/components/PhotoViewer/FileInfo/ExifData.tsx

@@ -0,0 +1,92 @@
+import React from 'react';
+import constants from 'utils/strings/constants';
+
+import { Stack, styled, Typography } from '@mui/material';
+import { FileInfoSidebar } from '.';
+import Titlebar from 'components/Titlebar';
+import { Box } from '@mui/system';
+import CopyButton from 'components/CodeBlock/CopyButton';
+import { formatDateFull } from 'utils/time/format';
+
+const ExifItem = styled(Box)`
+    padding-left: 8px;
+    padding-right: 8px;
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+`;
+
+function parseExifValue(value: any) {
+    switch (typeof value) {
+        case 'string':
+        case 'number':
+            return value;
+        default:
+            if (value instanceof Date) {
+                return formatDateFull(value);
+            }
+            try {
+                return JSON.stringify(Array.from(value));
+            } catch (e) {
+                return null;
+            }
+    }
+}
+export function ExifData(props: {
+    exif: any;
+    open: boolean;
+    onClose: () => void;
+    filename: string;
+    onInfoClose: () => void;
+}) {
+    const { exif, open, onClose, filename, onInfoClose } = props;
+
+    if (!exif) {
+        return <></>;
+    }
+    const handleRootClose = () => {
+        onClose();
+        onInfoClose();
+    };
+
+    return (
+        <FileInfoSidebar open={open} onClose={onClose}>
+            <Titlebar
+                onClose={onClose}
+                title={constants.EXIF}
+                caption={filename}
+                onRootClose={handleRootClose}
+                actionButton={
+                    <CopyButton
+                        code={JSON.stringify(exif)}
+                        color={'secondary'}
+                    />
+                }
+            />
+            <Stack py={3} px={1} spacing={2}>
+                {[...Object.entries(exif)].map(([key, value]) =>
+                    value ? (
+                        <ExifItem key={key}>
+                            <Typography
+                                variant="body2"
+                                color={'text.secondary'}>
+                                {key}
+                            </Typography>
+                            <Typography
+                                sx={{
+                                    width: '100%',
+                                    textOverflow: 'ellipsis',
+                                    whiteSpace: 'nowrap',
+                                    overflow: 'hidden',
+                                }}>
+                                {parseExifValue(value)}
+                            </Typography>
+                        </ExifItem>
+                    ) : (
+                        <></>
+                    )
+                )}
+            </Stack>
+        </FileInfoSidebar>
+    );
+}

+ 47 - 0
src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import constants from 'utils/strings/constants';
+import { DialogContent, DialogTitle } from '@mui/material';
+import DialogBoxBase from 'components/DialogBox/base';
+import SingleInputForm, {
+    SingleInputFormProps,
+} from 'components/SingleInputForm';
+
+export const FileNameEditDialog = ({
+    isInEditMode,
+    closeEditMode,
+    filename,
+    extension,
+    saveEdits,
+}) => {
+    const onSubmit: SingleInputFormProps['callback'] = async (
+        filename,
+        setFieldError
+    ) => {
+        try {
+            await saveEdits(filename);
+            closeEditMode();
+        } catch (e) {
+            setFieldError(constants.UNKNOWN_ERROR);
+        }
+    };
+    return (
+        <DialogBoxBase
+            open={isInEditMode}
+            onClose={closeEditMode}
+            sx={{ zIndex: 1600 }}>
+            <DialogTitle>{constants.RENAME_FILE}</DialogTitle>
+            <DialogContent>
+                <SingleInputForm
+                    initialValue={filename}
+                    callback={onSubmit}
+                    placeholder={constants.ENTER_FILE_NAME}
+                    buttonText={constants.RENAME}
+                    fieldType="text"
+                    caption={extension}
+                    secondaryButtonAction={closeEditMode}
+                    submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
+                />
+            </DialogContent>
+        </DialogBoxBase>
+    );
+};

+ 61 - 0
src/components/PhotoViewer/FileInfo/InfoItem.tsx

@@ -0,0 +1,61 @@
+import Edit from '@mui/icons-material/Edit';
+import { Box, IconButton, Typography } from '@mui/material';
+import { FlexWrapper } from 'components/Container';
+import React from 'react';
+import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
+
+interface Iprops {
+    icon: JSX.Element;
+    title?: string;
+    caption?: string | JSX.Element;
+    openEditor?: any;
+    loading?: boolean;
+    hideEditOption?: any;
+    customEndButton?: any;
+    children?: any;
+}
+
+export default function InfoItem({
+    icon,
+    title,
+    caption,
+    openEditor,
+    loading,
+    hideEditOption,
+    customEndButton,
+    children,
+}: Iprops): JSX.Element {
+    return (
+        <FlexWrapper justifyContent="space-between">
+            <Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
+                <IconButton
+                    color="secondary"
+                    sx={{ '&&': { cursor: 'default', m: 0.5 } }}
+                    disableRipple>
+                    {icon}
+                </IconButton>
+                <Box py={0.5}>
+                    {children ? (
+                        children
+                    ) : (
+                        <>
+                            <Typography sx={{ wordBreak: 'break-all' }}>
+                                {title}
+                            </Typography>
+                            <Typography variant="body2" color="text.secondary">
+                                {caption}
+                            </Typography>
+                        </>
+                    )}
+                </Box>
+            </Box>
+            {customEndButton
+                ? customEndButton
+                : !hideEditOption && (
+                      <IconButton onClick={openEditor} color="secondary">
+                          {!loading ? <Edit /> : <SmallLoadingSpinner />}
+                      </IconButton>
+                  )}
+        </FlexWrapper>
+    );
+}

+ 126 - 0
src/components/PhotoViewer/FileInfo/RenderCaption.tsx

@@ -0,0 +1,126 @@
+import React, { useState } from 'react';
+import { updateFilePublicMagicMetadata } from 'services/fileService';
+import { EnteFile } from 'types/file';
+import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
+import { logError } from 'utils/sentry';
+import { Box, IconButton, TextField } from '@mui/material';
+import { FlexWrapper } from 'components/Container';
+import { MAX_CAPTION_SIZE } from 'constants/file';
+import { Formik } from 'formik';
+import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
+import * as Yup from 'yup';
+import constants from 'utils/strings/constants';
+import Close from '@mui/icons-material/Close';
+import Done from '@mui/icons-material/Done';
+
+interface formValues {
+    caption: string;
+}
+
+export function RenderCaption({
+    file,
+    scheduleUpdate,
+    refreshPhotoswipe,
+}: {
+    shouldDisableEdits: boolean;
+    file: EnteFile;
+    scheduleUpdate: () => void;
+    refreshPhotoswipe: () => void;
+}) {
+    const [caption, setCaption] = useState(
+        file?.pubMagicMetadata?.data.caption
+    );
+
+    const [loading, setLoading] = useState(false);
+
+    const saveEdits = async (newCaption: string) => {
+        try {
+            if (file) {
+                if (caption === newCaption) {
+                    return;
+                }
+                setCaption(newCaption);
+
+                let updatedFile = await changeCaption(file, newCaption);
+                updatedFile = (
+                    await updateFilePublicMagicMetadata([updatedFile])
+                )[0];
+                updateExistingFilePubMetadata(file, updatedFile);
+                file.title = file.pubMagicMetadata.data.caption;
+                refreshPhotoswipe();
+                scheduleUpdate();
+            }
+        } catch (e) {
+            logError(e, 'failed to update caption');
+        }
+    };
+
+    const onSubmit = async (values: formValues) => {
+        try {
+            setLoading(true);
+            await saveEdits(values.caption);
+        } finally {
+            setLoading(false);
+        }
+    };
+    return (
+        <Box p={1}>
+            <Formik<formValues>
+                initialValues={{ caption }}
+                validationSchema={Yup.object().shape({
+                    caption: Yup.string().max(
+                        MAX_CAPTION_SIZE,
+                        constants.CAPTION_CHARACTER_LIMIT
+                    ),
+                })}
+                validateOnBlur={false}
+                onSubmit={onSubmit}>
+                {({
+                    values,
+                    errors,
+                    handleChange,
+                    handleSubmit,
+                    resetForm,
+                }) => (
+                    <form noValidate onSubmit={handleSubmit}>
+                        <TextField
+                            hiddenLabel
+                            fullWidth
+                            id="caption"
+                            name="caption"
+                            type="text"
+                            multiline
+                            placeholder={constants.CAPTION_PLACEHOLDER}
+                            value={values.caption}
+                            onChange={handleChange('caption')}
+                            error={Boolean(errors.caption)}
+                            helperText={errors.caption}
+                            disabled={loading}
+                        />
+                        {values.caption !== caption && (
+                            <FlexWrapper justifyContent={'flex-end'}>
+                                <IconButton type="submit" disabled={loading}>
+                                    {loading ? (
+                                        <SmallLoadingSpinner />
+                                    ) : (
+                                        <Done />
+                                    )}
+                                </IconButton>
+                                <IconButton
+                                    onClick={() =>
+                                        resetForm({
+                                            values: { caption: caption ?? '' },
+                                            touched: { caption: false },
+                                        })
+                                    }
+                                    disabled={loading}>
+                                    <Close />
+                                </IconButton>
+                            </FlexWrapper>
+                        )}
+                    </form>
+                )}
+            </Formik>
+        </Box>
+    );
+}

+ 21 - 38
src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx → src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx

@@ -1,18 +1,16 @@
 import React, { useState } from 'react';
 import { updateFilePublicMagicMetadata } from 'services/fileService';
 import { EnteFile } from 'types/file';
-import constants from 'utils/strings/constants';
+import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
 import {
     changeFileCreationTime,
     updateExistingFilePubMetadata,
 } from 'utils/file';
-import { formatDateTime } from 'utils/time';
-import EditIcon from '@mui/icons-material/Edit';
-import { Label, Row, Value } from 'components/Container';
+import { formatDate, formatTime } from 'utils/time/format';
+import { FlexWrapper } from 'components/Container';
 import { logError } from 'utils/sentry';
-import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
 import EnteDateTimePicker from 'components/EnteDateTimePicker';
-import { IconButton } from '@mui/material';
+import InfoItem from './InfoItem';
 
 export function RenderCreationTime({
     shouldDisableEdits,
@@ -59,39 +57,24 @@ export function RenderCreationTime({
 
     return (
         <>
-            <Row>
-                <Label width="30%">{constants.CREATION_TIME}</Label>
-                <Value
-                    width={
-                        !shouldDisableEdits ? !isInEditMode && '60%' : '70%'
-                    }>
-                    {isInEditMode ? (
-                        <EnteDateTimePicker
-                            initialValue={originalCreationTime}
-                            disabled={loading}
-                            onSubmit={saveEdits}
-                            onClose={closeEditMode}
-                        />
-                    ) : (
-                        formatDateTime(originalCreationTime)
-                    )}
-                </Value>
-                {!shouldDisableEdits && !isInEditMode && (
-                    <Value
-                        width={'10%'}
-                        style={{ cursor: 'pointer', marginLeft: '10px' }}>
-                        {loading ? (
-                            <IconButton>
-                                <SmallLoadingSpinner />
-                            </IconButton>
-                        ) : (
-                            <IconButton onClick={openEditMode}>
-                                <EditIcon />
-                            </IconButton>
-                        )}
-                    </Value>
+            <FlexWrapper>
+                <InfoItem
+                    icon={<CalendarTodayIcon />}
+                    title={formatDate(originalCreationTime)}
+                    caption={formatTime(originalCreationTime)}
+                    openEditor={openEditMode}
+                    loading={loading}
+                    hideEditOption={shouldDisableEdits || isInEditMode}
+                />
+                {isInEditMode && (
+                    <EnteDateTimePicker
+                        initialValue={originalCreationTime}
+                        disabled={loading}
+                        onSubmit={saveEdits}
+                        onClose={closeEditMode}
+                    />
                 )}
-            </Row>
+            </FlexWrapper>
         </>
     );
 }

+ 122 - 0
src/components/PhotoViewer/FileInfo/RenderFileName.tsx

@@ -0,0 +1,122 @@
+import React, { useEffect, useState } from 'react';
+import { updateFilePublicMagicMetadata } from 'services/fileService';
+import { EnteFile } from 'types/file';
+import {
+    changeFileName,
+    splitFilenameAndExtension,
+    updateExistingFilePubMetadata,
+} from 'utils/file';
+import { FlexWrapper } from 'components/Container';
+import { logError } from 'utils/sentry';
+import { FILE_TYPE } from 'constants/file';
+import InfoItem from './InfoItem';
+import { makeHumanReadableStorage } from 'utils/billing';
+import Box from '@mui/material/Box';
+import { FileNameEditDialog } from './FileNameEditDialog';
+import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
+import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
+
+const getFileTitle = (filename, extension) => {
+    if (extension) {
+        return filename + '.' + extension;
+    } else {
+        return filename;
+    }
+};
+
+const getCaption = (file: EnteFile, parsedExifData) => {
+    const megaPixels = parsedExifData?.['megaPixels'];
+    const resolution = parsedExifData?.['resolution'];
+    const fileSize = file.info?.fileSize;
+
+    const captionParts = [];
+    if (megaPixels) {
+        captionParts.push(megaPixels);
+    }
+    if (resolution) {
+        captionParts.push(resolution);
+    }
+    if (fileSize) {
+        captionParts.push(makeHumanReadableStorage(fileSize));
+    }
+    return (
+        <FlexWrapper gap={1}>
+            {captionParts.map((caption) => (
+                <Box key={caption}> {caption}</Box>
+            ))}
+        </FlexWrapper>
+    );
+};
+
+export function RenderFileName({
+    parsedExifData,
+    shouldDisableEdits,
+    file,
+    scheduleUpdate,
+}: {
+    parsedExifData: Record<string, any>;
+    shouldDisableEdits: boolean;
+    file: EnteFile;
+    scheduleUpdate: () => void;
+}) {
+    const [isInEditMode, setIsInEditMode] = useState(false);
+    const openEditMode = () => setIsInEditMode(true);
+    const closeEditMode = () => setIsInEditMode(false);
+    const [filename, setFilename] = useState<string>();
+    const [extension, setExtension] = useState<string>();
+
+    useEffect(() => {
+        const [filename, extension] = splitFilenameAndExtension(
+            file.metadata.title
+        );
+        setFilename(filename);
+        setExtension(extension);
+    }, []);
+
+    const saveEdits = async (newFilename: string) => {
+        try {
+            if (file) {
+                if (filename === newFilename) {
+                    closeEditMode();
+                    return;
+                }
+                setFilename(newFilename);
+                const newTitle = getFileTitle(newFilename, extension);
+                let updatedFile = await changeFileName(file, newTitle);
+                updatedFile = (
+                    await updateFilePublicMagicMetadata([updatedFile])
+                )[0];
+                updateExistingFilePubMetadata(file, updatedFile);
+                scheduleUpdate();
+            }
+        } catch (e) {
+            logError(e, 'failed to update file name');
+            throw e;
+        }
+    };
+
+    return (
+        <>
+            <InfoItem
+                icon={
+                    file.metadata.fileType === FILE_TYPE.VIDEO ? (
+                        <VideocamOutlined />
+                    ) : (
+                        <PhotoOutlined />
+                    )
+                }
+                title={getFileTitle(filename, extension)}
+                caption={getCaption(file, parsedExifData)}
+                openEditor={openEditMode}
+                hideEditOption={shouldDisableEdits || isInEditMode}
+            />
+            <FileNameEditDialog
+                isInEditMode={isInEditMode}
+                closeEditMode={closeEditMode}
+                filename={filename}
+                extension={extension}
+                saveEdits={saveEdits}
+            />
+        </>
+    );
+}

+ 0 - 0
src/components/PhotoSwipe/InfoDialog/RenderInfoItem.tsx → src/components/PhotoViewer/FileInfo/RenderInfoItem.tsx


+ 329 - 0
src/components/PhotoViewer/FileInfo/index.tsx

@@ -0,0 +1,329 @@
+import React, { useContext, useEffect, useState } from 'react';
+import constants from 'utils/strings/constants';
+import { RenderFileName } from './RenderFileName';
+import { RenderCreationTime } from './RenderCreationTime';
+import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
+import { Location } from 'types/upload';
+import { getEXIFLocation } from 'services/upload/exifService';
+import { RenderCaption } from './RenderCaption';
+
+import CopyButton from 'components/CodeBlock/CopyButton';
+import { formatDate, formatTime } from 'utils/time/format';
+import Titlebar from 'components/Titlebar';
+import InfoItem from './InfoItem';
+import { FlexWrapper } from 'components/Container';
+import EnteSpinner from 'components/EnteSpinner';
+import { EnteFile } from 'types/file';
+import { Chip } from 'components/Chip';
+import LinkButton from 'components/pages/gallery/LinkButton';
+import { ExifData } from './ExifData';
+import { EnteDrawer } from 'components/EnteDrawer';
+import CameraOutlined from '@mui/icons-material/CameraOutlined';
+import LocationOnOutlined from '@mui/icons-material/LocationOnOutlined';
+import TextSnippetOutlined from '@mui/icons-material/TextSnippetOutlined';
+import FolderOutlined from '@mui/icons-material/FolderOutlined';
+import BackupOutlined from '@mui/icons-material/BackupOutlined';
+
+import {
+    PhotoPeopleList,
+    UnidentifiedFaces,
+} from 'components/MachineLearning/PeopleList';
+
+import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
+import { WordList } from 'components/MachineLearning/WordList';
+import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
+import { AppContext } from 'pages/_app';
+import { Legend } from '../styledComponents/Legend';
+
+export const FileInfoSidebar = styled((props: DialogProps) => (
+    <EnteDrawer {...props} anchor="right" />
+))({
+    zIndex: 1501,
+    '& .MuiPaper-root': {
+        padding: 8,
+    },
+});
+
+interface Iprops {
+    shouldDisableEdits: boolean;
+    showInfo: boolean;
+    handleCloseInfo: () => void;
+    file: EnteFile;
+    exif: any;
+    scheduleUpdate: () => void;
+    refreshPhotoswipe: () => void;
+    fileToCollectionsMap: Map<number, number[]>;
+    collectionNameMap: Map<number, string>;
+    isTrashCollection: boolean;
+}
+
+function BasicDeviceCamera({
+    parsedExifData,
+}: {
+    parsedExifData: Record<string, any>;
+}) {
+    return (
+        <FlexWrapper gap={1}>
+            <Box>{parsedExifData['fNumber']}</Box>
+            <Box>{parsedExifData['exposureTime']}</Box>
+            <Box>{parsedExifData['ISO']}</Box>
+        </FlexWrapper>
+    );
+}
+
+function getOpenStreetMapLink(location: {
+    latitude: number;
+    longitude: number;
+}) {
+    return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
+}
+
+export function FileInfo({
+    shouldDisableEdits,
+    showInfo,
+    handleCloseInfo,
+    file,
+    exif,
+    scheduleUpdate,
+    refreshPhotoswipe,
+    fileToCollectionsMap,
+    collectionNameMap,
+    isTrashCollection,
+}: Iprops) {
+    const appContext = useContext(AppContext);
+    const [location, setLocation] = useState<Location>(null);
+    const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
+    const [showExif, setShowExif] = useState(false);
+    const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
+
+    const openExif = () => setShowExif(true);
+    const closeExif = () => setShowExif(false);
+
+    useEffect(() => {
+        if (!location && file && file.metadata) {
+            if (file.metadata.longitude || file.metadata.longitude === 0) {
+                setLocation({
+                    latitude: file.metadata.latitude,
+                    longitude: file.metadata.longitude,
+                });
+            }
+        }
+    }, [file]);
+
+    useEffect(() => {
+        if (!location && exif) {
+            const exifLocation = getEXIFLocation(exif);
+            if (exifLocation.latitude || exifLocation.latitude === 0) {
+                setLocation(exifLocation);
+            }
+        }
+    }, [exif]);
+
+    useEffect(() => {
+        if (!exif) {
+            setParsedExifData({});
+            return;
+        }
+        const parsedExifData = {};
+        if (exif['fNumber']) {
+            parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`;
+        } else if (exif['ApertureValue'] && exif['FocalLength']) {
+            parsedExifData['fNumber'] = `f/${Math.ceil(
+                exif['FocalLength'] / exif['ApertureValue']
+            )}`;
+        }
+        const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth'];
+        const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight'];
+        if (imageWidth && imageHeight) {
+            parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`;
+            const megaPixels = Math.round((imageWidth * imageHeight) / 1000000);
+            if (megaPixels) {
+                parsedExifData['megaPixels'] = `${Math.round(
+                    (imageWidth * imageHeight) / 1000000
+                )}MP`;
+            }
+        }
+        if (exif['Make'] && exif['Model']) {
+            parsedExifData[
+                'takenOnDevice'
+            ] = `${exif['Make']} ${exif['Model']}`;
+        }
+        if (exif['ExposureTime']) {
+            parsedExifData['exposureTime'] = `1/${
+                1 / parseFloat(exif['ExposureTime'])
+            }`;
+        }
+        if (exif['ISO']) {
+            parsedExifData['ISO'] = `ISO${exif['ISO']}`;
+        }
+        setParsedExifData(parsedExifData);
+    }, [exif]);
+
+    if (!file) {
+        return <></>;
+    }
+
+    return (
+        <FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
+            <Titlebar
+                onClose={handleCloseInfo}
+                title={constants.INFO}
+                backIsClose
+            />
+            <Stack pt={1} pb={3} spacing={'20px'}>
+                <RenderCaption
+                    shouldDisableEdits={shouldDisableEdits}
+                    file={file}
+                    scheduleUpdate={scheduleUpdate}
+                    refreshPhotoswipe={refreshPhotoswipe}
+                />
+
+                <RenderCreationTime
+                    shouldDisableEdits={shouldDisableEdits}
+                    file={file}
+                    scheduleUpdate={scheduleUpdate}
+                />
+
+                <RenderFileName
+                    parsedExifData={parsedExifData}
+                    shouldDisableEdits={shouldDisableEdits}
+                    file={file}
+                    scheduleUpdate={scheduleUpdate}
+                />
+                {parsedExifData && parsedExifData['takenOnDevice'] && (
+                    <InfoItem
+                        icon={<CameraOutlined />}
+                        title={parsedExifData['takenOnDevice']}
+                        caption={
+                            <BasicDeviceCamera
+                                parsedExifData={parsedExifData}
+                            />
+                        }
+                        hideEditOption
+                    />
+                )}
+
+                {location && (
+                    <InfoItem
+                        icon={<LocationOnOutlined />}
+                        title={constants.LOCATION}
+                        caption={
+                            <Link
+                                href={getOpenStreetMapLink({
+                                    latitude: file.metadata.latitude,
+                                    longitude: file.metadata.longitude,
+                                })}
+                                target="_blank"
+                                sx={{ fontWeight: 'bold' }}>
+                                {constants.SHOW_ON_MAP}
+                            </Link>
+                        }
+                        customEndButton={
+                            <CopyButton
+                                code={getOpenStreetMapLink({
+                                    latitude: file.metadata.latitude,
+                                    longitude: file.metadata.longitude,
+                                })}
+                                color="secondary"
+                                size="medium"
+                            />
+                        }
+                    />
+                )}
+                <InfoItem
+                    icon={<TextSnippetOutlined />}
+                    title={constants.DETAILS}
+                    caption={
+                        typeof exif === 'undefined' ? (
+                            <EnteSpinner size={11.33} />
+                        ) : exif !== null ? (
+                            <LinkButton
+                                onClick={openExif}
+                                sx={{
+                                    textDecoration: 'none',
+                                    color: 'text.secondary',
+                                    fontWeight: 'bold',
+                                }}>
+                                {constants.VIEW_EXIF}
+                            </LinkButton>
+                        ) : (
+                            constants.NO_EXIF
+                        )
+                    }
+                    hideEditOption
+                />
+                <InfoItem
+                    icon={<BackupOutlined />}
+                    title={formatDate(file.metadata.modificationTime / 1000)}
+                    caption={formatTime(file.metadata.modificationTime / 1000)}
+                    hideEditOption
+                />
+                {!isTrashCollection && (
+                    <InfoItem icon={<FolderOutlined />} hideEditOption>
+                        <Box
+                            display={'flex'}
+                            gap={1}
+                            flexWrap="wrap"
+                            justifyContent={'flex-start'}
+                            alignItems={'flex-start'}>
+                            {fileToCollectionsMap
+                                .get(file.id)
+                                ?.filter((collectionID) =>
+                                    collectionNameMap.has(collectionID)
+                                )
+                                ?.map((collectionID) => (
+                                    <Chip key={collectionID}>
+                                        {collectionNameMap.get(collectionID)}
+                                    </Chip>
+                                ))}
+                        </Box>
+                    </InfoItem>
+                )}
+                {appContext.mlSearchEnabled && (
+                    <>
+                        <div>
+                            <Legend>{constants.PEOPLE}</Legend>
+                        </div>
+                        <PhotoPeopleList
+                            file={file}
+                            updateMLDataIndex={updateMLDataIndex}
+                        />
+                        <div>
+                            <Legend>{constants.UNIDENTIFIED_FACES}</Legend>
+                        </div>
+                        <UnidentifiedFaces
+                            file={file}
+                            updateMLDataIndex={updateMLDataIndex}
+                        />
+                        <div>
+                            <Legend>{constants.OBJECTS}</Legend>
+                            <ObjectLabelList
+                                file={file}
+                                updateMLDataIndex={updateMLDataIndex}
+                            />
+                        </div>
+                        <div>
+                            <Legend>{constants.TEXT}</Legend>
+                            <WordList
+                                file={file}
+                                updateMLDataIndex={updateMLDataIndex}
+                            />
+                        </div>
+                        <MLServiceFileInfoButton
+                            file={file}
+                            updateMLDataIndex={updateMLDataIndex}
+                            setUpdateMLDataIndex={setUpdateMLDataIndex}
+                        />
+                    </>
+                )}
+            </Stack>
+            <ExifData
+                exif={exif}
+                open={showExif}
+                onClose={closeExif}
+                onInfoClose={handleCloseInfo}
+                filename={file.metadata.title}
+            />
+        </FileInfoSidebar>
+    );
+}

+ 0 - 0
src/components/PhotoSwipe/PhotoSwipe-old.tsx → src/components/PhotoViewer/PhotoSwipe-old.tsx


+ 287 - 80
src/components/PhotoSwipe/index.tsx → src/components/PhotoViewer/index.tsx

@@ -9,26 +9,54 @@ import {
 import { EnteFile } from 'types/file';
 import constants from 'utils/strings/constants';
 import exifr from 'exifr';
-import events from './events';
-import { downloadFile } from 'utils/file';
-import { prettyPrintExif } from 'utils/exif';
+import {
+    downloadFile,
+    copyFileToClipboard,
+    getFileExtension,
+} from 'utils/file';
 import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
 import { logError } from 'utils/sentry';
 
 import { FILE_TYPE } from 'constants/file';
-import { sleep } from 'utils/common';
+import { isClipboardItemPresent } from 'utils/common';
 import { playVideo, pauseVideo } from 'utils/photoFrame';
 import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
 import { AppContext } from 'pages/_app';
-import { FileInfo } from './InfoDialog';
-import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe';
+import { FileInfo } from './FileInfo';
+import {
+    defaultLivePhotoDefaultOptions,
+    photoSwipeV4Events,
+} from 'constants/photoViewer';
 import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
 import DownloadIcon from '@mui/icons-material/Download';
 import InfoIcon from '@mui/icons-material/InfoOutlined';
 import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
 import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
 import ChevronRight from '@mui/icons-material/ChevronRight';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { trashFiles } from 'services/fileService';
+import { getTrashFileMessage } from 'utils/ui';
+import { styled } from '@mui/material';
+import { addLocalLog } from 'utils/logging';
+import ContentCopy from '@mui/icons-material/ContentCopy';
+import ChevronLeft from '@mui/icons-material/ChevronLeft';
+
+interface PhotoswipeFullscreenAPI {
+    enter: () => void;
+    exit: () => void;
+    isFullscreen: () => boolean;
+}
 
+const CaptionContainer = styled('div')(({ theme }) => ({
+    padding: theme.spacing(2),
+    wordBreak: 'break-word',
+    textAlign: 'right',
+    maxWidth: '375px',
+    fontSize: '14px',
+    lineHeight: '17px',
+    backgroundColor: theme.palette.backdrop.light,
+    backdropFilter: `blur(${theme.palette.blur.base})`,
+}));
 interface Iprops {
     isOpen: boolean;
     items: any[];
@@ -38,13 +66,17 @@ interface Iprops {
     id?: string;
     className?: string;
     favItemIds: Set<number>;
+    deletedFileIds: Set<number>;
+    setDeletedFileIds?: (value: Set<number>) => void;
     isSharedCollection: boolean;
     isTrashCollection: boolean;
     enableDownload: boolean;
     isSourceLoaded: boolean;
+    fileToCollectionsMap: Map<number, number[]>;
+    collectionNameMap: Map<number, string>;
 }
 
-function PhotoSwipe(props: Iprops) {
+function PhotoViewer(props: Iprops) {
     const pswpElement = useRef<HTMLDivElement>();
     const [photoSwipe, setPhotoSwipe] =
         useState<Photoswipe<Photoswipe.Options>>();
@@ -52,8 +84,9 @@ function PhotoSwipe(props: Iprops) {
     const { isOpen, items, isSourceLoaded } = props;
     const [isFav, setIsFav] = useState(false);
     const [showInfo, setShowInfo] = useState(false);
-    const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
-    const [exif, setExif] = useState<any>(null);
+    const [exif, setExif] =
+        useState<{ key: string; value: Record<string, any> }>();
+    const exifCopy = useRef(null);
     const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
         defaultLivePhotoDefaultOptions
     );
@@ -63,6 +96,9 @@ function PhotoSwipe(props: Iprops) {
     );
     const appContext = useContext(AppContext);
 
+    const exifExtractionInProgress = useRef<string>(null);
+    const [shouldShowCopyOption] = useState(isClipboardItemPresent());
+
     useEffect(() => {
         if (!pswpElement) return;
         if (isOpen) {
@@ -76,6 +112,57 @@ function PhotoSwipe(props: Iprops) {
         };
     }, [isOpen]);
 
+    useEffect(() => {
+        if (!photoSwipe) return;
+        function handleCopyEvent() {
+            copyToClipboardHelper(photoSwipe.currItem as EnteFile);
+        }
+
+        function handleKeyUp(event: KeyboardEvent) {
+            if (!isOpen || showInfo) {
+                return;
+            }
+
+            addLocalLog(() => 'Event: ' + event.key);
+
+            switch (event.key) {
+                case 'i':
+                case 'I':
+                    setShowInfo(true);
+                    break;
+                case 'Backspace':
+                case 'Delete':
+                    confirmTrashFile(photoSwipe?.currItem as EnteFile);
+                    break;
+                case 'd':
+                case 'D':
+                    downloadFileHelper(photoSwipe?.currItem as EnteFile);
+                    break;
+                case 'f':
+                case 'F':
+                    toggleFullscreen(photoSwipe);
+                    break;
+                case 'l':
+                case 'L':
+                    onFavClick(photoSwipe?.currItem as EnteFile);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        window.addEventListener('keyup', handleKeyUp);
+        if (shouldShowCopyOption) {
+            window.addEventListener('copy', handleCopyEvent);
+        }
+        return () => {
+            window.removeEventListener('keyup', handleKeyUp);
+            if (shouldShowCopyOption) {
+                window.removeEventListener('copy', handleCopyEvent);
+            }
+        };
+    }, [isOpen, photoSwipe, showInfo]);
+
     useEffect(() => {
         updateItems(items);
     }, [items]);
@@ -152,8 +239,12 @@ function PhotoSwipe(props: Iprops) {
         }
     }, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
 
-    function updateFavButton() {
-        setIsFav(isInFav(this?.currItem));
+    useEffect(() => {
+        exifCopy.current = exif;
+    }, [exif]);
+
+    function updateFavButton(file: EnteFile) {
+        setIsFav(isInFav(file));
     }
 
     const openPhotoSwipe = () => {
@@ -198,12 +289,12 @@ function PhotoSwipe(props: Iprops) {
             items,
             options
         );
-        events.forEach((event) => {
+        photoSwipeV4Events.forEach((event) => {
             const callback = props[event];
             if (callback || event === 'destroy') {
                 photoSwipe.listen(event, function (...args) {
                     if (callback) {
-                        args.unshift(this);
+                        args.unshift(photoSwipe);
                         callback(...args);
                     }
                     if (event === 'destroy') {
@@ -215,11 +306,39 @@ function PhotoSwipe(props: Iprops) {
                 });
             }
         });
-        photoSwipe.listen('beforeChange', function () {
-            updateInfo.call(this);
-            updateFavButton.call(this);
+        photoSwipe.listen('beforeChange', () => {
+            const currItem = photoSwipe?.currItem as EnteFile;
+            updateFavButton(currItem);
+            if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
+                setExif({ key: currItem.src, value: null });
+                return;
+            }
+            if (
+                !currItem ||
+                !exifCopy?.current?.value === null ||
+                exifCopy?.current?.key === currItem.src
+            ) {
+                return;
+            }
+            setExif({ key: currItem.src, value: undefined });
+            checkExifAvailable(currItem);
+        });
+        photoSwipe.listen('resize', () => {
+            const currItem = photoSwipe?.currItem as EnteFile;
+            if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
+                setExif({ key: currItem.src, value: null });
+                return;
+            }
+            if (
+                !currItem ||
+                !exifCopy?.current?.value === null ||
+                exifCopy?.current?.key === currItem.src
+            ) {
+                return;
+            }
+            setExif({ key: currItem.src, value: undefined });
+            checkExifAvailable(currItem);
         });
-        photoSwipe.listen('resize', checkExifAvailable);
         photoSwipe.init();
         needUpdate.current = false;
         setPhotoSwipe(photoSwipe);
@@ -240,7 +359,7 @@ function PhotoSwipe(props: Iprops) {
         }
         handleCloseInfo();
     };
-    const isInFav = (file) => {
+    const isInFav = (file: EnteFile) => {
         const { favItemIds } = props;
         if (favItemIds && file) {
             return favItemIds.has(file.id);
@@ -248,7 +367,7 @@ function PhotoSwipe(props: Iprops) {
         return false;
     };
 
-    const onFavClick = async (file) => {
+    const onFavClick = async (file: EnteFile) => {
         const { favItemIds } = props;
         if (!isInFav(file)) {
             favItemIds.add(file.id);
@@ -262,46 +381,80 @@ function PhotoSwipe(props: Iprops) {
         needUpdate.current = true;
     };
 
+    const trashFile = async (file: EnteFile) => {
+        const { deletedFileIds, setDeletedFileIds } = props;
+        deletedFileIds.add(file.id);
+        setDeletedFileIds(new Set(deletedFileIds));
+        await trashFiles([file]);
+        needUpdate.current = true;
+    };
+
+    const confirmTrashFile = (file: EnteFile) => {
+        if (props.isSharedCollection || props.isTrashCollection) {
+            return;
+        }
+        appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
+    };
+
     const updateItems = (items = []) => {
         if (photoSwipe) {
+            if (items.length === 0) {
+                photoSwipe.close();
+            }
             photoSwipe.items.length = 0;
             items.forEach((item) => {
                 photoSwipe.items.push(item);
             });
             photoSwipe.invalidateCurrItems();
-            // photoSwipe.updateSize(true);
+            if (isOpen) {
+                photoSwipe.updateSize(true);
+                if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
+                    photoSwipe.goTo(0);
+                }
+            }
         }
     };
 
-    const checkExifAvailable = async () => {
-        setExif(null);
-        await sleep(100);
+    const refreshPhotoswipe = () => {
+        photoSwipe.invalidateCurrItems();
+        if (isOpen) {
+            photoSwipe.updateSize(true);
+        }
+    };
+
+    const checkExifAvailable = async (file: EnteFile) => {
         try {
-            const img: HTMLImageElement = document.querySelector(
-                '.pswp__img:not(.pswp__img--placeholder)'
-            );
-            if (img) {
-                const exifData = await exifr.parse(img);
-                if (!exifData) {
-                    return;
+            if (exifExtractionInProgress.current === file.src) {
+                return;
+            }
+            try {
+                if (file.isSourceLoaded) {
+                    exifExtractionInProgress.current = file.src;
+                    const imageBlob = await (
+                        await fetch(file.originalImageURL)
+                    ).blob();
+                    const exifData = (await exifr.parse(imageBlob)) as Record<
+                        string,
+                        any
+                    >;
+                    if (exifExtractionInProgress.current === file.src) {
+                        if (exifData) {
+                            setExif({ key: file.src, value: exifData });
+                        } else {
+                            setExif({ key: file.src, value: null });
+                        }
+                    }
                 }
-                exifData.raw = prettyPrintExif(exifData);
-                setExif(exifData);
+            } finally {
+                exifExtractionInProgress.current = null;
             }
         } catch (e) {
-            logError(e, 'exifr parsing failed');
+            setExif({ key: file.src, value: null });
+            const fileExtension = getFileExtension(file.metadata.title);
+            logError(e, 'exifr parsing failed', { extension: fileExtension });
         }
     };
 
-    function updateInfo() {
-        const file: EnteFile = this?.currItem;
-        if (file?.metadata) {
-            setMetaData(file.metadata);
-            setExif(null);
-            checkExifAvailable();
-        }
-    }
-
     const handleCloseInfo = () => {
         setShowInfo(false);
     };
@@ -310,15 +463,37 @@ function PhotoSwipe(props: Iprops) {
     };
 
     const downloadFileHelper = async (file) => {
-        appContext.startLoading();
-        await downloadFile(
-            file,
-            publicCollectionGalleryContext.accessedThroughSharedURL,
-            publicCollectionGalleryContext.token,
-            publicCollectionGalleryContext.passwordToken
-        );
+        if (props.enableDownload) {
+            appContext.startLoading();
+            await downloadFile(
+                file,
+                publicCollectionGalleryContext.accessedThroughSharedURL,
+                publicCollectionGalleryContext.token,
+                publicCollectionGalleryContext.passwordToken
+            );
+            appContext.finishLoading();
+        }
+    };
+
+    const copyToClipboardHelper = async (file: EnteFile) => {
+        if (props.enableDownload && shouldShowCopyOption) {
+            appContext.startLoading();
+            await copyFileToClipboard(file.src);
+            appContext.finishLoading();
+        }
+    };
 
-        appContext.finishLoading();
+    const toggleFullscreen = (photoSwipe) => {
+        const fullScreenApi: PhotoswipeFullscreenAPI =
+            photoSwipe?.ui?.getFullscreenAPI();
+        if (!fullScreenApi) {
+            return;
+        }
+        if (fullScreenApi.isFullscreen()) {
+            fullScreenApi.exit();
+        } else {
+            fullScreenApi.enter();
+        }
     };
     const scheduleUpdate = () => (needUpdate.current = true);
     const { id } = props;
@@ -355,33 +530,74 @@ function PhotoSwipe(props: Iprops) {
 
                             <button
                                 className="pswp__button pswp__button--close"
-                                title={constants.CLOSE}
+                                title={constants.CLOSE_OPTION}
                             />
 
                             {props.enableDownload && (
                                 <button
                                     className="pswp__button pswp__button--custom"
-                                    title={constants.DOWNLOAD}
+                                    title={constants.DOWNLOAD_OPTION}
                                     onClick={() =>
                                         downloadFileHelper(photoSwipe.currItem)
                                     }>
                                     <DownloadIcon fontSize="small" />
                                 </button>
                             )}
-                            <button
-                                className="pswp__button pswp__button--fs"
-                                title={constants.TOGGLE_FULLSCREEN}
-                            />
+                            {props.enableDownload && shouldShowCopyOption && (
+                                <button
+                                    className="pswp__button pswp__button--custom"
+                                    title={constants.COPY_OPTION}
+                                    onClick={() =>
+                                        copyToClipboardHelper(
+                                            photoSwipe.currItem as EnteFile
+                                        )
+                                    }>
+                                    <ContentCopy fontSize="small" />
+                                </button>
+                            )}
+                            {!props.isSharedCollection &&
+                                !props.isTrashCollection && (
+                                    <button
+                                        className="pswp__button pswp__button--custom"
+                                        title={constants.DELETE_OPTION}
+                                        onClick={() => {
+                                            confirmTrashFile(
+                                                photoSwipe?.currItem as EnteFile
+                                            );
+                                        }}>
+                                        <DeleteIcon fontSize="small" />
+                                    </button>
+                                )}
                             <button
                                 className="pswp__button pswp__button--zoom"
                                 title={constants.ZOOM_IN_OUT}
                             />
+                            <button
+                                className="pswp__button pswp__button--fs"
+                                title={constants.TOGGLE_FULLSCREEN}
+                            />
+
+                            {!props.isSharedCollection && (
+                                <button
+                                    className="pswp__button pswp__button--custom"
+                                    title={constants.INFO_OPTION}
+                                    onClick={handleOpenInfo}>
+                                    <InfoIcon fontSize="small" />
+                                </button>
+                            )}
                             {!props.isSharedCollection &&
                                 !props.isTrashCollection && (
                                     <button
+                                        title={
+                                            isFav
+                                                ? constants.UNFAVORITE_OPTION
+                                                : constants.FAVORITE_OPTION
+                                        }
                                         className="pswp__button pswp__button--custom"
                                         onClick={() => {
-                                            onFavClick(photoSwipe?.currItem);
+                                            onFavClick(
+                                                photoSwipe?.currItem as EnteFile
+                                            );
                                         }}>
                                         {isFav ? (
                                             <FavoriteIcon fontSize="small" />
@@ -390,14 +606,7 @@ function PhotoSwipe(props: Iprops) {
                                         )}
                                     </button>
                                 )}
-                            {!props.isSharedCollection && (
-                                <button
-                                    className="pswp__button pswp__button--custom"
-                                    title={constants.INFO}
-                                    onClick={handleOpenInfo}>
-                                    <InfoIcon fontSize="small" />
-                                </button>
-                            )}
+
                             <div className="pswp__preloader">
                                 <div className="pswp__preloader__icn">
                                     <div className="pswp__preloader__cut">
@@ -411,36 +620,34 @@ function PhotoSwipe(props: Iprops) {
                         </div>
                         <button
                             className="pswp__button pswp__button--arrow--left"
-                            title={constants.PREVIOUS}
-                            onClick={photoSwipe?.prev}>
-                            <ChevronRight
-                                sx={{ transform: 'rotate(180deg)' }}
-                            />
+                            title={constants.PREVIOUS}>
+                            <ChevronLeft sx={{ pointerEvents: 'none' }} />
                         </button>
                         <button
                             className="pswp__button pswp__button--arrow--right"
-                            title={constants.NEXT}
-                            onClick={photoSwipe?.next}>
-                            <ChevronRight />
+                            title={constants.NEXT}>
+                            <ChevronRight sx={{ pointerEvents: 'none' }} />
                         </button>
-                        <div className="pswp__caption">
-                            <div />
+                        <div className="pswp__caption pswp-custom-caption-container">
+                            <CaptionContainer />
                         </div>
                     </div>
                 </div>
             </div>
             <FileInfo
+                isTrashCollection={props.isTrashCollection}
                 shouldDisableEdits={props.isSharedCollection}
                 showInfo={showInfo}
                 handleCloseInfo={handleCloseInfo}
-                items={items}
-                photoSwipe={photoSwipe}
-                metadata={metadata}
-                exif={exif}
+                file={photoSwipe?.currItem as EnteFile}
+                exif={exif?.value}
                 scheduleUpdate={scheduleUpdate}
+                refreshPhotoswipe={refreshPhotoswipe}
+                fileToCollectionsMap={props.fileToCollectionsMap}
+                collectionNameMap={props.collectionNameMap}
             />
         </>
     );
 }
 
-export default PhotoSwipe;
+export default PhotoViewer;

+ 0 - 0
src/components/PhotoSwipe/styledComponents/Legend.tsx → src/components/PhotoViewer/styledComponents/Legend.tsx


+ 0 - 0
src/components/PhotoSwipe/styledComponents/LegendContainer.tsx → src/components/PhotoViewer/styledComponents/LegendContainer.tsx


+ 0 - 0
src/components/PhotoSwipe/styledComponents/LivePhotoBtn.tsx → src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx


+ 0 - 0
src/components/PhotoSwipe/styledComponents/Pre.tsx → src/components/PhotoViewer/styledComponents/Pre.tsx


+ 0 - 0
src/components/PhotoSwipe/styledComponents/SmallLoadingSpinner.tsx → src/components/PhotoViewer/styledComponents/SmallLoadingSpinner.tsx


+ 6 - 1
src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx

@@ -1,6 +1,5 @@
 import React, { useContext } from 'react';
 import { PeopleList } from 'components/MachineLearning/PeopleList';
-import { Legend } from 'components/PhotoSwipe/styledComponents/Legend';
 import { IndexStatus } from 'types/machineLearning/ui';
 import { SuggestionType, Suggestion } from 'types/search';
 import { components } from 'react-select';
@@ -18,6 +17,12 @@ const LegendRow = styled(Row)`
     margin-bottom: 0px;
 `;
 
+const Legend = styled('span')`
+    font-size: 20px;
+    color: #ddd;
+    display: inline;
+`;
+
 const Caption = styled('span')`
     font-size: 12px;
     display: inline;

+ 3 - 1
src/components/Search/SearchBar/searchInput/index.tsx

@@ -44,7 +44,9 @@ export default function SearchInput(props: Iprops) {
     };
     const [defaultOptions, setDefaultOptions] = useState([]);
 
-    useEffect(() => search(value), [value]);
+    useEffect(() => {
+        search(value);
+    }, [value]);
 
     useEffect(() => {
         refreshDefaultOptions();

+ 0 - 47
src/components/Sidebar/DebugLogs.tsx

@@ -1,47 +0,0 @@
-import { AppContext } from 'pages/_app';
-import React, { useContext } from 'react';
-import { downloadAsFile } from 'utils/file';
-import constants from 'utils/strings/constants';
-import { addLogLine, getDebugLogs } from 'utils/logging';
-import SidebarButton from './Button';
-import { getData, LS_KEYS } from 'utils/storage/localStorage';
-import { User } from 'types/user';
-import { getSentryUserID } from 'utils/user';
-
-export default function DebugLogs() {
-    const appContext = useContext(AppContext);
-    const confirmLogDownload = () =>
-        appContext.setDialogMessage({
-            title: constants.DOWNLOAD_LOGS,
-            content: constants.DOWNLOAD_LOGS_MESSAGE(),
-            proceed: {
-                text: constants.DOWNLOAD,
-                variant: 'accent',
-                action: downloadDebugLogs,
-            },
-            close: {
-                text: constants.CANCEL,
-            },
-        });
-
-    const downloadDebugLogs = () => {
-        addLogLine(
-            'latest commit id :' + process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH
-        );
-        addLogLine(`user sentry id ${getSentryUserID()}`);
-        addLogLine(`ente userID ${(getData(LS_KEYS.USER) as User)?.id}`);
-        addLogLine('exporting logs');
-        const logs = getDebugLogs();
-        const logString = logs.join('\n');
-        downloadAsFile(`debug_logs_${Date.now()}.txt`, logString);
-    };
-
-    return (
-        <SidebarButton
-            onClick={confirmLogDownload}
-            typographyVariant="caption"
-            sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
-            {constants.DOWNLOAD_UPLOAD_LOGS}
-        </SidebarButton>
-    );
-}

+ 84 - 0
src/components/Sidebar/DebugSection.tsx

@@ -0,0 +1,84 @@
+import { AppContext } from 'pages/_app';
+import React, { useContext, useEffect, useState } from 'react';
+import { downloadAsFile } from 'utils/file';
+import constants from 'utils/strings/constants';
+import { addLogLine, getDebugLogs } from 'utils/logging';
+import SidebarButton from './Button';
+import isElectron from 'is-electron';
+import ElectronService from 'services/electron/common';
+import Typography from '@mui/material/Typography';
+import { isInternalUser } from 'utils/user';
+import { testUpload } from '../../../tests/upload.test';
+import {
+    testZipFileReading,
+    testZipWithRootFileReadingTest,
+} from '../../../tests/zip-file-reading.test';
+
+export default function DebugSection() {
+    const appContext = useContext(AppContext);
+    const [appVersion, setAppVersion] = useState<string>(null);
+
+    useEffect(() => {
+        const main = async () => {
+            if (isElectron()) {
+                const appVersion = await ElectronService.getAppVersion();
+                setAppVersion(appVersion);
+            }
+        };
+        main();
+    });
+
+    const confirmLogDownload = () =>
+        appContext.setDialogMessage({
+            title: constants.DOWNLOAD_LOGS,
+            content: constants.DOWNLOAD_LOGS_MESSAGE(),
+            proceed: {
+                text: constants.DOWNLOAD,
+                variant: 'accent',
+                action: downloadDebugLogs,
+            },
+            close: {
+                text: constants.CANCEL,
+            },
+        });
+
+    const downloadDebugLogs = () => {
+        addLogLine('exporting logs');
+        if (isElectron()) {
+            ElectronService.openLogDirectory();
+        } else {
+            const logs = getDebugLogs();
+
+            downloadAsFile(`debug_logs_${Date.now()}.txt`, logs);
+        }
+    };
+
+    return (
+        <>
+            <SidebarButton
+                onClick={confirmLogDownload}
+                typographyVariant="caption"
+                sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
+                {constants.DOWNLOAD_UPLOAD_LOGS}
+            </SidebarButton>
+            {appVersion && (
+                <Typography p={1.5} color="text.secondary" variant="caption">
+                    {appVersion}
+                </Typography>
+            )}
+            {isInternalUser() && (
+                <>
+                    <SidebarButton onClick={testUpload}>
+                        Test Upload
+                    </SidebarButton>
+                    <SidebarButton onClick={testZipFileReading}>
+                        Test Zip file reading
+                    </SidebarButton>
+                    <SidebarButton onClick={testZipWithRootFileReadingTest}>
+                        Zip with Root file Test
+                    </SidebarButton>
+                </>
+            )}
+        </>
+    );
+}

+ 2 - 2
src/components/Sidebar/HelpSection.tsx

@@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app';
 import EnteSpinner from 'components/EnteSpinner';
 import { getDownloadAppMessage } from 'utils/ui';
 import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
+import { openLink } from 'utils/common';
 
 export default function HelpSection() {
     const [exportModalView, setExportModalView] = useState(false);
@@ -20,8 +21,7 @@ export default function HelpSection() {
         const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
             getToken()
         )}`;
-        const win = window.open(feedbackURL, '_blank');
-        win.focus();
+        openLink(feedbackURL, true);
     }
 
     function exportFiles() {

+ 4 - 4
src/components/Sidebar/ShortcutSection.tsx

@@ -2,10 +2,10 @@ import React, { useContext } from 'react';
 import constants from 'utils/strings/constants';
 import { GalleryContext } from 'pages/gallery';
 import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
-import DeleteIcon from '@mui/icons-material/Delete';
-import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
 import { CollectionSummaries } from 'types/collection';
 import ShortcutButton from './ShortcutButton';
+import DeleteOutline from '@mui/icons-material/DeleteOutline';
+import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
 interface Iprops {
     closeSidebar: () => void;
     collectionSummaries: CollectionSummaries;
@@ -30,13 +30,13 @@ export default function ShortcutSection({
     return (
         <>
             <ShortcutButton
-                startIcon={<DeleteIcon />}
+                startIcon={<DeleteOutline />}
                 label={constants.TRASH}
                 count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
                 onClick={openTrashSection}
             />
             <ShortcutButton
-                startIcon={<VisibilityOffIcon />}
+                startIcon={<ArchiveOutlined />}
                 label={constants.ARCHIVE_SECTION_NAME}
                 count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
                 onClick={openArchiveSection}

+ 2 - 2
src/components/Sidebar/SubscriptionCard/contentOverlay/family/usageSection/progressBar.tsx

@@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({
         <Box position={'relative'} width="100%">
             <Progressbar
                 sx={{ backgroundColor: 'transparent' }}
-                value={(userUsage * 100) / totalStorage}
+                value={Math.min((userUsage * 100) / totalStorage, 100)}
             />
             <Progressbar
                 sx={{
@@ -28,7 +28,7 @@ export function FamilyUsageProgressBar({
                     },
                     width: '100%',
                 }}
-                value={(totalUsage * 100) / totalStorage}
+                value={Math.min((totalUsage * 100) / totalStorage, 100)}
             />
         </Box>
     );

+ 1 - 1
src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx

@@ -13,7 +13,7 @@ interface Iprops {
 export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
     return (
         <Box width="100%">
-            <Progressbar value={(usage * 100) / storage} />
+            <Progressbar value={Math.min((usage * 100) / storage, 100)} />
             <SpaceBetweenFlex
                 sx={{
                     marginTop: 1.5,

+ 17 - 17
src/components/Sidebar/SubscriptionStatus/index.tsx

@@ -1,5 +1,5 @@
 import { GalleryContext } from 'pages/gallery';
-import React, { useContext, useMemo } from 'react';
+import React, { MouseEventHandler, useContext, useMemo } from 'react';
 import {
     hasPaidSubscription,
     isFamilyAdmin,
@@ -43,19 +43,23 @@ export default function SubscriptionStatus({
     }, [userDetails]);
 
     const handleClick = useMemo(() => {
-        if (userDetails) {
-            if (isSubscriptionActive(userDetails.subscription)) {
-                if (hasExceededStorageQuota(userDetails)) {
-                    return showPlanSelectorModal;
-                }
-            } else {
-                if (hasStripeSubscription(userDetails.subscription)) {
-                    return billingService.redirectToCustomerPortal;
+        const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
+            e.stopPropagation();
+            if (userDetails) {
+                if (isSubscriptionActive(userDetails.subscription)) {
+                    if (hasExceededStorageQuota(userDetails)) {
+                        showPlanSelectorModal();
+                    }
                 } else {
-                    return showPlanSelectorModal;
+                    if (hasStripeSubscription(userDetails.subscription)) {
+                        billingService.redirectToCustomerPortal();
+                    } else {
+                        showPlanSelectorModal();
+                    }
                 }
             }
-        }
+        };
+        return eventHandler;
     }, [userDetails]);
 
     if (!hasAMessage) {
@@ -80,13 +84,9 @@ export default function SubscriptionStatus({
                           )
                         : hasExceededStorageQuota(userDetails) &&
                           constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO(
-                              showPlanSelectorModal
+                              handleClick
                           )
-                    : constants.SUBSCRIPTION_EXPIRED_MESSAGE(
-                          hasStripeSubscription(userDetails.subscription)
-                              ? billingService.redirectToCustomerPortal
-                              : showPlanSelectorModal
-                      )}
+                    : constants.SUBSCRIPTION_EXPIRED_MESSAGE(handleClick)}
             </Typography>
         </Box>
     );

+ 2 - 2
src/components/Sidebar/index.tsx

@@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
 import UtilitySection from './UtilitySection';
 import HelpSection from './HelpSection';
 import ExitSection from './ExitSection';
-import DebugLogs from './DebugLogs';
+import DebugSection from './DebugSection';
 import { DrawerSidebar } from './styledComponents';
 import HeaderSection from './Header';
 import { CollectionSummaries } from 'types/collection';
@@ -37,7 +37,7 @@ export default function Sidebar({
                 <Divider />
                 <ExitSection />
                 <Divider />
-                <DebugLogs />
+                <DebugSection />
             </Stack>
         </DrawerSidebar>
     );

+ 3 - 5
src/components/Sidebar/styledComponents.tsx

@@ -1,11 +1,9 @@
-import { Drawer, styled } from '@mui/material';
+import { styled } from '@mui/material';
 import CircleIcon from '@mui/icons-material/Circle';
+import { EnteDrawer } from 'components/EnteDrawer';
 
-export const DrawerSidebar = styled(Drawer)(({ theme }) => ({
+export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
     '& .MuiPaper-root': {
-        maxWidth: '375px',
-        width: '100%',
-        scrollbarWidth: 'thin',
         padding: theme.spacing(1.5),
     },
 }));

+ 20 - 23
src/components/SignUp.tsx

@@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) {
         { email, passphrase, confirm }: FormValues,
         { setFieldError }: FormikHelpers<FormValues>
     ) => {
-        setLoading(true);
         try {
+            if (passphrase !== confirm) {
+                setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
+                return;
+            }
+            setLoading(true);
             try {
                 setData(LS_KEYS.USER, { email });
                 await sendOtt(email);
@@ -60,30 +64,23 @@ export default function SignUp(props: SignUpProps) {
                 throw e;
             }
             try {
-                if (passphrase === confirm) {
-                    const { keyAttributes, masterKey } =
-                        await generateKeyAttributes(passphrase);
-                    setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
-                    await generateAndSaveIntermediateKeyAttributes(
-                        passphrase,
-                        keyAttributes,
-                        masterKey
-                    );
+                const { keyAttributes, masterKey } =
+                    await generateKeyAttributes(passphrase);
+                setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
+                await generateAndSaveIntermediateKeyAttributes(
+                    passphrase,
+                    keyAttributes,
+                    masterKey
+                );
 
-                    await saveKeyInSessionStore(
-                        SESSION_KEYS.ENCRYPTION_KEY,
-                        masterKey
-                    );
-                    setJustSignedUp(true);
-                    router.push(PAGES.VERIFY);
-                } else {
-                    setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
-                }
-            } catch (e) {
-                setFieldError(
-                    'passphrase',
-                    constants.PASSWORD_GENERATION_FAILED
+                await saveKeyInSessionStore(
+                    SESSION_KEYS.ENCRYPTION_KEY,
+                    masterKey
                 );
+                setJustSignedUp(true);
+                router.push(PAGES.VERIFY);
+            } catch (e) {
+                setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
                 throw e;
             }
         } catch (err) {

+ 36 - 5
src/components/SingleInputForm.tsx

@@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton';
 import TextField from '@mui/material/TextField';
 import ShowHidePassword from './Form/ShowHidePassword';
 import { FlexWrapper } from './Container';
-import { Button } from '@mui/material';
+import { Button, FormHelperText } from '@mui/material';
 
 interface formValues {
     inputValue: string;
@@ -24,8 +24,11 @@ export interface SingleInputFormProps {
     secondaryButtonAction?: () => void;
     disableAutoFocus?: boolean;
     hiddenPreInput?: any;
+    caption?: any;
     hiddenPostInput?: any;
     autoComplete?: string;
+    blockButton?: boolean;
+    hiddenLabel?: boolean;
 }
 
 export default function SingleInputForm(props: SingleInputFormProps) {
@@ -86,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
                 <form noValidate onSubmit={handleSubmit}>
                     {props.hiddenPreInput}
                     <TextField
+                        hiddenLabel={props.hiddenLabel}
                         variant="filled"
                         fullWidth
                         type={showPassword ? 'text' : props.fieldType}
                         id={props.fieldType}
                         name={props.fieldType}
-                        label={props.placeholder}
+                        {...(props.hiddenLabel
+                            ? { placeholder: props.placeholder }
+                            : { label: props.placeholder })}
                         value={values.inputValue}
                         onChange={handleChange('inputValue')}
                         error={Boolean(errors.inputValue)}
@@ -113,20 +119,45 @@ export default function SingleInputForm(props: SingleInputFormProps) {
                             ),
                         }}
                     />
+                    <FormHelperText
+                        sx={{
+                            position: 'relative',
+                            top: errors.inputValue ? '-22px' : '0',
+                            float: 'right',
+                            padding: '0 8px',
+                        }}>
+                        {props.caption}
+                    </FormHelperText>
                     {props.hiddenPostInput}
-                    <FlexWrapper justifyContent={'flex-end'}>
+                    <FlexWrapper
+                        justifyContent={'flex-end'}
+                        flexWrap={
+                            props.blockButton ? 'wrap-reverse' : 'nowrap'
+                        }>
                         {props.secondaryButtonAction && (
                             <Button
                                 onClick={props.secondaryButtonAction}
                                 size="large"
                                 color="secondary"
-                                sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }}
+                                sx={{
+                                    '&&&': {
+                                        mt: !props.blockButton ? 2 : 0.5,
+                                        mb: !props.blockButton ? 4 : 0,
+                                        mr: !props.blockButton ? 1 : 0,
+                                        ...buttonSx,
+                                    },
+                                }}
                                 {...restSubmitButtonProps}>
                                 {constants.CANCEL}
                             </Button>
                         )}
                         <SubmitButton
-                            sx={{ mt: 2, ...buttonSx }}
+                            sx={{
+                                '&&&': {
+                                    mt: 2,
+                                    ...buttonSx,
+                                },
+                            }}
                             buttonText={props.buttonText}
                             loading={loading}
                             {...restSubmitButtonProps}

+ 9 - 4
src/components/SubmitButton.tsx

@@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
             disabled={disabled || loading || success}
             sx={{
                 my: 4,
-                '&.Mui-disabled': {
-                    backgroundColor: (theme) => theme.palette.accent.main,
-                    color: (theme) => theme.palette.text.primary,
-                },
+                ...(loading
+                    ? {
+                          '&.Mui-disabled': {
+                              backgroundColor: (theme) =>
+                                  theme.palette.accent.main,
+                              color: (theme) => theme.palette.text.primary,
+                          },
+                      }
+                    : {}),
                 ...sx,
             }}
             {...props}>

+ 57 - 0
src/components/Titlebar.tsx

@@ -0,0 +1,57 @@
+import Close from '@mui/icons-material/Close';
+import ArrowBack from '@mui/icons-material/ArrowBack';
+import { Box, IconButton, Typography } from '@mui/material';
+import React from 'react';
+import { FlexWrapper } from './Container';
+
+interface Iprops {
+    title: string;
+    caption?: string;
+    onClose: () => void;
+    backIsClose?: boolean;
+    onRootClose?: () => void;
+    actionButton?: JSX.Element;
+}
+
+export default function Titlebar({
+    title,
+    caption,
+    onClose,
+    backIsClose,
+    actionButton,
+    onRootClose,
+}: Iprops): JSX.Element {
+    return (
+        <>
+            <FlexWrapper
+                height={48}
+                alignItems={'center'}
+                justifyContent="space-between">
+                <IconButton
+                    onClick={onClose}
+                    color={backIsClose ? 'secondary' : 'primary'}>
+                    {backIsClose ? <Close /> : <ArrowBack />}
+                </IconButton>
+                <Box display={'flex'} gap="4px">
+                    {actionButton && actionButton}
+                    {!backIsClose && (
+                        <IconButton onClick={onRootClose} color={'secondary'}>
+                            <Close />
+                        </IconButton>
+                    )}
+                </Box>
+            </FlexWrapper>
+            <Box py={0.5} px={2}>
+                <Typography variant="h3" fontWeight={'bold'}>
+                    {title}
+                </Typography>
+                <Typography
+                    variant="body2"
+                    color="text.secondary"
+                    sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
+                    {caption}
+                </Typography>
+            </Box>
+        </>
+    );
+}

+ 21 - 8
src/components/Upload/UploadButton.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
-import { IconButton, styled } from '@mui/material';
+import { ButtonProps, IconButton, styled } from '@mui/material';
 import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
 import { Button } from '@mui/material';
 import constants from 'utils/strings/constants';
 import uploadManager from 'services/upload/uploadManager';
 
-const Wrapper = styled('div')`
+const Wrapper = styled('div')<{ $disableShrink: boolean }>`
     display: flex;
     align-items: center;
     justify-content: center;
@@ -14,22 +14,35 @@ const Wrapper = styled('div')`
     & .mobile-button {
         display: none;
     }
-    @media (max-width: 624px) {
+    ${({ $disableShrink }) =>
+        !$disableShrink &&
+        `@media (max-width: 624px) {
         & .mobile-button {
             display: block;
         }
         & .desktop-button {
             display: none;
         }
-    }
+    }`}
 `;
 
 interface Iprops {
     openUploader: () => void;
+    text?: string;
+    color?: ButtonProps['color'];
+    disableShrink?: boolean;
+    icon?: JSX.Element;
 }
-function UploadButton({ openUploader }: Iprops) {
+function UploadButton({
+    openUploader,
+    text,
+    color,
+    disableShrink,
+    icon,
+}: Iprops) {
     return (
         <Wrapper
+            $disableShrink={disableShrink}
             style={{
                 cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
             }}>
@@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) {
                 onClick={openUploader}
                 disabled={!uploadManager.shouldAllowNewUpload()}
                 className="desktop-button"
-                color="secondary"
-                startIcon={<FileUploadOutlinedIcon />}>
-                {constants.UPLOAD}
+                color={color ?? 'secondary'}
+                startIcon={icon ?? <FileUploadOutlinedIcon />}>
+                {text ?? constants.UPLOAD}
             </Button>
 
             <IconButton

+ 78 - 61
src/components/Upload/UploadProgress/dialog.tsx

@@ -7,9 +7,10 @@ import { UploadProgressHeader } from './header';
 import { InProgressSection } from './inProgressSection';
 import { ResultSection } from './resultSection';
 import { NotUploadSectionHeader } from './styledComponents';
-import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
 import UploadProgressContext from 'contexts/uploadProgress';
 import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
+import { APP_DOWNLOAD_URL } from 'utils/common';
+import { ENTE_WEBSITE_LINK } from 'constants/urls';
 
 export function UploadProgressDialog() {
     const { open, onClose, uploadStage, finishedUploads } = useContext(
@@ -26,7 +27,8 @@ export function UploadProgressDialog() {
             finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
                 ?.length > 0 ||
             finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 ||
-            finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0
+            finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 ||
+            finishedUploads.get(UPLOAD_RESULT.SKIPPED_VIDEOS)?.length > 0
         ) {
             setHasUnUploadedFiles(true);
         } else {
@@ -40,70 +42,85 @@ export function UploadProgressDialog() {
         <Dialog maxWidth="xs" open={open} onClose={handleClose}>
             <UploadProgressHeader />
             {(uploadStage === UPLOAD_STAGES.UPLOADING ||
-                uploadStage === UPLOAD_STAGES.FINISH) && (
+                uploadStage === UPLOAD_STAGES.FINISH ||
+                uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
                 <DialogContent sx={{ '&&&': { px: 0 } }}>
-                    {uploadStage === UPLOAD_STAGES.UPLOADING && (
+                    {(uploadStage === UPLOAD_STAGES.UPLOADING ||
+                        uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
                         <InProgressSection />
                     )}
+                    {(uploadStage === UPLOAD_STAGES.UPLOADING ||
+                        uploadStage === UPLOAD_STAGES.FINISH) && (
+                        <>
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.UPLOADED}
+                                sectionTitle={constants.SUCCESSFUL_UPLOADS}
+                            />
+                            <ResultSection
+                                uploadResult={
+                                    UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
+                                }
+                                sectionTitle={
+                                    constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
+                                }
+                                sectionInfo={
+                                    constants.THUMBNAIL_GENERATION_FAILED_INFO
+                                }
+                            />
 
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.UPLOADED}
-                        sectionTitle={constants.SUCCESSFUL_UPLOADS}
-                    />
-                    <ResultSection
-                        uploadResult={
-                            UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
-                        }
-                        sectionTitle={
-                            constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
-                        }
-                        sectionInfo={constants.THUMBNAIL_GENERATION_FAILED_INFO}
-                    />
+                            {uploadStage === UPLOAD_STAGES.FINISH &&
+                                hasUnUploadedFiles && (
+                                    <NotUploadSectionHeader>
+                                        {constants.FILE_NOT_UPLOADED_LIST}
+                                    </NotUploadSectionHeader>
+                                )}
 
-                    {uploadStage === UPLOAD_STAGES.FINISH &&
-                        hasUnUploadedFiles && (
-                            <NotUploadSectionHeader>
-                                {constants.FILE_NOT_UPLOADED_LIST}
-                            </NotUploadSectionHeader>
-                        )}
-
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.BLOCKED}
-                        sectionTitle={constants.BLOCKED_UPLOADS}
-                        sectionInfo={constants.ETAGS_BLOCKED(
-                            getOSSpecificDesktopAppDownloadLink()
-                        )}
-                    />
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.FAILED}
-                        sectionTitle={constants.FAILED_UPLOADS}
-                    />
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
-                        sectionTitle={constants.SKIPPED_FILES}
-                        sectionInfo={constants.SKIPPED_INFO}
-                    />
-                    <ResultSection
-                        uploadResult={
-                            UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE
-                        }
-                        sectionTitle={
-                            constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS
-                        }
-                        sectionInfo={
-                            constants.LARGER_THAN_AVAILABLE_STORAGE_INFO
-                        }
-                    />
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.UNSUPPORTED}
-                        sectionTitle={constants.UNSUPPORTED_FILES}
-                        sectionInfo={constants.UNSUPPORTED_INFO}
-                    />
-                    <ResultSection
-                        uploadResult={UPLOAD_RESULT.TOO_LARGE}
-                        sectionTitle={constants.TOO_LARGE_UPLOADS}
-                        sectionInfo={constants.TOO_LARGE_INFO}
-                    />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.BLOCKED}
+                                sectionTitle={constants.BLOCKED_UPLOADS}
+                                sectionInfo={constants.ETAGS_BLOCKED(
+                                    APP_DOWNLOAD_URL
+                                )}
+                            />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.FAILED}
+                                sectionTitle={constants.FAILED_UPLOADS}
+                            />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
+                                sectionTitle={constants.SKIPPED_VIDEOS}
+                                sectionInfo={constants.SKIPPED_VIDEOS_INFO(
+                                    ENTE_WEBSITE_LINK
+                                )}
+                            />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
+                                sectionTitle={constants.SKIPPED_FILES}
+                                sectionInfo={constants.SKIPPED_INFO}
+                            />
+                            <ResultSection
+                                uploadResult={
+                                    UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE
+                                }
+                                sectionTitle={
+                                    constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS
+                                }
+                                sectionInfo={
+                                    constants.LARGER_THAN_AVAILABLE_STORAGE_INFO
+                                }
+                            />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.UNSUPPORTED}
+                                sectionTitle={constants.UNSUPPORTED_FILES}
+                                sectionInfo={constants.UNSUPPORTED_INFO}
+                            />
+                            <ResultSection
+                                uploadResult={UPLOAD_RESULT.TOO_LARGE}
+                                sectionTitle={constants.TOO_LARGE_UPLOADS}
+                                sectionInfo={constants.TOO_LARGE_INFO}
+                            />
+                        </>
+                    )}
                 </DialogContent>
             )}
             {uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}

+ 14 - 7
src/components/Upload/UploadProgress/inProgressSection.tsx

@@ -10,17 +10,19 @@ import {
 } from './section';
 import UploadProgressContext from 'contexts/uploadProgress';
 import constants from 'utils/strings/constants';
+import { UPLOAD_STAGES } from 'constants/upload';
 
 export const InProgressSection = () => {
-    const { inProgressUploads, hasLivePhotos, uploadFileNames } = useContext(
-        UploadProgressContext
-    );
+    const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } =
+        useContext(UploadProgressContext);
     const fileList = inProgressUploads ?? [];
 
     return (
-        <UploadProgressSection defaultExpanded>
+        <UploadProgressSection>
             <UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
-                {constants.INPROGRESS_UPLOADS}
+                {uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
+                    ? constants.INPROGRESS_METADATA_EXTRACTION
+                    : constants.INPROGRESS_UPLOADS}
             </UploadProgressSectionTitle>
             <UploadProgressSectionContent>
                 {hasLivePhotos && (
@@ -30,8 +32,13 @@ export const InProgressSection = () => {
                     fileList={fileList.map(({ localFileID, progress }) => (
                         <InProgressItemContainer key={localFileID}>
                             <span>{uploadFileNames.get(localFileID)}</span>
-                            <span className="separator">{`-`}</span>
-                            <span>{`${progress}%`}</span>
+                            {uploadStage === UPLOAD_STAGES.UPLOADING && (
+                                <>
+                                    {' '}
+                                    <span className="separator">{`-`}</span>
+                                    <span>{`${progress}%`}</span>
+                                </>
+                            )}
                         </InProgressItemContainer>
                     ))}
                 />

+ 2 - 0
src/components/Upload/UploadProgress/title.tsx

@@ -20,6 +20,8 @@ function UploadProgressSubtitleText() {
     return (
         <Typography color="text.secondary">
             {uploadStage === UPLOAD_STAGES.UPLOADING
+                ? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
+                : uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
                 ? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
                 : constants.UPLOAD_STAGE_MESSAGE[uploadStage]}
         </Typography>

+ 45 - 12
src/components/Upload/UploadTypeSelector/index.tsx

@@ -1,19 +1,48 @@
-import React from 'react';
+import React, { useContext, useEffect, useRef } from 'react';
 import constants from 'utils/strings/constants';
 import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
 import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
 import GoogleIcon from '@mui/icons-material/Google';
 import { UploadTypeOption } from './option';
-import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
+import DialogTitleWithCloseButton, {
+    dialogCloseHandler,
+} from 'components/DialogBox/TitleWithCloseButton';
 import { Box, Dialog, Stack, Typography } from '@mui/material';
+import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
+import { isMobileOrTable } from 'utils/common/deviceDetection';
 
+interface Iprops {
+    onClose: () => void;
+    show: boolean;
+    uploadFiles: () => void;
+    uploadFolders: () => void;
+    uploadGoogleTakeoutZips: () => void;
+    hideZipUploadOption?: boolean;
+}
 export default function UploadTypeSelector({
-    onHide,
+    onClose,
     show,
     uploadFiles,
     uploadFolders,
     uploadGoogleTakeoutZips,
-}) {
+    hideZipUploadOption,
+}: Iprops) {
+    const publicCollectionGalleryContext = useContext(
+        PublicCollectionGalleryContext
+    );
+    const directlyShowUploadFiles = useRef(isMobileOrTable());
+
+    useEffect(() => {
+        if (
+            show &&
+            directlyShowUploadFiles.current &&
+            publicCollectionGalleryContext.accessedThroughSharedURL
+        ) {
+            uploadFiles();
+            onClose();
+        }
+    }, [show]);
+
     return (
         <Dialog
             open={show}
@@ -24,9 +53,11 @@ export default function UploadTypeSelector({
                     [theme.breakpoints.down(360)]: { p: 0 },
                 }),
             }}
-            onClose={onHide}>
-            <DialogTitleWithCloseButton onClose={onHide}>
-                {constants.UPLOAD}
+            onClose={dialogCloseHandler({ onClose })}>
+            <DialogTitleWithCloseButton onClose={onClose}>
+                {publicCollectionGalleryContext.accessedThroughSharedURL
+                    ? constants.SELECT_PHOTOS
+                    : constants.UPLOAD}
             </DialogTitleWithCloseButton>
             <Box p={1.5} pt={0.5}>
                 <Stack spacing={0.5}>
@@ -40,11 +71,13 @@ export default function UploadTypeSelector({
                         startIcon={<FolderUploadIcon />}>
                         {constants.UPLOAD_DIRS}
                     </UploadTypeOption>
-                    <UploadTypeOption
-                        onClick={uploadGoogleTakeoutZips}
-                        startIcon={<GoogleIcon />}>
-                        {constants.UPLOAD_GOOGLE_TAKEOUT}
-                    </UploadTypeOption>
+                    {!hideZipUploadOption && (
+                        <UploadTypeOption
+                            onClick={uploadGoogleTakeoutZips}
+                            startIcon={<GoogleIcon />}>
+                            {constants.UPLOAD_GOOGLE_TAKEOUT}
+                        </UploadTypeOption>
+                    )}
                 </Stack>
                 <Typography p={1.5} pt={4} color="text.secondary">
                     {constants.DRAG_AND_DROP_HINT}

+ 245 - 84
src/components/Upload/Uploader.tsx

@@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
 import { GalleryContext } from 'pages/gallery';
 import { AppContext } from 'pages/_app';
 import { logError } from 'utils/sentry';
-import UploadManager from 'services/upload/uploadManager';
 import uploadManager from 'services/upload/uploadManager';
 import ImportService from 'services/importService';
 import isElectron from 'is-electron';
@@ -40,7 +39,10 @@ import {
     PICKED_UPLOAD_TYPE,
 } from 'constants/upload';
 import importService from 'services/importService';
-import { getDownloadAppMessage } from 'utils/ui';
+import {
+    getDownloadAppMessage,
+    getRootLevelFileWithFolderNotAllowMessage,
+} from 'utils/ui';
 import UploadTypeSelector from './UploadTypeSelector';
 import {
     filterOutSystemFiles,
@@ -49,21 +51,29 @@ import {
 } from 'utils/upload';
 import { getUserOwnedCollections } from 'utils/collection';
 import billingService from 'services/billingService';
+import { addLogLine } from 'utils/logging';
+import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
+import UserNameInputDialog from 'components/UserNameInputDialog';
+import {
+    getPublicCollectionUID,
+    getPublicCollectionUploaderName,
+    savePublicCollectionUploaderName,
+} from 'services/publicCollectionService';
 
 const FIRST_ALBUM_NAME = 'My First Album';
 
 interface Props {
     syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
-    closeCollectionSelector: () => void;
+    closeCollectionSelector?: () => void;
     closeUploadTypeSelector: () => void;
-    setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
-    setCollectionNamerAttributes: SetCollectionNamerAttributes;
+    setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
+    setCollectionNamerAttributes?: SetCollectionNamerAttributes;
     setLoading: SetLoading;
     setShouldDisableDropzone: (value: boolean) => void;
-    showCollectionSelector: () => void;
+    showCollectionSelector?: () => void;
     setFiles: SetFiles;
-    setCollections: SetCollections;
-    isFirstUpload: boolean;
+    setCollections?: SetCollections;
+    isFirstUpload?: boolean;
     uploadTypeSelectorView: boolean;
     showSessionExpiredMessage: () => void;
     showUploadFilesDialog: () => void;
@@ -71,9 +81,17 @@ interface Props {
     webFolderSelectorFiles: File[];
     webFileSelectorFiles: File[];
     dragAndDropFiles: File[];
+    zipUploadDisabled?: boolean;
+    uploadCollection?: Collection;
 }
 
 export default function Uploader(props: Props) {
+    const appContext = useContext(AppContext);
+    const galleryContext = useContext(GalleryContext);
+    const publicCollectionGalleryContext = useContext(
+        PublicCollectionGalleryContext
+    );
+
     const [uploadProgressView, setUploadProgressView] = useState(false);
     const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
         UPLOAD_STAGES.START
@@ -92,11 +110,13 @@ export default function Uploader(props: Props) {
     const [hasLivePhotos, setHasLivePhotos] = useState(false);
 
     const [choiceModalView, setChoiceModalView] = useState(false);
+    const [userNameInputDialogView, setUserNameInputDialogView] =
+        useState(false);
     const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
         DEFAULT_IMPORT_SUGGESTION
     );
-    const appContext = useContext(AppContext);
-    const galleryContext = useContext(GalleryContext);
+    const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
+    const [webFiles, setWebFiles] = useState([]);
 
     const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
     const isPendingDesktopUpload = useRef(false);
@@ -105,20 +125,33 @@ export default function Uploader(props: Props) {
     const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
     const zipPaths = useRef<string[]>(null);
     const currentUploadPromise = useRef<Promise<void>>(null);
-    const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
-    const [webFiles, setWebFiles] = useState([]);
+    const uploadRunning = useRef(false);
+    const uploaderNameRef = useRef<string>(null);
 
     const closeUploadProgress = () => setUploadProgressView(false);
+    const showUserNameInputDialog = () => setUserNameInputDialogView(true);
 
     const setCollectionName = (collectionName: string) => {
         isPendingDesktopUpload.current = true;
         pendingDesktopUploadCollectionName.current = collectionName;
     };
 
-    const uploadRunning = useRef(false);
+    const handleChoiceModalClose = () => {
+        setChoiceModalView(false);
+        uploadRunning.current = false;
+    };
+    const handleCollectionSelectorCancel = () => {
+        uploadRunning.current = false;
+        appContext.resetSharedFiles();
+    };
+
+    const handleUserNameInputDialogClose = () => {
+        setUserNameInputDialogView(false);
+        uploadRunning.current = false;
+    };
 
     useEffect(() => {
-        UploadManager.init(
+        uploadManager.init(
             {
                 setPercentComplete,
                 setUploadCounter,
@@ -128,12 +161,16 @@ export default function Uploader(props: Props) {
                 setUploadFilenames: setUploadFileNames,
                 setHasLivePhotos,
             },
-            props.setFiles
+            props.setFiles,
+            publicCollectionGalleryContext
         );
 
         if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
             ImportService.getPendingUploads().then(
                 ({ files: electronFiles, collectionName, type }) => {
+                    addLogLine(
+                        `found pending desktop upload, resuming uploads`
+                    );
                     resumeDesktopUpload(type, electronFiles, collectionName);
                 }
             );
@@ -144,7 +181,11 @@ export default function Uploader(props: Props) {
                 appContext.setIsFolderSyncRunning
             );
         }
-    }, []);
+    }, [
+        publicCollectionGalleryContext.accessedThroughSharedURL,
+        publicCollectionGalleryContext.token,
+        publicCollectionGalleryContext.passwordToken,
+    ]);
 
     // this handles the change of selectorFiles changes on web when user selects
     // files for upload through the opened file/folder selector or dragAndDrop them
@@ -159,13 +200,16 @@ export default function Uploader(props: Props) {
             pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
             props.webFolderSelectorFiles?.length > 0
         ) {
+            addLogLine(`received folder upload request`);
             setWebFiles(props.webFolderSelectorFiles);
         } else if (
             pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
             props.webFileSelectorFiles?.length > 0
         ) {
+            addLogLine(`received file upload request`);
             setWebFiles(props.webFileSelectorFiles);
         } else if (props.dragAndDropFiles?.length > 0) {
+            addLogLine(`received drag and drop upload request`);
             setWebFiles(props.dragAndDropFiles);
         }
     }, [
@@ -180,17 +224,37 @@ export default function Uploader(props: Props) {
             webFiles?.length > 0 ||
             appContext.sharedFiles?.length > 0
         ) {
+            addLogLine(
+                `upload request type:${
+                    electronFiles?.length > 0
+                        ? 'electronFiles'
+                        : webFiles?.length > 0
+                        ? 'webFiles'
+                        : 'sharedFiles'
+                } count ${
+                    electronFiles?.length ??
+                    webFiles?.length ??
+                    appContext?.sharedFiles.length
+                }`
+            );
             if (uploadRunning.current) {
                 if (watchFolderService.isUploadRunning()) {
+                    addLogLine(
+                        'watchFolder upload was running, pausing it to run user upload'
+                    );
                     // pause watch folder service on user upload
                     watchFolderService.pauseRunningSync();
                 } else {
+                    addLogLine(
+                        'an upload is already running, rejecting new upload request'
+                    );
                     // no-op
                     // a user upload is already in progress
                     return;
                 }
             }
             if (isCanvasBlocked()) {
+                addLogLine('canvas blocked, blocking upload');
                 appContext.setDialogMessage({
                     title: constants.CANVAS_BLOCKED_TITLE,
 
@@ -235,7 +299,8 @@ export default function Uploader(props: Props) {
             handleCollectionCreationAndUpload(
                 importSuggestion,
                 props.isFirstUpload,
-                pickedUploadType.current
+                pickedUploadType.current,
+                publicCollectionGalleryContext.accessedThroughSharedURL
             );
             pickedUploadType.current = null;
             props.setLoading(false);
@@ -256,14 +321,20 @@ export default function Uploader(props: Props) {
     };
 
     const preCollectionCreationAction = async () => {
-        props.closeCollectionSelector();
+        props.closeCollectionSelector?.();
         props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
         setUploadStage(UPLOAD_STAGES.START);
         setUploadProgressView(true);
     };
 
-    const uploadFilesToExistingCollection = async (collection: Collection) => {
+    const uploadFilesToExistingCollection = async (
+        collection: Collection,
+        uploaderName?: string
+    ) => {
         try {
+            addLogLine(
+                `upload file to an existing collection - "${collection.name}"`
+            );
             await preCollectionCreationAction();
             const filesWithCollectionToUpload: FileWithCollection[] =
                 toUploadFiles.current.map((file, index) => ({
@@ -271,10 +342,11 @@ export default function Uploader(props: Props) {
                     localID: index,
                     collectionID: collection.id,
                 }));
-            waitInQueueAndUploadFiles(filesWithCollectionToUpload, [
-                collection,
-            ]);
-            toUploadFiles.current = null;
+            waitInQueueAndUploadFiles(
+                filesWithCollectionToUpload,
+                [collection],
+                uploaderName
+            );
         } catch (e) {
             logError(e, 'Failed to upload files to existing collections');
         }
@@ -285,8 +357,11 @@ export default function Uploader(props: Props) {
         collectionName?: string
     ) => {
         try {
+            addLogLine(
+                `upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
+            );
             await preCollectionCreationAction();
-            const filesWithCollectionToUpload: FileWithCollection[] = [];
+            let filesWithCollectionToUpload: FileWithCollection[] = [];
             const collections: Collection[] = [];
             let collectionNameToFilesMap = new Map<
                 string,
@@ -302,6 +377,9 @@ export default function Uploader(props: Props) {
                     toUploadFiles.current
                 );
             }
+            addLogLine(
+                `upload collections - [${[...collectionNameToFilesMap.keys()]}]`
+            );
             try {
                 const existingCollection = getUserOwnedCollections(
                     await syncCollections()
@@ -320,13 +398,14 @@ export default function Uploader(props: Props) {
                         ...existingCollection,
                         ...collections,
                     ]);
-                    filesWithCollectionToUpload.push(
+                    filesWithCollectionToUpload = [
+                        ...filesWithCollectionToUpload,
                         ...files.map((file) => ({
                             localID: index++,
                             collectionID: collection.id,
                             file,
-                        }))
-                    );
+                        })),
+                    ];
                 }
             } catch (e) {
                 closeUploadProgress();
@@ -348,13 +427,18 @@ export default function Uploader(props: Props) {
 
     const waitInQueueAndUploadFiles = (
         filesWithCollectionToUploadIn: FileWithCollection[],
-        collections: Collection[]
+        collections: Collection[],
+        uploaderName?: string
     ) => {
         const currentPromise = currentUploadPromise.current;
         currentUploadPromise.current = waitAndRun(
             currentPromise,
             async () =>
-                await uploadFiles(filesWithCollectionToUploadIn, collections)
+                await uploadFiles(
+                    filesWithCollectionToUploadIn,
+                    collections,
+                    uploaderName
+                )
         );
     };
 
@@ -372,9 +456,11 @@ export default function Uploader(props: Props) {
 
     const uploadFiles = async (
         filesWithCollectionToUploadIn: FileWithCollection[],
-        collections: Collection[]
+        collections: Collection[],
+        uploaderName?: string
     ) => {
         try {
+            addLogLine('uploadFiles called');
             preUploadAction();
             if (
                 isElectron() &&
@@ -399,7 +485,8 @@ export default function Uploader(props: Props) {
             const shouldCloseUploadProgress =
                 await uploadManager.queueFilesForUpload(
                     filesWithCollectionToUploadIn,
-                    collections
+                    collections,
+                    uploaderName
                 );
             if (shouldCloseUploadProgress) {
                 closeUploadProgress();
@@ -416,6 +503,7 @@ export default function Uploader(props: Props) {
                 }
             }
         } catch (err) {
+            logError(err, 'failed to upload files');
             showUserFacingError(err.message);
             closeUploadProgress();
             throw err;
@@ -426,14 +514,18 @@ export default function Uploader(props: Props) {
 
     const retryFailed = async () => {
         try {
+            addLogLine('user retrying failed  upload');
             const filesWithCollections =
-                await uploadManager.getFailedFilesWithCollections();
+                uploadManager.getFailedFilesWithCollections();
+            const uploaderName = uploadManager.getUploaderName();
             await preUploadAction();
             await uploadManager.queueFilesForUpload(
                 filesWithCollections.files,
-                filesWithCollections.collections
+                filesWithCollections.collections,
+                uploaderName
             );
         } catch (err) {
+            logError(err, 'retry failed files failed');
             showUserFacingError(err.message);
             closeUploadProgress();
         } finally {
@@ -449,35 +541,33 @@ export default function Uploader(props: Props) {
             case CustomError.SUBSCRIPTION_EXPIRED:
                 notification = {
                     variant: 'danger',
-                    message: constants.SUBSCRIPTION_EXPIRED,
-                    action: {
-                        text: constants.RENEW_NOW,
-                        callback: billingService.redirectToCustomerPortal,
-                    },
+                    subtext: constants.SUBSCRIPTION_EXPIRED,
+                    message: constants.RENEW_NOW,
+                    onClick: () => billingService.redirectToCustomerPortal(),
                 };
                 break;
             case CustomError.STORAGE_QUOTA_EXCEEDED:
                 notification = {
                     variant: 'danger',
-                    message: constants.STORAGE_QUOTA_EXCEEDED,
-                    action: {
-                        text: constants.UPGRADE_NOW,
-                        callback: galleryContext.showPlanSelectorModal,
-                    },
-                    icon: <DiscFullIcon fontSize="large" />,
+                    subtext: constants.STORAGE_QUOTA_EXCEEDED,
+                    message: constants.UPGRADE_NOW,
+                    onClick: () => galleryContext.showPlanSelectorModal(),
+                    startIcon: <DiscFullIcon />,
                 };
                 break;
             default:
                 notification = {
                     variant: 'danger',
                     message: constants.UNKNOWN_ERROR,
+                    onClick: () => null,
                 };
         }
-        galleryContext.setNotificationAttributes(notification);
+        appContext.setNotificationAttributes(notification);
     }
 
     const uploadToSingleNewCollection = (collectionName: string) => {
         if (collectionName) {
+            addLogLine(`upload to single collection - "${collectionName}"`);
             uploadFilesToNewCollections(
                 UPLOAD_STRATEGY.SINGLE_COLLECTION,
                 collectionName
@@ -495,45 +585,75 @@ export default function Uploader(props: Props) {
         });
     };
 
-    const handleCollectionCreationAndUpload = (
+    const handleCollectionCreationAndUpload = async (
         importSuggestion: ImportSuggestion,
         isFirstUpload: boolean,
-        pickedUploadType: PICKED_UPLOAD_TYPE
+        pickedUploadType: PICKED_UPLOAD_TYPE,
+        accessedThroughSharedURL?: boolean
     ) => {
-        if (isPendingDesktopUpload.current) {
-            isPendingDesktopUpload.current = false;
-            if (pendingDesktopUploadCollectionName.current) {
-                uploadToSingleNewCollection(
-                    pendingDesktopUploadCollectionName.current
+        try {
+            if (accessedThroughSharedURL) {
+                addLogLine(
+                    `uploading files to pulbic collection - ${props.uploadCollection.name}  - ${props.uploadCollection.id}`
                 );
-                pendingDesktopUploadCollectionName.current = null;
-            } else {
+                const uploaderName = await getPublicCollectionUploaderName(
+                    getPublicCollectionUID(publicCollectionGalleryContext.token)
+                );
+                uploaderNameRef.current = uploaderName;
+                showUserNameInputDialog();
+                return;
+            }
+            if (isPendingDesktopUpload.current) {
+                isPendingDesktopUpload.current = false;
+                if (pendingDesktopUploadCollectionName.current) {
+                    addLogLine(
+                        `upload pending files to collection - ${pendingDesktopUploadCollectionName.current}`
+                    );
+                    uploadToSingleNewCollection(
+                        pendingDesktopUploadCollectionName.current
+                    );
+                    pendingDesktopUploadCollectionName.current = null;
+                } else {
+                    addLogLine(
+                        `pending upload - strategy - "multiple collections" `
+                    );
+                    uploadFilesToNewCollections(
+                        UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
+                    );
+                }
+                return;
+            }
+            if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
+                addLogLine('uploading zip files');
                 uploadFilesToNewCollections(
                     UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
                 );
+                return;
             }
-            return;
-        }
-        if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
-            uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
-            return;
-        }
-        if (isFirstUpload && !importSuggestion.rootFolderName) {
-            importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
-        }
-        let showNextModal = () => {};
-        if (importSuggestion.hasNestedFolders) {
-            showNextModal = () => setChoiceModalView(true);
-        } else {
-            showNextModal = () =>
-                uploadToSingleNewCollection(importSuggestion.rootFolderName);
+            if (isFirstUpload && !importSuggestion.rootFolderName) {
+                importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
+            }
+            let showNextModal = () => {};
+            if (importSuggestion.hasNestedFolders) {
+                addLogLine(`nested folders detected`);
+                showNextModal = () => setChoiceModalView(true);
+            } else {
+                showNextModal = () =>
+                    uploadToSingleNewCollection(
+                        importSuggestion.rootFolderName
+                    );
+            }
+            props.setCollectionSelectorAttributes({
+                callback: uploadFilesToExistingCollection,
+                onCancel: handleCollectionSelectorCancel,
+                showNextModal,
+                title: constants.UPLOAD_TO_COLLECTION,
+            });
+        } catch (e) {
+            logError(e, 'handleCollectionCreationAndUpload failed');
         }
-        props.setCollectionSelectorAttributes({
-            callback: uploadFilesToExistingCollection,
-            showNextModal,
-            title: constants.UPLOAD_TO_COLLECTION,
-        });
     };
+
     const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => {
         let files: ElectronFile[];
         pickedUploadType.current = type;
@@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
             zipPaths.current = response.zipPaths;
         }
         if (files?.length > 0) {
+            addLogLine(
+                ` desktop upload for type:${type} and fileCount: ${files?.length} requested`
+            );
             setElectronFiles(files);
             props.closeUploadTypeSelector();
         }
@@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
     const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
     const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS);
 
+    const handlePublicUpload = async (
+        uploaderName: string,
+        skipSave?: boolean
+    ) => {
+        try {
+            if (!skipSave) {
+                savePublicCollectionUploaderName(
+                    getPublicCollectionUID(
+                        publicCollectionGalleryContext.token
+                    ),
+                    uploaderName
+                );
+            }
+            await uploadFilesToExistingCollection(
+                props.uploadCollection,
+                uploaderName
+            );
+        } catch (e) {
+            logError(e, 'public upload failed ');
+        }
+    };
+
+    const handleUploadToSingleCollection = () => {
+        uploadToSingleNewCollection(importSuggestion.rootFolderName);
+    };
+
+    const handleUploadToMultipleCollections = () => {
+        if (importSuggestion.hasRootLevelFileWithFolder) {
+            appContext.setDialogMessage(
+                getRootLevelFileWithFolderNotAllowMessage()
+            );
+            return;
+        }
+        uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
+    };
+
     return (
         <>
             <UploadStrategyChoiceModal
                 open={choiceModalView}
-                onClose={() => setChoiceModalView(false)}
-                uploadToSingleCollection={() =>
-                    uploadToSingleNewCollection(importSuggestion.rootFolderName)
-                }
-                uploadToMultipleCollection={() =>
-                    uploadFilesToNewCollections(
-                        UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
-                    )
-                }
+                onClose={handleChoiceModalClose}
+                uploadToSingleCollection={handleUploadToSingleCollection}
+                uploadToMultipleCollection={handleUploadToMultipleCollections}
             />
             <UploadTypeSelector
                 show={props.uploadTypeSelectorView}
-                onHide={props.closeUploadTypeSelector}
+                onClose={props.closeUploadTypeSelector}
                 uploadFiles={handleFileUpload}
                 uploadFolders={handleFolderUpload}
                 uploadGoogleTakeoutZips={handleZipUpload}
+                hideZipUploadOption={props.zipUploadDisabled}
             />
             <UploadProgress
                 open={uploadProgressView}
@@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
                 finishedUploads={finishedUploads}
                 cancelUploads={cancelUploads}
             />
+            <UserNameInputDialog
+                open={userNameInputDialogView}
+                onClose={handleUserNameInputDialogClose}
+                onNameSubmit={handlePublicUpload}
+                toUploadFilesCount={toUploadFiles.current?.length}
+                uploaderName={uploaderNameRef.current}
+            />
         </>
     );
 }

+ 43 - 0
src/components/UserNameInputDialog.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import constants from 'utils/strings/constants';
+import DialogBox from './DialogBox';
+import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
+import { Typography } from '@mui/material';
+import SingleInputForm from './SingleInputForm';
+
+export default function UserNameInputDialog({
+    open,
+    onClose,
+    onNameSubmit,
+    toUploadFilesCount,
+    uploaderName,
+}) {
+    const handleSubmit = async (inputValue: string) => {
+        onClose();
+        await onNameSubmit(inputValue);
+    };
+    return (
+        <DialogBox
+            size="xs"
+            open={open}
+            onClose={onClose}
+            attributes={{
+                title: constants.ENTER_NAME,
+                icon: <AutoAwesomeOutlinedIcon />,
+            }}>
+            <Typography color={'text.secondary'} pb={1}>
+                {constants.PUBLIC_UPLOADER_NAME_MESSAGE}
+            </Typography>
+            <SingleInputForm
+                hiddenLabel
+                initialValue={uploaderName}
+                callback={handleSubmit}
+                placeholder={constants.NAME_PLACEHOLDER}
+                buttonText={constants.ADD_X_PHOTOS(toUploadFilesCount)}
+                fieldType="text"
+                blockButton
+                secondaryButtonAction={onClose}
+            />
+        </DialogBox>
+    );
+}

+ 3 - 3
src/components/VerifyMasterPasswordForm.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 
 import constants from 'utils/strings/constants';
-import CryptoWorker from 'utils/crypto';
 import SingleInputForm, {
     SingleInputFormProps,
 } from 'components/SingleInputForm';
@@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
 
 import { Input } from '@mui/material';
 import { KeyAttributes, User } from 'types/user';
+import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
 
 export interface VerifyMasterPasswordFormProps {
     user: User;
@@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
         setFieldError
     ) => {
         try {
-            const cryptoWorker = await new CryptoWorker();
+            const cryptoWorker = await ComlinkCryptoWorker.getInstance();
             let kek: string = null;
             try {
                 kek = await cryptoWorker.deriveKey(
@@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
                 throw Error(CustomError.WEAK_DEVICE);
             }
             try {
-                const key: string = await cryptoWorker.decryptB64(
+                const key = await cryptoWorker.decryptB64(
                     keyAttributes.encryptedKey,
                     keyAttributes.keyDecryptionNonce,
                     kek

+ 1 - 4
src/components/WatchFolder/styledComponents.tsx

@@ -3,15 +3,12 @@ import { Box } from '@mui/material';
 import { styled } from '@mui/material/styles';
 import VerticallyCentered from 'components/Container';
 
-export const MappingsContainer = styled(Box)(({ theme }) => ({
+export const MappingsContainer = styled(Box)(() => ({
     height: '278px',
     overflow: 'auto',
     '&::-webkit-scrollbar': {
         width: '4px',
     },
-    '&::-webkit-scrollbar-thumb': {
-        backgroundColor: theme.palette.secondary.main,
-    },
 }));
 
 export const NoMappingsContainer = styled(VerticallyCentered)({

文件差异内容过多而无法显示
+ 10 - 0
src/components/icons/ente.tsx


+ 3 - 4
src/components/pages/dedupe/SelectedFileOptions.tsx

@@ -2,9 +2,8 @@ import { FluidContainer } from 'components/Container';
 import { SelectionBar } from '../../Navbar/SelectionBar';
 import constants from 'utils/strings/constants';
 import React, { useContext } from 'react';
-import { Box, IconButton, styled } from '@mui/material';
+import { Box, IconButton, styled, Tooltip } from '@mui/material';
 import { DeduplicateContext } from 'pages/deduplicate';
-import { IconWithMessage } from 'components/IconWithMessage';
 import { AppContext } from 'pages/_app';
 import CloseIcon from '@mui/icons-material/Close';
 import BackButton from '@mui/icons-material/ArrowBackOutlined';
@@ -78,11 +77,11 @@ export default function DeduplicateOptions({
             <div>
                 <VerticalLine />
             </div>
-            <IconWithMessage message={constants.DELETE}>
+            <Tooltip title={constants.DELETE}>
                 <IconButton onClick={trashHandler}>
                     <DeleteIcon />
                 </IconButton>
-            </IconWithMessage>
+            </Tooltip>
         </SelectionBar>
     );
 }

+ 2 - 23
src/components/pages/gallery/LinkButton.tsx

@@ -1,34 +1,12 @@
-import Link, { LinkProps } from '@mui/material/Link';
+import { ButtonProps, Link, LinkProps } from '@mui/material';
 import React, { FC } from 'react';
-import { ButtonProps } from 'react-bootstrap';
 
-export enum ButtonVariant {
-    success = 'success',
-    danger = 'danger',
-    secondary = 'secondary',
-    warning = 'warning',
-}
 export type LinkButtonProps = React.PropsWithChildren<{
     onClick: () => void;
     variant?: string;
     style?: React.CSSProperties;
 }>;
 
-export function getVariantColor(variant: string) {
-    switch (variant) {
-        case ButtonVariant.success:
-            return '#51cd7c';
-        case ButtonVariant.danger:
-            return '#c93f3f';
-        case ButtonVariant.secondary:
-            return '#858585';
-        case ButtonVariant.warning:
-            return '#D7BB63';
-        default:
-            return '#d1d1d1';
-    }
-}
-
 const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
     children,
     sx,
@@ -41,6 +19,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
             sx={{
                 color: 'text.primary',
                 textDecoration: 'underline rgba(255, 255, 255, 0.4)',
+                paddingBottom: 0.5,
                 '&:hover': {
                     color: `${color}.main`,
                     textDecoration: `underline `,

+ 1 - 2
src/components/pages/gallery/Navbar.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import NavbarBase from 'components/Navbar/base';
 import SidebarToggler from 'components/Navbar/SidebarToggler';
-import { getNonTrashedUniqueUserFiles } from 'utils/file';
 import SearchBar from 'components/Search/SearchBar';
 import { Collection } from 'types/collection';
 import { EnteFile } from 'types/file';
@@ -36,7 +35,7 @@ export function GalleryNavbar({
                 isInSearchMode={isInSearchMode}
                 setIsInSearchMode={setIsInSearchMode}
                 collections={collections}
-                files={getNonTrashedUniqueUserFiles(files)}
+                files={files}
                 setActiveCollection={setActiveCollection}
                 updateSearch={updateSearch}
             />

+ 14 - 12
src/components/pages/gallery/PlanSelector/card/index.tsx

@@ -13,6 +13,7 @@ import {
     hasPaidSubscription,
     getTotalFamilyUsage,
     isPartOfFamily,
+    isSubscriptionActive,
 } from 'utils/billing';
 import { reverseString } from 'utils/common';
 import { GalleryContext } from 'pages/gallery';
@@ -68,18 +69,19 @@ function PlanSelectorCard(props: Props) {
         const main = async () => {
             try {
                 props.setLoading(true);
-                let plans = await billingService.getPlans();
-
-                const planNotListed =
-                    plans.filter((plan) =>
-                        isUserSubscribedPlan(plan, subscription)
-                    ).length === 0;
-                if (
-                    subscription &&
-                    !isOnFreePlan(subscription) &&
-                    planNotListed
-                ) {
-                    plans = [planForSubscription(subscription), ...plans];
+                const plans = await billingService.getPlans();
+                if (isSubscriptionActive(subscription)) {
+                    const planNotListed =
+                        plans.filter((plan) =>
+                            isUserSubscribedPlan(plan, subscription)
+                        ).length === 0;
+                    if (
+                        subscription &&
+                        !isOnFreePlan(subscription) &&
+                        planNotListed
+                    ) {
+                        plans.push(planForSubscription(subscription));
+                    }
                 }
                 setPlans(plans);
             } catch (e) {

+ 2 - 1
src/components/pages/gallery/PlanSelector/manageSubscription/index.tsx

@@ -2,6 +2,7 @@ import { Stack } from '@mui/material';
 import { AppContext } from 'pages/_app';
 import React, { useContext } from 'react';
 import { Subscription } from 'types/billing';
+import { SetLoading } from 'types/gallery';
 import {
     activateSubscription,
     cancelSubscription,
@@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button';
 interface Iprops {
     subscription: Subscription;
     closeModal: () => void;
-    setLoading: (value: boolean) => void;
+    setLoading: SetLoading;
 }
 
 export function ManageSubscription({

部分文件因为文件数量过多而无法显示