Browse Source

added recorder, drafts for messages

DimaBondarenko 1 year ago
parent
commit
1469cc5269
99 changed files with 1639 additions and 948 deletions
  1. 471 6
      package-lock.json
  2. 3 0
      package.json
  3. 6 65
      src/App.js
  4. 0 31
      src/App.scss
  5. 4 0
      src/actions/actionAboutMe.jsx
  6. 0 27
      src/actions/actionFindUser.jsx
  7. 0 82
      src/actions/actionGetMessageForChat.jsx
  8. 8 7
      src/actions/actionLogin.jsx
  9. 58 52
      src/actions/actionsForChats.jsx
  10. 11 0
      src/actions/actionsForModal.js
  11. 22 2
      src/actions/actionsForUser.jsx
  12. 17 18
      src/actions/actionsMedia.jsx
  13. 102 14
      src/actions/actionsMessages.jsx
  14. 0 1
      src/actions/actionsPromise.jsx
  15. 0 36
      src/components/AppBar/AppBar.jsx
  16. 0 28
      src/components/AppBar/AppBar.style.js
  17. 28 10
      src/components/Aside/Aside.jsx
  18. 27 0
      src/components/Aside/Aside.style.js
  19. 6 12
      src/components/CreateNewChat.jsx
  20. 87 0
      src/components/ChatInfoDrawer/ChatInfoDrawer.jsx
  21. 22 0
      src/components/ChatInfoDrawer/ChatInfoDrawer.style.js
  22. 38 0
      src/components/ChatInfoDrawerMediaList/ChatInfoDrawerMediaList.jsx
  23. 17 0
      src/components/ChatInfoDrawerMediaList/ChatInfoDrawerMediaList.style.js
  24. 11 15
      src/components/ChatInfoModal/ChatInfoModal.jsx
  25. 1 2
      src/components/ChatInfoModal/ChatInfoModal.style.js
  26. 1 3
      src/components/ChatInfoOptions/ChatInfoOptions.jsx
  27. 4 10
      src/components/ChatList/ChatList.jsx
  28. 34 22
      src/components/ChatListItem/ChatListItem.jsx
  29. 14 6
      src/components/ChatListItem/ChatListItem.style.js
  30. 3 14
      src/components/ChatPageData/ChatPageData.jsx
  31. 0 1
      src/components/ChatPageData/ChatPageData.style.js
  32. 15 11
      src/components/ChatPageHeader/ChatPageHeader.jsx
  33. 0 2
      src/components/ChatPageHeader/ChatPageHeader.style.js
  34. 9 24
      src/components/Drawer/UserMenu.jsx
  35. 1 0
      src/components/Drawer/UserMenu.style.js
  36. 2 5
      src/components/DropMediaFilesOptions/DropMediaFilesOptions.jsx
  37. 0 6
      src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.jsx
  38. 0 2
      src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.style.js
  39. 1 6
      src/components/DropMediaItem/DropMediaItem.jsx
  40. 0 3
      src/components/DropZone/DropZoneAvatar.jsx
  41. 3 5
      src/components/DropZone/MessageDropZone/MessageDropZone.jsx
  42. 36 37
      src/components/InputArea/InputArea.jsx
  43. 1 3
      src/components/InputArea/InputArea.style.js
  44. 2 6
      src/components/InputArea/InputAreaMessageEditor.jsx
  45. 12 8
      src/components/MemberList/MemberList.jsx
  46. 2 0
      src/components/MemberList/MemberList.style.js
  47. 15 26
      src/components/Message/Message.jsx
  48. 19 4
      src/components/Message/Message.style.js
  49. 16 0
      src/components/MessageDate/MessagesDate.jsx
  50. 19 0
      src/components/MessageDate/MessagesDate.style.js
  51. 2 1
      src/components/MessageDraft/MessageDraft.style.js
  52. 1 5
      src/components/MessageDraft/MessageEditor.jsx
  53. 2 4
      src/components/MessageDraft/MessageReplyForwarded.jsx
  54. 2 3
      src/components/MessageEditorMediaModal/MessageEditorMediaModal.jsx
  55. 2 3
      src/components/MessageMediaItem/MessageMediaItem.jsx
  56. 1 1
      src/components/MessageMediaItems/MessageAudioItem/AudioItem.jsx
  57. 0 1
      src/components/MessageMediaItems/MessageAudioItem/AudioItem.style.js
  58. 1 2
      src/components/MessageMediaItems/MessageFileItem/MessageFileItem.jsx
  59. 4 2
      src/components/MessageMediaItems/MessageImgItem/MessageImgItem.jsx
  60. 2 1
      src/components/MessageMediaItems/MessageImgItem/MessageImgItem.style.js
  61. 3 2
      src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.jsx
  62. 3 6
      src/components/MessageMediaModal/MessageMediaModal.jsx
  63. 4 9
      src/components/MessageOptions/MessageOptions.jsx
  64. 0 2
      src/components/MessageOptions/MessageOptions.style.js
  65. 63 44
      src/components/MessagesArea/MessagesArea.jsx
  66. 21 14
      src/components/MessagesArea/MessagesArea.style.js
  67. BIN
      src/components/MessagesArea/icons8-scrollBottom.png
  68. 13 15
      src/components/Modal.jsx
  69. 0 19
      src/components/NewChatButton.jsx
  70. 0 0
      src/components/Recorder/Recorder.style.js
  71. 22 0
      src/components/Recorder/RecorderAudio.jsx
  72. 24 0
      src/components/Recorder/RecorderVideo.jsx
  73. 3 3
      src/components/ReplyMessage/ReplyMessage.jsx
  74. 0 3
      src/components/ReplyMessage/ReplyMessage.style.js
  75. 18 0
      src/components/Routes/Routes.jsx
  76. 0 1
      src/components/SearchChatListItem/SearchChatListItem.style.js
  77. 2 8
      src/components/SearchChatModal/SearchChatModal.jsx
  78. 0 2
      src/components/SearchChatModal/SearchChatModal.style.js
  79. 1 3
      src/components/SearchUserInput/SearchUserInput.jsx
  80. 0 2
      src/components/SearchUserItem/SearchUserItem.jsx
  81. 0 2
      src/components/SearchUserItem/SearchUserItem.style.js
  82. 28 0
      src/components/SearchUserModal/SearchUserModal.jsx
  83. 12 1
      src/components/Time/Time.jsx
  84. 0 3
      src/components/UserEditorModal/UserEditorModal.jsx
  85. 31 2
      src/components/UserEditorModal/UserEditorSubModals/UserPasswordEditor.jsx
  86. 0 0
      src/helpers/addUploadDate.js
  87. 0 0
      src/helpers/calculateFileSize.js
  88. 9 0
      src/helpers/jwt.js
  89. 0 0
      src/helpers/saveFile.js
  90. 5 1
      src/helpers/uploadFile.js
  91. 3 2
      src/index.js
  92. 60 42
      src/pages/LoginPage.jsx
  93. 12 26
      src/pages/MainPage.jsx
  94. 100 41
      src/pages/RegisterPage.jsx
  95. 2 10
      src/reducers/authReducer.js
  96. 25 36
      src/reducers/chatReducer.js
  97. 0 14
      src/reducers/modalReducer.js
  98. 0 0
      src/redux/reducers/promiseReducer.js
  99. 10 0
      src/redux/store.js

+ 471 - 6
package-lock.json

@@ -23,7 +23,10 @@
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-dropzone": "^12.0.4",
+        "react-hook-form": "^7.33.1",
         "react-infinite-scroll-component": "^6.1.0",
+        "react-media-recorder": "^1.6.6",
+        "react-recorder-voice": "^1.2.8",
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
         "react-scripts": "5.0.0",
@@ -4982,6 +4985,18 @@
         "node": ">=4"
       }
     },
+    "node_modules/automation-events": {
+      "version": "4.0.19",
+      "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.19.tgz",
+      "integrity": "sha512-e8ALmWdXSkDAtDVkmqLcCDMfMMvHcPTwsg+1GsxEO8+Cyg7FQs7F3VKifzdRTADVU6S1aqlPzDE3+KLFsH0sxA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=12.20.1"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -5508,6 +5523,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/broker-factory": {
+      "version": "3.0.65",
+      "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.0.65.tgz",
+      "integrity": "sha512-0z8YLszaRFN3Q91oPjjSXUXmsRQFqEJN9wduzQh9snlYght+BhnPDaGJ3FSkgepBdmgeNCDwCAmG1WMczYW+sQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "fast-unique-numbers": "^6.0.18",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "node_modules/browser-process-hrtime": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
@@ -5971,6 +5997,20 @@
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
     },
+    "node_modules/compilerr": {
+      "version": "9.0.19",
+      "resolved": "https://registry.npmjs.org/compilerr/-/compilerr-9.0.19.tgz",
+      "integrity": "sha512-SyL2BuRMMXWFXQUxvwZN/BfGGaDMqaWfbv3Y2r1Ig85pgf7UWsju2gl2pNLpehBvQ8oQz2v+eIgE2t4u3N5ZbA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "dashify": "^2.0.0",
+        "indefinite-article": "0.0.2",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=12.20.1"
+      }
+    },
     "node_modules/compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -6616,6 +6656,14 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/dashify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz",
+      "integrity": "sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/data-urls": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
@@ -8090,6 +8138,52 @@
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
     },
+    "node_modules/extendable-media-recorder": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-6.6.7.tgz",
+      "integrity": "sha512-/kEFVMZtJsHiiC0f7bjWRpqF8bAx86LEa9JCNgaedXTDS0AqZtOaj285LUtkpKT6I6rLQMNOkfDCMYjMWNthug==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "media-encoder-host": "^8.0.75",
+        "multi-buffer-data-view": "^3.0.19",
+        "recorder-audio-worklet": "^5.1.24",
+        "standardized-audio-context": "^25.3.28",
+        "subscribable-things": "^2.1.5",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/extendable-media-recorder-wav-encoder": {
+      "version": "7.0.73",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.73.tgz",
+      "integrity": "sha512-tITv5EtY7ZeOKnLEUf74nOmcaGEMalDamqxrnmi2aVK6si45pitUmnRbjYcThykogwlN1zlIuvg153JyCkWTuQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "extendable-media-recorder-wav-encoder-broker": "^7.0.67",
+        "extendable-media-recorder-wav-encoder-worker": "^8.0.66",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/extendable-media-recorder-wav-encoder-broker": {
+      "version": "7.0.67",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.67.tgz",
+      "integrity": "sha512-AXtiOuk2NviYM4H3cSp2bzLPM28pYR5NTJ9Gw7+YWppICqi3PHjXpOG6jHVb/AIvgYG4AX73UuSmJZUmxbtoWA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "extendable-media-recorder-wav-encoder-worker": "^8.0.66",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/extendable-media-recorder-wav-encoder-worker": {
+      "version": "8.0.66",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.66.tgz",
+      "integrity": "sha512-tagYFsYKRVP3Afz7cu57DTEMRBBum1KnSU/bskMCwmfx0vbgwHhrNOct3zwD1MSbCxAgKy1/nsIpcZYMKGmSJw==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "node_modules/extsprintf": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -8139,6 +8233,18 @@
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
     },
+    "node_modules/fast-unique-numbers": {
+      "version": "6.0.18",
+      "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-6.0.18.tgz",
+      "integrity": "sha512-HB9C9Kwn+bcWjjDHzxM8poxm5VjxCvXGkc/XijeeAMGGOlw423QjjqNVcVBx8HLRUAEvhrv6kYoD10x8bXJWuQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=12.20.1"
+      }
+    },
     "node_modules/fastq": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
@@ -9353,6 +9459,11 @@
         "node": ">=0.8.19"
       }
     },
+    "node_modules/indefinite-article": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/indefinite-article/-/indefinite-article-0.0.2.tgz",
+      "integrity": "sha512-Au/2XzRkvxq2J6w5uvSSbBKPZ5kzINx5F2wb0SF8xpRL8BP9Lav81TnRbfPp6p+SYjYxwaaLn4EUwI3/MmYKSw=="
+    },
     "node_modules/indent-string": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -11984,6 +12095,40 @@
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
       "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="
     },
+    "node_modules/media-encoder-host": {
+      "version": "8.0.75",
+      "resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-8.0.75.tgz",
+      "integrity": "sha512-w8ugZFFregPAz5K0Nfr8HtglAHZkf5RmFstGsb6BwbbCIpupc4i7oDfa1yuHf6nlfyjwnTowTKelE/rQuZ/fgA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "media-encoder-host-broker": "^7.0.67",
+        "media-encoder-host-worker": "^9.0.67",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/media-encoder-host-broker": {
+      "version": "7.0.67",
+      "resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-7.0.67.tgz",
+      "integrity": "sha512-50fkSxu24TXdctYcvMRGjtvemBNccE8k7Zl3yyIjVpYRFiqilYN8mkOJ+55RCXoeaDFKjG+XAbxZTURrV5ARcQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "fast-unique-numbers": "^6.0.18",
+        "media-encoder-host-worker": "^9.0.67",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/media-encoder-host-worker": {
+      "version": "9.0.67",
+      "resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-9.0.67.tgz",
+      "integrity": "sha512-ulwF34fIj3Q9Zys0qz/VEciY4W0vD7NUl25j0Ga7B0Rq+mgjvsGaslsRMWYwe49BgkcXTnOGzwtyFKq1G447IA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "extendable-media-recorder-wav-encoder-broker": "^7.0.67",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "node_modules/media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -12344,6 +12489,18 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/multi-buffer-data-view": {
+      "version": "3.0.19",
+      "resolved": "https://registry.npmjs.org/multi-buffer-data-view/-/multi-buffer-data-view-3.0.19.tgz",
+      "integrity": "sha512-w4RRwd+PDVwRsWIV44HQsscNWScunh0OKCDMSbyfyDNzjMnnl+H+TgNBaAdL2dExNxfabhgQYhBm3luehbL/iA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=12.20.1"
+      }
+    },
     "node_modules/multicast-dns": {
       "version": "6.2.3",
       "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
@@ -14701,6 +14858,21 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
       "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
     },
+    "node_modules/react-hook-form": {
+      "version": "7.33.1",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.33.1.tgz",
+      "integrity": "sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==",
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/react-hook-form"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17 || ^18"
+      }
+    },
     "node_modules/react-infinite-scroll-component": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
@@ -14717,6 +14889,20 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
     },
+    "node_modules/react-media-recorder": {
+      "version": "1.6.6",
+      "resolved": "https://registry.npmjs.org/react-media-recorder/-/react-media-recorder-1.6.6.tgz",
+      "integrity": "sha512-VdC4bUINMWJyqOAHw1DaZ8HZhdCyVBK85zJ4cHMo9tsrekui3wq5ZxNtBmNe6nbAFQBTNj/pRnLEsiVrCW+TNQ==",
+      "dependencies": {
+        "extendable-media-recorder": "^6.6.5",
+        "extendable-media-recorder-wav-encoder": "^7.0.68"
+      }
+    },
+    "node_modules/react-recorder-voice": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/react-recorder-voice/-/react-recorder-voice-1.2.8.tgz",
+      "integrity": "sha512-w9Bd1u2QRdtaRygw28Epu0t6/yXF11y6dcUCkVz5wk00J3I19jYAWAI13ka9ohD+KVTw9fxXEwsN4feOcCeLVA=="
+    },
     "node_modules/react-redux": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -15049,6 +15235,30 @@
         "node": ">=8.10.0"
       }
     },
+    "node_modules/recorder-audio-worklet": {
+      "version": "5.1.24",
+      "resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-5.1.24.tgz",
+      "integrity": "sha512-zdl4NHgA4OtIlMRlVrCk4eteqfFIWcAEzmk/JYFybp1kcQ5K43csKIBkdXdyRtqwRVgGM5kLHFcjSKclllvVMw==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "fast-unique-numbers": "^6.0.18",
+        "recorder-audio-worklet-processor": "^4.2.13",
+        "standardized-audio-context": "^25.3.28",
+        "subscribable-things": "^2.1.5",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
+    "node_modules/recorder-audio-worklet-processor": {
+      "version": "4.2.13",
+      "resolved": "https://registry.npmjs.org/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-4.2.13.tgz",
+      "integrity": "sha512-sgK+PlHZO4Q7dfBJb+t51QDsbP49YK/J+YSlpjgrDpU2nrsheefo1WV0xlJxA77LLxF/NukhLUfTlKYedRkShg==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/recursive-readdir": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
@@ -15538,6 +15748,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/rxjs-interop": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/rxjs-interop/-/rxjs-interop-2.0.0.tgz",
+      "integrity": "sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw=="
+    },
     "node_modules/safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -16179,6 +16394,16 @@
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
       "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
     },
+    "node_modules/standardized-audio-context": {
+      "version": "25.3.28",
+      "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.28.tgz",
+      "integrity": "sha512-w3TIDQ2z5iA76A3qFjL/knf45BAwHYAqQmDNlDrlE2El89kFKv43TjiN94N0joiMr7G3CRmO1bOuQ7wopDon9g==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "automation-events": "^4.0.19",
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -16456,6 +16681,16 @@
       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
       "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag=="
     },
+    "node_modules/subscribable-things": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/subscribable-things/-/subscribable-things-2.1.5.tgz",
+      "integrity": "sha512-uxIRhQF+KxhBecK3hHso/6EvPEtMjl5KxOLdo0efzga2Yh/jmCXpmHZOByTV3cfYFlw7AfkBstO6iY350wlj/Q==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "rxjs-interop": "^2.0.0",
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -17032,9 +17267,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
     },
     "node_modules/tsutils": {
       "version": "3.21.0",
@@ -18186,6 +18421,17 @@
         "workbox-core": "6.4.2"
       }
     },
+    "node_modules/worker-factory": {
+      "version": "6.0.65",
+      "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-6.0.65.tgz",
+      "integrity": "sha512-94UnSG5Y164ujaWc7+exuRC9MDci1bfEM8I3qr7k4yjuWa0orgaOgUBwTtdH8HEUtmE9O5O+khAFulGzuief0Q==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "compilerr": "^9.0.19",
+        "fast-unique-numbers": "^6.0.18",
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -21807,6 +22053,15 @@
       "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
       "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
     },
+    "automation-events": {
+      "version": "4.0.19",
+      "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.19.tgz",
+      "integrity": "sha512-e8ALmWdXSkDAtDVkmqLcCDMfMMvHcPTwsg+1GsxEO8+Cyg7FQs7F3VKifzdRTADVU6S1aqlPzDE3+KLFsH0sxA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      }
+    },
     "autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -22218,6 +22473,17 @@
         "fill-range": "^7.0.1"
       }
     },
+    "broker-factory": {
+      "version": "3.0.65",
+      "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.0.65.tgz",
+      "integrity": "sha512-0z8YLszaRFN3Q91oPjjSXUXmsRQFqEJN9wduzQh9snlYght+BhnPDaGJ3FSkgepBdmgeNCDwCAmG1WMczYW+sQ==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "fast-unique-numbers": "^6.0.18",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "browser-process-hrtime": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
@@ -22567,6 +22833,17 @@
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
     },
+    "compilerr": {
+      "version": "9.0.19",
+      "resolved": "https://registry.npmjs.org/compilerr/-/compilerr-9.0.19.tgz",
+      "integrity": "sha512-SyL2BuRMMXWFXQUxvwZN/BfGGaDMqaWfbv3Y2r1Ig85pgf7UWsju2gl2pNLpehBvQ8oQz2v+eIgE2t4u3N5ZbA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "dashify": "^2.0.0",
+        "indefinite-article": "0.0.2",
+        "tslib": "^2.4.0"
+      }
+    },
     "compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -23024,6 +23301,11 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "dashify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dashify/-/dashify-2.0.0.tgz",
+      "integrity": "sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A=="
+    },
     "data-urls": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
@@ -24120,6 +24402,52 @@
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
     },
+    "extendable-media-recorder": {
+      "version": "6.6.7",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-6.6.7.tgz",
+      "integrity": "sha512-/kEFVMZtJsHiiC0f7bjWRpqF8bAx86LEa9JCNgaedXTDS0AqZtOaj285LUtkpKT6I6rLQMNOkfDCMYjMWNthug==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "media-encoder-host": "^8.0.75",
+        "multi-buffer-data-view": "^3.0.19",
+        "recorder-audio-worklet": "^5.1.24",
+        "standardized-audio-context": "^25.3.28",
+        "subscribable-things": "^2.1.5",
+        "tslib": "^2.4.0"
+      }
+    },
+    "extendable-media-recorder-wav-encoder": {
+      "version": "7.0.73",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.73.tgz",
+      "integrity": "sha512-tITv5EtY7ZeOKnLEUf74nOmcaGEMalDamqxrnmi2aVK6si45pitUmnRbjYcThykogwlN1zlIuvg153JyCkWTuQ==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "extendable-media-recorder-wav-encoder-broker": "^7.0.67",
+        "extendable-media-recorder-wav-encoder-worker": "^8.0.66",
+        "tslib": "^2.4.0"
+      }
+    },
+    "extendable-media-recorder-wav-encoder-broker": {
+      "version": "7.0.67",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.67.tgz",
+      "integrity": "sha512-AXtiOuk2NviYM4H3cSp2bzLPM28pYR5NTJ9Gw7+YWppICqi3PHjXpOG6jHVb/AIvgYG4AX73UuSmJZUmxbtoWA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "extendable-media-recorder-wav-encoder-worker": "^8.0.66",
+        "tslib": "^2.4.0"
+      }
+    },
+    "extendable-media-recorder-wav-encoder-worker": {
+      "version": "8.0.66",
+      "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.66.tgz",
+      "integrity": "sha512-tagYFsYKRVP3Afz7cu57DTEMRBBum1KnSU/bskMCwmfx0vbgwHhrNOct3zwD1MSbCxAgKy1/nsIpcZYMKGmSJw==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "extsprintf": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -24162,6 +24490,15 @@
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
     },
+    "fast-unique-numbers": {
+      "version": "6.0.18",
+      "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-6.0.18.tgz",
+      "integrity": "sha512-HB9C9Kwn+bcWjjDHzxM8poxm5VjxCvXGkc/XijeeAMGGOlw423QjjqNVcVBx8HLRUAEvhrv6kYoD10x8bXJWuQ==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      }
+    },
     "fastq": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
@@ -25036,6 +25373,11 @@
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
     },
+    "indefinite-article": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/indefinite-article/-/indefinite-article-0.0.2.tgz",
+      "integrity": "sha512-Au/2XzRkvxq2J6w5uvSSbBKPZ5kzINx5F2wb0SF8xpRL8BP9Lav81TnRbfPp6p+SYjYxwaaLn4EUwI3/MmYKSw=="
+    },
     "indent-string": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@@ -26948,6 +27290,40 @@
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
       "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="
     },
+    "media-encoder-host": {
+      "version": "8.0.75",
+      "resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-8.0.75.tgz",
+      "integrity": "sha512-w8ugZFFregPAz5K0Nfr8HtglAHZkf5RmFstGsb6BwbbCIpupc4i7oDfa1yuHf6nlfyjwnTowTKelE/rQuZ/fgA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "media-encoder-host-broker": "^7.0.67",
+        "media-encoder-host-worker": "^9.0.67",
+        "tslib": "^2.4.0"
+      }
+    },
+    "media-encoder-host-broker": {
+      "version": "7.0.67",
+      "resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-7.0.67.tgz",
+      "integrity": "sha512-50fkSxu24TXdctYcvMRGjtvemBNccE8k7Zl3yyIjVpYRFiqilYN8mkOJ+55RCXoeaDFKjG+XAbxZTURrV5ARcQ==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "fast-unique-numbers": "^6.0.18",
+        "media-encoder-host-worker": "^9.0.67",
+        "tslib": "^2.4.0"
+      }
+    },
+    "media-encoder-host-worker": {
+      "version": "9.0.67",
+      "resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-9.0.67.tgz",
+      "integrity": "sha512-ulwF34fIj3Q9Zys0qz/VEciY4W0vD7NUl25j0Ga7B0Rq+mgjvsGaslsRMWYwe49BgkcXTnOGzwtyFKq1G447IA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "extendable-media-recorder-wav-encoder-broker": "^7.0.67",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -27206,6 +27582,15 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "multi-buffer-data-view": {
+      "version": "3.0.19",
+      "resolved": "https://registry.npmjs.org/multi-buffer-data-view/-/multi-buffer-data-view-3.0.19.tgz",
+      "integrity": "sha512-w4RRwd+PDVwRsWIV44HQsscNWScunh0OKCDMSbyfyDNzjMnnl+H+TgNBaAdL2dExNxfabhgQYhBm3luehbL/iA==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      }
+    },
     "multicast-dns": {
       "version": "6.2.3",
       "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
@@ -28791,6 +29176,12 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
       "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
     },
+    "react-hook-form": {
+      "version": "7.33.1",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.33.1.tgz",
+      "integrity": "sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==",
+      "requires": {}
+    },
     "react-infinite-scroll-component": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
@@ -28804,6 +29195,20 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
     },
+    "react-media-recorder": {
+      "version": "1.6.6",
+      "resolved": "https://registry.npmjs.org/react-media-recorder/-/react-media-recorder-1.6.6.tgz",
+      "integrity": "sha512-VdC4bUINMWJyqOAHw1DaZ8HZhdCyVBK85zJ4cHMo9tsrekui3wq5ZxNtBmNe6nbAFQBTNj/pRnLEsiVrCW+TNQ==",
+      "requires": {
+        "extendable-media-recorder": "^6.6.5",
+        "extendable-media-recorder-wav-encoder": "^7.0.68"
+      }
+    },
+    "react-recorder-voice": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/react-recorder-voice/-/react-recorder-voice-1.2.8.tgz",
+      "integrity": "sha512-w9Bd1u2QRdtaRygw28Epu0t6/yXF11y6dcUCkVz5wk00J3I19jYAWAI13ka9ohD+KVTw9fxXEwsN4feOcCeLVA=="
+    },
     "react-redux": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -29056,6 +29461,30 @@
         "picomatch": "^2.2.1"
       }
     },
+    "recorder-audio-worklet": {
+      "version": "5.1.24",
+      "resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-5.1.24.tgz",
+      "integrity": "sha512-zdl4NHgA4OtIlMRlVrCk4eteqfFIWcAEzmk/JYFybp1kcQ5K43csKIBkdXdyRtqwRVgGM5kLHFcjSKclllvVMw==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "broker-factory": "^3.0.65",
+        "fast-unique-numbers": "^6.0.18",
+        "recorder-audio-worklet-processor": "^4.2.13",
+        "standardized-audio-context": "^25.3.28",
+        "subscribable-things": "^2.1.5",
+        "tslib": "^2.4.0",
+        "worker-factory": "^6.0.65"
+      }
+    },
+    "recorder-audio-worklet-processor": {
+      "version": "4.2.13",
+      "resolved": "https://registry.npmjs.org/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-4.2.13.tgz",
+      "integrity": "sha512-sgK+PlHZO4Q7dfBJb+t51QDsbP49YK/J+YSlpjgrDpU2nrsheefo1WV0xlJxA77LLxF/NukhLUfTlKYedRkShg==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "tslib": "^2.4.0"
+      }
+    },
     "recursive-readdir": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
@@ -29409,6 +29838,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "rxjs-interop": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/rxjs-interop/-/rxjs-interop-2.0.0.tgz",
+      "integrity": "sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw=="
+    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -29926,6 +30360,16 @@
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
       "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
     },
+    "standardized-audio-context": {
+      "version": "25.3.28",
+      "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.28.tgz",
+      "integrity": "sha512-w3TIDQ2z5iA76A3qFjL/knf45BAwHYAqQmDNlDrlE2El89kFKv43TjiN94N0joiMr7G3CRmO1bOuQ7wopDon9g==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "automation-events": "^4.0.19",
+        "tslib": "^2.4.0"
+      }
+    },
     "statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -30125,6 +30569,16 @@
       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
       "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag=="
     },
+    "subscribable-things": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/subscribable-things/-/subscribable-things-2.1.5.tgz",
+      "integrity": "sha512-uxIRhQF+KxhBecK3hHso/6EvPEtMjl5KxOLdo0efzga2Yh/jmCXpmHZOByTV3cfYFlw7AfkBstO6iY350wlj/Q==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "rxjs-interop": "^2.0.0",
+        "tslib": "^2.4.0"
+      }
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -30551,9 +31005,9 @@
       }
     },
     "tslib": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
     },
     "tsutils": {
       "version": "3.21.0",
@@ -31453,6 +31907,17 @@
         "workbox-core": "6.4.2"
       }
     },
+    "worker-factory": {
+      "version": "6.0.65",
+      "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-6.0.65.tgz",
+      "integrity": "sha512-94UnSG5Y164ujaWc7+exuRC9MDci1bfEM8I3qr7k4yjuWa0orgaOgUBwTtdH8HEUtmE9O5O+khAFulGzuief0Q==",
+      "requires": {
+        "@babel/runtime": "^7.18.3",
+        "compilerr": "^9.0.19",
+        "fast-unique-numbers": "^6.0.18",
+        "tslib": "^2.4.0"
+      }
+    },
     "wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

+ 3 - 0
package.json

@@ -18,7 +18,10 @@
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-dropzone": "^12.0.4",
+    "react-hook-form": "^7.33.1",
     "react-infinite-scroll-component": "^6.1.0",
+    "react-media-recorder": "^1.6.6",
+    "react-recorder-voice": "^1.2.8",
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",

+ 6 - 65
src/App.js

@@ -1,96 +1,37 @@
-import { applyMiddleware, combineReducers, createStore } from 'redux';
-import thunk from 'redux-thunk';
 import { Provider,connect } from 'react-redux';
 import './App.scss';
-import { backendURL, gql } from './helpers/gql';
-import { useEffect, useState, useRef } from 'react';
-import { authReducer } from './reducers/authReducer';
-import {promiseReducer } from './reducers/promiseReducer';
-import React, { Component } from 'react';
-import {Router, Route, Link,  Navigate, Switch, Redirect} from 'react-router-dom';
+import React from 'react';
+import {Router, Route, Switch, Redirect} from 'react-router-dom';
 import { createBrowserHistory } from 'history'
-
-
-import LoginPage from './pages/LoginPage';
-import RegisterPage from './pages/RegisterPage';
-import {MainPage} from './pages/MainPage';
-import { actionAuthLogout } from './actions/actionLogin';
-
-import { actionPromise } from './actions/actionsPromise';
-import { socket } from './actions/actionLogin';
-import { actionAboutMe } from './actions/actionAboutMe';
-
-
-
-import {chatReducer} from './reducers/chatReducer';
-import ModalNewChat from './components/CreateNewChat';
-import { modalReducer } from './reducers/modalReducer';
 import { actionGetMessageFromSocket } from './actions/actionsMessages';
 import { actionGetOneChat, actionLeftChat } from './actions/actionsForChats';
-
-
-// import { uploadFile } from '../helpers/uploadFile';
-
+import { store } from './redux/store';
+import Routes from './components/Routes/Routes';
 export const history = createBrowserHistory();
+export const socket = window.io("ws://chat.ed.asmer.org.ua/")
 
-
-
-
-
-
-
-
-
-
-
-
-export const store = createStore(combineReducers({
-		promise: promiseReducer, auth: authReducer, chats: chatReducer, modal: modalReducer
-	}), applyMiddleware(thunk))
 console.log(store.getState())
 store.subscribe(() => console.log(store.getState()));
 
-
 if (localStorage.authToken) socket.emit('jwt', localStorage.authToken)
-
 socket.on('jwt_ok',   data => console.log(data))
 socket.on('jwt_fail', error => console.log(error))
 socket.on('msg', msg => {console.log('from socket msg', msg); store.dispatch(actionGetMessageFromSocket(msg))})
 socket.on('chat', chat => {console.log('from socket chat', chat); if (localStorage.authToken) {socket.emit('jwt', localStorage.authToken)}; store.dispatch(actionGetOneChat(chat._id))})
 socket.on('chat_left', chat => {console.log('from socket chat_left', chat); store.dispatch(actionLeftChat(chat)) });
-socket.on('media', media => console.log(media));
-
 socket.on("connect", () => {
 	console.log(socket.id)
 })
 
 
 
-const AllRoutes = ({auth}) => {
-	return (
-				<Switch>
-					<Route path="/login" component={LoginPage}/>
-					<Route path="/register" component={RegisterPage}/>
-					<Route path="/main" component={MainPage} />
-					<Route exact path="/">{auth ? <Redirect to="/main"/> : <Redirect to="/login" /> }</Route>
-				</Switch>
-			)
-}
-
-const CAllRoutes = connect(state => ({auth : state.auth?.payload}))(AllRoutes)
-
-
-
-
 function App() {
-	
   return (
 	<Router history={history}>
 		<Provider store={store}>
 			<div className="App">
-				<CAllRoutes />
+				<Routes />
 			</div>
-			
 		</Provider>
  	</Router>
   );

+ 0 - 31
src/App.scss

@@ -3,7 +3,6 @@
 }
 
 .App {
-  
   text-align: center;
   font-family: 'Poppins', sans-serif;
   color: #e2e2e2;
@@ -12,31 +11,14 @@
 		display: flex;
 		align-items: center;
 	}
-	// background-color: #a1a1a1;
 	.Form{
 		max-width: 400px;
 		margin: 0 auto;
     	padding: 20px;
 	}
-	
-	
 }
-
-// .color{
-// 	background-color: rgb(138, 150, 95);
-// }
-
 .MainChatModal{
-	// position: absolute;
-	// top: 50%;
-	// left: 50%;
-	// transform: translate(-50%, -50%);
-	// min-height: 10px;
-	// width: 350px;
-	// background-color: rgb(252, 252, 252);
-	// border-radius: 10px;
 	padding: 10px;
-
 	&>div:nth-child(1){
 		display: flex;
 		justify-content: space-between;
@@ -47,7 +29,6 @@
 		justify-content: end;
 	}
 }
-
 .ModalBox{
 	position: absolute;
 	top: 50%;
@@ -57,16 +38,4 @@
 	width: 350px;
 	background-color: rgb(255, 255, 255);
 	border-radius: 10px;
-	
-	// padding: 10px;
 }
-
-
-// .Message{
-// 	// display: inline;
-// 	width: 200px;
-// 	padding: 10px;
-// 	background-color: beige;
-// 	color: #000;
-// 	margin: 20px 0;
-// }

+ 4 - 0
src/actions/actionAboutMe.jsx

@@ -22,6 +22,10 @@ export const actionFindUser = (_id) => (
                 _id
              }
              lastMessage {
+                media{
+                   _id
+                   url
+                }
                 text 
                 createdAt
                 owner{

+ 0 - 27
src/actions/actionFindUser.jsx

@@ -1,27 +0,0 @@
-import { gql } from "../helpers/gql";
-import { actionPromise } from "./actionsPromise";
-
-export const actionFindUser = (value) => 
-    actionPromise('usersList', gql(`query findUser($q: String) {
-        UserFind(query: $q) {
-          login
-          _id
-        nick
-          avatar {
-            _id
-            url
-          }
-        }
-      }`,{
-          q: JSON.stringify([
-              {
-                $or : [{nick: `/${value}/`}]
-              },
-              {
-                sort : [{nick: 1}]
-              }
-          ])
-      } ))
-
-
-      

+ 0 - 82
src/actions/actionGetMessageForChat.jsx

@@ -1,82 +0,0 @@
-import { actionPromise } from "./actionsPromise";
-import { gql } from "../helpers/gql";
-import { actionAddMessages } from "./actionsMessages";
-
-
-export const actionGetMessageForChat = (_id, fromChatItem) => 
-	async (dispatch,getState) => {
-		let messLen = fromChatItem ? 0 : Object.values(getState().chats[_id]?.messages || {}).length; 
-
-		let messages = await dispatch(
-			actionPromise('messages', gql(`query FindMessChat($chat: String) {
-				MessageFind(query: $chat) {
-				  _id
-				  text
-				  createdAt
-				  chat{
-					  _id
-				  }
-				  media {
-					url
-					_id
-					type
-					text
-					originalFileName
-				  }
-				  replyTo{
-					_id
-					text 
-					media {
-						url
-						_id
-						type
-					  }
-					owner {
-						nick
-					  }
-					}
-					
-					replies{
-						_id text 
-					}
-					forwarded{
-						_id
-						text
-						createdAt
-						owner {
-							nick
-						  }
-						  media {
-							originalFileName 
-							url
-							_id
-							type
-						  }
-					}
-					forwardWith{
-						_id
-						text
-					}
-				  owner {
-					_id
-					nick
-					avatar {
-					  url
-					}
-				  }
-				}
-			  }`, {chat: JSON.stringify(
-				  	[{"chat._id": _id}, 
-						{
-                    		sort: [{_id: -1}], limit: [20], skip: [messLen]
-						}
-					]
-				)}
-			))
-		)
-		if(messages){
-            
-			dispatch(actionAddMessages([...messages], _id))
-		}
-		return messages
-	}

+ 8 - 7
src/actions/actionLogin.jsx

@@ -1,10 +1,7 @@
 import { actionPromise } from "./actionsPromise";
 import { gql } from "../helpers/gql";
-import { history } from '../App';
-import { actionAboutMe } from "./actionAboutMe";
-
-export const socket = window.io("ws://chat.ed.asmer.org.ua/")
-
+import { history, socket } from '../App';
+import { actionClearChats } from "./actionsForChats";
 
 const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
 
@@ -13,6 +10,12 @@ export const actionAuthLogout = () => {
   return ({type: 'AUTH_LOGOUT'})
 }
 
+export const actionLogout = () => 
+  (dispatch) => {
+    dispatch(actionAuthLogout());
+    dispatch(actionClearChats());
+  }
+
 export const actionFullLogin = (log, pass) => 
 async (dispatch) => {
 	let token = await dispatch(
@@ -24,7 +27,6 @@ async (dispatch) => {
 	  socket.emit('jwt', token)
 	  dispatch(actionAuthLogin(token))
 	  history.push("/");
-	  
   }
   return token
 }
@@ -36,7 +38,6 @@ async (dispatch) => {
 	  login(login: $login, password: $password)
 	  }`, {login: log, password: pass}))
   );
-  console.log(token)
 }
 
 export const actionFullRegister = (log, pass, nick) => 

+ 58 - 52
src/actions/actionsForChats.jsx

@@ -1,15 +1,26 @@
 import { actionPromise } from "./actionsPromise";
 import { gql } from "../helpers/gql";
 import { actionSetChatAvatar } from "./actionsMedia";
-import { actionAboutMe } from "./actionAboutMe";
-import { createBrowserHistory } from 'history'
 import { history } from "../App";
 
 export const actionAddChats = (data) => ({type: 'CHATS', data})
 export const actionAddChat = (chat) => ({type: 'CHATS', data: [chat]})
 
+export const actionSetDropMedia = (id, data) => ({type: 'SETMEDIA', data, id});
 
+export const actionDeleteDropMedia = (id) => ({type: 'CLEARMEDIA', id});
 
+export const actionDeleteOneMediaFile = (id, mediaKey) => ({type: 'DELETEMEDIA', id, mediaKey});
+
+export const actionChangeFile = (id, data, mediaKey) => ({type: 'CHANGE', id, data, mediaKey}); 
+
+export const actionSetInputMessageValue = (id, data, name) => ({type: 'SETINPUTMESSAGE', id, data, name});
+
+export const actionAddDraftMessage = (id, data) => ({type: 'ADDMESSAGEDRAFT', id, data});
+
+export const actionClearChats = () => ({type: 'CLEARCHATS'})
+
+export const actionSetMessageEditor = (id, data) => ({type: 'MESEDITOR', id, data})
 
 export const actionLeftChat = (data) => {
 	const [,route, histId] = history.location.pathname.split('/');
@@ -19,7 +30,6 @@ export const actionLeftChat = (data) => {
 	return ({type: 'LEFTCHAT', data})
 }
 
-
 export const actionGetAllChats = (userId) => (
     actionPromise('getAllChats', gql(`query getAll($q: String){
        ChatFind (query: $q){
@@ -62,50 +72,54 @@ export const actionUpsertChat = (id, title, members, avatar) =>
    async (dispatch, getState) => {
       const chat = Object.fromEntries(Object.entries({_id: id, title, members}).filter(([_, v]) => v != null));
       
-      const {_id} = await dispatch(actionUpdateChat(chat));
-      console.log(_id)
-      if(_id && avatar){
-         dispatch(actionSetChatAvatar(avatar));
+      const data = await dispatch(actionUpdateChat(chat));
+      console.log(data)
+      if(data?._id && avatar){
+         dispatch(actionSetChatAvatar(avatar, data));
       }
    }
 
-   export const actionGetOneChat = (_id) => 
-	async dispatch => {
-		let chat = await dispatch(
-			actionPromise('oneChat', gql(`query findChatById($chatId: String) {
-				ChatFindOne(query: $chatId) {
-				  _id
-				  title
-				  members{
-					  _id
-					  nick
-					  avatar{
-						  url
-					  }
-				  }
-				  owner{
-					  _id
-				  }
-				  avatar{
-					_id
-					url
-				  }
-				  lastModified
-				  lastMessage {
-					text
-					createdAt
-					owner{
-						nick
-						_id
-					}
-				  } 
-				}
-			  }`, {chatId: JSON.stringify([{_id}])}))
-		)
-		if(chat){
-			dispatch(actionAddChat(chat))
-		}
-	}
+export const actionGetOneChat = (_id) => 
+async dispatch => {
+   let chat = await dispatch(
+      actionPromise('oneChat', gql(`query findChatById($chatId: String) {
+         ChatFindOne(query: $chatId) {
+            _id
+            title
+            members{
+               _id
+               nick
+               avatar{
+                  url
+               }
+            }
+            owner{
+               _id
+            }
+            avatar{
+            _id
+            url
+            }
+            lastModified
+            lastMessage {
+            text
+            media{
+               _id
+               url
+            }
+            createdAt
+            owner{
+               nick
+               _id
+            }
+            } 
+         }
+         }`, {chatId: JSON.stringify([{_id}])}))
+   )
+   if(chat){
+      dispatch(actionAddChat(chat))
+   }
+}
 
 // export const actionUpdateChat = (id, title, members) => 
 //    async (dispatch, getState) => {
@@ -115,13 +129,5 @@ export const actionUpsertChat = (id, title, members, avatar) =>
 //       console.log(newChat);
 //    }
 
-export const actionRemoveChat = (chatId) => 
-async (dispatch, getState) => {
-   const state = getState();
-   const curUserId = state.auth?.payload?.sub?.id;
-   const chats = state.chats;
-   console.log(chatId in chats);  
-//   console.log(chatId);
-}
 
 

+ 11 - 0
src/actions/actionsForModal.js

@@ -0,0 +1,11 @@
+export const actionIsOpen = (value) => ({type: 'OPEN', value});
+
+export const actionModalContent = (value) => ({type: 'CONTENT', value});
+
+export const actionModalDraft = (value, draftName) => ({type: 'MODALDRAFT', value, draftName});
+
+export const actionOpenModal = (content) => 
+    dispatch => {
+        dispatch(actionModalContent(content));
+        dispatch(actionIsOpen(true));
+    }

+ 22 - 2
src/actions/actionsForUser.jsx

@@ -3,6 +3,28 @@ import { actionAboutMe } from "./actionAboutMe";
 import { actionLeftChat, actionUpdateChat } from "./actionsForChats";
 import { actionPromise } from "./actionsPromise";
 
+export const actionFindUser = (value) => 
+    actionPromise('usersList', gql(`query findUser($q: String) {
+        UserFind(query: $q) {
+          login
+          _id
+        nick
+          avatar {
+            _id
+            url
+          }
+        }
+      }`,{
+          q: JSON.stringify([
+              {
+                $or : [{nick: `/${value}/`}]
+              },
+              {
+                sort : [{login: 1}]
+              }
+          ])
+      } ))
+
 export const actionUpdateUser = (user) => 
     actionPromise('updateUser', gql(`mutation leaveChatByUser($user: UserInput) {
         UserUpsert(user: $user) {
@@ -24,9 +46,7 @@ export const actionUpsertUser = (login, nick, password, chatId) =>
                     return {"_id": _id}
                 }) 
             : null;
-        
         const user = Object.fromEntries(Object.entries({_id: currUserId, login, nick, password, chats}).filter(([_, v]) => v != null));
-        
         const res = await dispatch(actionUpdateUser(user));
             (res && chatId) ? 
                 //actionUpdateChat doesn't work, but it helps to everyone members from the chat to get callback from socket,

+ 17 - 18
src/actions/actionsMedia.jsx

@@ -3,6 +3,7 @@ import { gql } from "../helpers/gql";
 import { uploadFile } from "../helpers/uploadFile";
 import { actionAboutMe } from "./actionAboutMe";
 import { actionUpdateChat } from "./actionsForChats";
+import { actionGetAllMessage } from "./actionsMessages";
 import { actionPromise } from "./actionsPromise";
 
 const actionAddMedia = (typeId, mediaId, type) => 
@@ -20,8 +21,6 @@ actionPromise('file', uploadFile(file))
 export const actionUploadFiles = (files) => 
 actionPromise('files', Promise.all(files.map(file => uploadFile(file))))          
 
-
-
 export const actionSetUserAvatar =  (file) => 
   async (dispatch, getState) => {
     let userId = getState().auth.payload.sub.id;
@@ -35,30 +34,30 @@ export const actionSetUserAvatar =  (file) =>
       }
   }
 
-export const actionSetChatAvatar = (file) => 
+export const actionSetChatAvatar = (file, data) => 
   async (dispatch, getState) => {
     const [,route, chatId] = history.location.pathname.split('/');
-    const chat = getState().chats[chatId];
-    
+    const chat = data ? data : getState().chats[chatId];
+    console.log(chat)
     const media = await dispatch(actionUploadFile(file));
-    console.log(media)
     if(media._id){
-      const dataId = await dispatch(actionAddMedia(chatId, media._id, "chatAvatars"))
+      const dataId = await dispatch(actionAddMedia(chat?._id, media._id, "chatAvatars"))
       if (dataId){
         //it helps to everyone to get changed chat with avatar from socket (something to change for example title)
-        dispatch(actionUpdateChat({_id: chatId, title: chat.title}));
-        
+        dispatch(actionUpdateChat({_id: chat?._id, title: chat?.title}));
       }
       return dataId
     }
   }
 
-
-// const actionAddAvatar = (userId, avatarId) => 
-// actionPromise('avatar', gql(`mutation setAvatar($userId: ID, $avatarId: ID){
-//   UserUpsert(user:{_id: $userId, avatar: {_id: $avatarId}}){
-//       _id, avatar{
-//           _id
-//       }
-//   }
-// }`,{userId: userId, avatarId: avatarId}))
+export const actionAddAllMediaChat = (id, data) => ({type: 'ALLMEDIACHAT', id, data})
+
+export const actionGetAllMediaFromChat = (id) => 
+	async(dispatch, getState) => {
+		const messages = await dispatch(actionGetAllMessage(id));
+		if(messages.length !== 0) {
+    		const media = messages.reduce((a, b) => b.media ? [...a, ...b.media] : a,[] )
+			dispatch(actionAddAllMediaChat(id, media));
+		}
+	}
+	

+ 102 - 14
src/actions/actionsMessages.jsx

@@ -1,15 +1,27 @@
 import { actionPromise } from "./actionsPromise";
 import { gql } from "../helpers/gql";
-import { actionUploadFiles } from "./actionsMedia";
-import { history } from "../App";
-import { actionSetDropMedia, actionSetMessageEditor } from "../reducers/chatReducer";
-import { addUploadDate } from "../components/utils/addUploadDate";
-import { actionOpenModal } from "../reducers/modalReducer";
-import { actionGetOneChat } from "./actionsForChats";
+import { actionAddAllMediaChat, actionUploadFiles } from "./actionsMedia";
+import { addUploadDate } from "../helpers/addUploadDate";
+import { actionGetOneChat, actionSetDropMedia, actionSetMessageEditor } from "./actionsForChats";
+import { actionGetAllMediaFromChat } from "./actionsMedia";
+import { actionOpenModal } from "./actionsForModal";
 
 export const actionAddMessages = (data, id) => ({type: 'MSG', data, id});
 const actionAddMessage = (message, id) => ({type: 'MSG', data: [message], id});
 
+export const actionGetAllMessage = (_id) => 
+	actionPromise('allMessages', gql(`query findAllMes($chatId: String) {
+		MessageFind(query: $chatId) {
+		  media {
+			url
+			_id
+			type
+			originalFileName
+		  }
+		}
+	  }`, {chatId: JSON.stringify([{"chat._id": _id}, {limit: [200]}])}))
+
+
 const actionGetOneMessage = (_id) => 
 	actionPromise('oneMessage', gql(`query getOneMes($mesId: String){
 		MessageFindOne(query: $mesId){
@@ -108,23 +120,19 @@ export const actionUpsertMSG = (message) =>
 
 export const actionSendMessage = (messageId, chatId, text, media, replyTo, forwarded) => 
 	async (dispatch, getState) => {
+		// media.map((item) => item?.name || (item.name = 'record'))
 		let newMedia = null
 		if(media){
-			let checkedMedia = media.filter(item => !item._id && item)
-			console.log(media, checkedMedia)
+			let checkedMedia = media.filter(item => !item?._id && item)
 			let uploadedFiles =  checkedMedia.length !== 0 ? await  dispatch(actionUploadFiles(checkedMedia)) : null;
 			newMedia = uploadedFiles ? [...uploadedFiles, ...media.filter(item => item._id && item)].map(({_id}) => ({_id})) : null;
-			console.log(uploadedFiles)
 		}
 		
 		const mes = Object.fromEntries(Object.entries({
 			_id: messageId, chat: (chatId ? {_id: chatId} : chatId), text: text, media: newMedia, replyTo: (replyTo ? {_id: replyTo} : replyTo), forwarded: (forwarded ? {_id: forwarded} : forwarded) 
 		}).filter(([_, v]) => v != null));
-		console.log(mes)
 			let message = await dispatch(actionUpsertMSG(mes))
-			
 			return message
-		
 	}
 
 export const actionGetMessageFromSocket = (msg) => 
@@ -142,7 +150,6 @@ export const actionGetMessageFromSocket = (msg) =>
 					let mes = await dispatch(actionGetOneMessage(_id))
 					dispatch(actionAddMessage(mes, chat._id));
 				}
-				
 			})
 		}
 		if(forwardWith){
@@ -151,9 +158,12 @@ export const actionGetMessageFromSocket = (msg) =>
 					let mes = await dispatch(actionGetOneMessage(_id))
 					dispatch(actionAddMessage(mes, chat._id));
 				}
-				
 			})
 		}
+		const {media} = msg;
+		if(media){
+			dispatch(actionAddAllMediaChat(msg.chat._id, media))
+		}
 	}
 
 export const actionEditMessage = (chatId, message) => 
@@ -166,3 +176,81 @@ export const actionEditMessage = (chatId, message) =>
 			dispatch(actionOpenModal('messageEditorMediaModal'))
 		}
 	}
+
+	export const actionGetMessageForChat = (_id, fromChatItem) => 
+	async (dispatch,getState) => {
+		let messLen = fromChatItem ? 0 : Object.values(getState().chats[_id]?.messages || {}).length; 
+
+		let messages = await dispatch(
+			actionPromise('messages', gql(`query FindMessChat($chat: String) {
+				MessageFind(query: $chat) {
+				  _id
+				  text
+				  createdAt
+				  chat{
+					  _id
+				  }
+				  media {
+					url
+					_id
+					type
+					text
+					originalFileName
+				  }
+				  replyTo{
+					_id
+					text 
+					media {
+						url
+						_id
+						type
+					  }
+					owner {
+						nick
+					  }
+					}
+					
+					replies{
+						_id text 
+					}
+					forwarded{
+						_id
+						text
+						createdAt
+						owner {
+							nick
+						  }
+						  media {
+							originalFileName 
+							url
+							_id
+							type
+						  }
+					}
+					forwardWith{
+						_id
+						text
+					}
+				  owner {
+					_id
+					nick
+					avatar {
+					  url
+					}
+				  }
+				}
+			  }`, {chat: JSON.stringify(
+				  	[{"chat._id": _id}, 
+						{
+                    		sort: [{_id: -1}], limit: [20], skip: [messLen]
+						}
+					]
+				)}
+			))
+		)
+		if(messages){
+			dispatch(actionAddMessages([...messages], _id));
+			dispatch(actionGetAllMediaFromChat(_id))
+		}
+		return messages
+	}

+ 0 - 1
src/actions/actionsPromise.jsx

@@ -3,7 +3,6 @@ const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFIL
 const actionRejected  = (name,error)   => ({type:'PROMISE',name, status: 'REJECTED', error})
 export const actionPromise = (name, promise) =>
 	async (dispatch) => {
-	
 	dispatch(actionPending(name))
 	try {
 		let payload = await promise;

+ 0 - 36
src/components/AppBar/AppBar.jsx

@@ -1,36 +0,0 @@
-import * as React from 'react';
-import AppBar from '@mui/material/AppBar';
-import Box from '@mui/material/Box';
-import Toolbar from '@mui/material/Toolbar';
-import IconButton from '@mui/material/IconButton';
-import MenuIcon from '@mui/icons-material/Menu';
-import SearchIcon from '@mui/icons-material/Search';
-import { useState } from 'react';
-import { SearchAppBarWrapper, SearchInput } from './AppBar.style';
-
-
-
-
-
-export default function SearchAppBar({openUserMenu}) {
-  const [value, setValue] = useState('');
-  return (
-    <SearchAppBarWrapper>
-          <IconButton
-              onClick={openUserMenu}
-              size="medium"
-              edge="start"
-              aria-label="open drawer"
-              sx={{ m: '0 7px' }}
-            >
-              <MenuIcon style={{color: '#b4b4b4'}}/>
-            </IconButton>
-
-          <SearchInput placeholder='Search' value={value} onChange={e => setValue(e.target.value)}></SearchInput>
-    </SearchAppBarWrapper>
-          
-          
-          
-     
-  );
-}

+ 0 - 28
src/components/AppBar/AppBar.style.js

@@ -1,28 +0,0 @@
-import styled from "styled-components";
-
-export const SearchInput = styled.input`
-    height: 32px;
-    width: 100%;
-    background-color: #f1f1f1;
-    box-sizing: border-box;
-    border-radius: 3px;
-    border: none;
-    padding: 0 10px;
-    &::placeholder{
-        color: #9b9b9b;
-        letter-spacing: 0.5px;
-    }
-    &:focus{
-        outline: none !important;
-        border: 2px solid #00a6dd;
-        background-color: #fff;
-    }
-`;
-
-export const SearchAppBarWrapper = styled.div`
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 54px;
-    margin-right: 12px;
-`;

+ 28 - 10
src/components/Aside/Aside.jsx

@@ -1,19 +1,37 @@
-import { useState } from "react";
-import SearchAppBar from "../AppBar/AppBar";
-import ChatList from "../ChatList/ChatList";
+import { useEffect, useState } from "react";
+import { connect } from "react-redux";
+import {ChatList} from "../ChatList/ChatList";
 import UserMenu from "../Drawer/UserMenu";
+import IconButton from '@mui/material/IconButton';
+import MenuIcon from '@mui/icons-material/Menu';
+import { AsideWrap, SearchAppBarWrapper, SearchInput } from "./Aside.style";
 
-import { AsideWrap } from "./Aside.style";
-
-
-export const Aside = () => {
+export const Aside = ({chats}) => {
 	const [isOpen, setOpen] = useState(false);
+	const [filteredChats, setFilteredChats] = useState([]);
+	const [value, setValue] = useState('');
+	useEffect(() => {
+		setFilteredChats(Object.values(chats).filter((chat) => chat?.title?.toLowerCase().includes(value)));
+	}, [value, chats]);
 	
 	return (
 		<AsideWrap>
-			<SearchAppBar openUserMenu={() => setOpen(true)}/>
-			<ChatList/>
+			<SearchAppBarWrapper>
+				<IconButton
+					onClick={() => setOpen(true)}
+					size="medium"
+					edge="start"
+					aria-label="open drawer"
+					sx={{ m: '0 7px' }}
+					>
+					<MenuIcon style={{color: '#b4b4b4'}}/>
+					</IconButton>
+				<SearchInput placeholder='Search' value={value} onChange={e => setValue(e.target.value)}></SearchInput>
+			</SearchAppBarWrapper>
+			<ChatList chats={filteredChats} clearInput={() => setValue('')}/>
 			<UserMenu open={isOpen} closeUserMenu={() => setOpen(false)}/>
 		</AsideWrap>
 	)
-}
+}
+
+export default connect(state => ({chats: state?.chats || {} }))(Aside);

+ 27 - 0
src/components/Aside/Aside.style.js

@@ -7,4 +7,31 @@ export const AsideWrap = styled.aside`
     height: 100vh;
     // box-shadow: 0 0 10px -1px #BDBDBD;
     border-right: 1px solid #e9e9e9;
+`;
+
+export const SearchInput = styled.input`
+    height: 32px;
+    width: 100%;
+    background-color: #f1f1f1;
+    box-sizing: border-box;
+    border-radius: 3px;
+    border: none;
+    padding: 0 10px;
+    &::placeholder{
+        color: #9b9b9b;
+        letter-spacing: 0.5px;
+    }
+    &:focus{
+        outline: none !important;
+        border: 2px solid #00a6dd;
+        background-color: #fff;
+    }
+`;
+
+export const SearchAppBarWrapper = styled.div`
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 54px;
+    margin-right: 12px;
 `;

+ 6 - 12
src/components/CreateNewChat.jsx

@@ -1,14 +1,9 @@
-
-
-import { Button, Paper, TextField } from '@mui/material';
-
+import { Button, TextField } from '@mui/material';
 import { connect } from 'react-redux';
-import DropZoneAvatar from './DropZone/DropZoneAvatar';
-import { actionUploadFile } from '../actions/actionsMedia';
-import { SearchUserForChatWrap } from './SearchUserForChat/SearchUserForChat.style';
-import { SearchUserForChat } from './SearchUserForChat/SearchUserForChat';
+import DropZoneAvatar from '../DropZone/DropZoneAvatar';
+import { SearchUserForChat } from '../SearchUserForChat/SearchUserForChat';
 import { useEffect, useState } from 'react';
-import {actionUpsertChat } from '../actions/actionsForChats';
+import {actionUpsertChat } from '../../actions/actionsForChats';
 
 export const NewChatModal = ({url, handleClose, handleSetAvatar, inputValue, setChatNameValue, setShowNextModal}) => {
 
@@ -37,7 +32,7 @@ export const NewChatModal = ({url, handleClose, handleSetAvatar, inputValue, set
 	)
 } 
 
-function CreateNewChat({createChat, handleClose}) {
+const ChatCreatorModal = ({createChat, handleClose}) => {
    
     const [avatar, setAvatar] = useState('');
 	const [chatNameValue, setChatNameValue] = useState('');
@@ -64,8 +59,7 @@ function CreateNewChat({createChat, handleClose}) {
 				<Button variant="text" onClick={() => {createChat(null, chatNameValue, users, avatar); handleClose()}}>Create</Button>
 			</div>
 		</>
-		
 }
 
-export default connect(null, {createChat: actionUpsertChat})(CreateNewChat);
+export default connect(null, {createChat: actionUpsertChat})(ChatCreatorModal);
 

+ 87 - 0
src/components/ChatInfoDrawer/ChatInfoDrawer.jsx

@@ -0,0 +1,87 @@
+import { Avatar, Drawer, IconButton } from "@mui/material"
+import { ChatInfoModalAvatar, ChatInfoModalMain, ChatInfoModalName, ChatInfoTitle, Divider, MemberAmount, MemberListHeader } from "../ChatInfoModal/ChatInfoModal.style"
+import { ChatInfoDrawerHeader, LeaveChatButton } from "./ChatInfoDrawer.style"
+import CloseIcon from "../ChatInfoModal/icons8-close.svg";
+import { ChatMembersAmount } from "../ChatPageHeader/ChatPageHeader.style";
+import { CDropZoneAvatarForChat } from "../ChatInfoModal/ChatInfoModal";
+import { backendURL } from "../../helpers/gql";
+import { history } from "../../App";
+import { useEffect } from "react";
+import { connect } from "react-redux";
+import { actionUpsertChat } from "../../actions/actionsForChats";
+import { MemberList } from "../MemberList/MemberList";
+import MembersIcon from "../ChatInfoModal/user.png";
+import AddUserIcon from "../ChatInfoModal/add-user.png";
+import Exit from "../ChatInfoOptions/icons8_exit.png";
+import { actionUpsertUser } from "../../actions/actionsForUser";
+import { ChatInfoDrawerMediaList } from "../ChatInfoDrawerMediaList/ChatInfoDrawerMediaList";
+import { actionOpenModal } from "../../actions/actionsForModal";
+
+const ChatInfoDrawer = ({open, closeChatMenu, chats, userId, openModal, leaveChat, updateChat}) => {
+
+    const [,route, histId] = history.location.pathname.split('/');
+    useEffect(() => {
+        chats[histId] || closeChatMenu()
+    }, [chats[histId]])
+
+    const amount = chats[histId]?.members?.length;
+    const isOwner = userId === chats[histId]?.owner?._id;
+
+    return(
+        <Drawer
+            anchor='right'
+            variant="persistent"
+			open={open}
+			onClose={closeChatMenu}>
+            <div style={{width: '300px'}}>
+                <ChatInfoDrawerHeader>
+                    <ChatInfoModalName>Chat Info</ChatInfoModalName>
+                    <IconButton onClick={closeChatMenu}>
+                        <img style={{cursor: 'pointer'}} src={CloseIcon}/>
+                    </IconButton>
+                </ChatInfoDrawerHeader>
+                <ChatInfoModalMain>
+                    <ChatInfoModalAvatar>
+                        <Avatar
+                            src={`${backendURL}/${chats[histId]?.avatar?.url || ''}`}
+                            alt={chats[histId]?.title || 'chat'}
+                            sx={{ width: 75, height: 75}}
+                        />
+                        {isOwner && <CDropZoneAvatarForChat component={"dropAvatarPopupComponent"}/>}
+                    </ChatInfoModalAvatar>
+                    <div>
+                        <ChatInfoTitle>{chats[histId]?.title}</ChatInfoTitle>
+                        <ChatMembersAmount>{amount} {amount === 1 ? 'member' : 'members'}</ChatMembersAmount>
+                    </div>
+                    
+                </ChatInfoModalMain>
+                
+                <ChatInfoDrawerMediaList chatMedia={chats[histId]?.chatMedia}/>
+
+                <Divider/>
+
+                <MemberListHeader>
+                    <div style={{display: 'flex', justifyContent: 'left'}}>
+                        <img src={MembersIcon} style={{height: "20px", marginRight: "30px"}}/>
+                        <MemberAmount>{amount} {amount === 1 ? 'member' : 'members'}</MemberAmount>
+                    </div>
+
+                    {isOwner && <img src={AddUserIcon} onClick={() => openModal('searchUserModal')} style={{height: "20px", cursor: "pointer"}} />}
+                </MemberListHeader>
+                <MemberList members={chats[histId]?.members}/>
+
+                <Divider style={{marginTop: '10px'}}/>
+
+                <LeaveChatButton onClick={()=> leaveChat(null, null, null, histId)}>
+                    <img src={Exit} alt="exit" style={{margin: ' 0 20px', height: '24px'}}/>
+                    Leave chat
+                </LeaveChatButton>
+            </div>
+        </Drawer>
+    )
+}
+
+export default connect(state => ({
+    chats: state?.chats || [], 
+    userId: state?.auth?.payload?.sub?.id
+}), {updateChat: actionUpsertChat, openModal: actionOpenModal, leaveChat: actionUpsertUser})(ChatInfoDrawer);

+ 22 - 0
src/components/ChatInfoDrawer/ChatInfoDrawer.style.js

@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+export const ChatInfoDrawerHeader = styled.div`
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 10px;
+`;
+
+export const LeaveChatButton = styled.div`
+    
+    display: flex;
+    align-items: center;
+    font-size: 18px;
+    font-weight: 500;
+    height: 40px;
+    cursor: pointer;
+    &:hover{
+        background-color: #f3f3f3;
+    }
+`;

+ 38 - 0
src/components/ChatInfoDrawerMediaList/ChatInfoDrawerMediaList.jsx

@@ -0,0 +1,38 @@
+import { connect } from "react-redux"
+import { ChatInfoDrawerMediaItem, ChatInfoDrawerMediaListWrapper } from "./ChatInfoDrawerMediaList.style"
+import InsertPhotoOutlinedIcon from '@mui/icons-material/InsertPhotoOutlined';
+import VideocamRoundedIcon from '@mui/icons-material/VideocamRounded';
+import HeadsetRoundedIcon from '@mui/icons-material/HeadsetRounded';
+import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
+import { Divider } from "../ChatInfoModal/ChatInfoModal.style";
+
+export const ChatInfoDrawerMediaList = ({chatMedia}) => {
+
+    const media = Object.values(chatMedia || {})
+    const audioFiles = media?.filter((item) => item?.type?.includes('audio') && !item?.originalFileName?.includes('record'));
+	const photos = media?.filter(({type}) => type?.includes('image'));
+	const videos = media?.filter(({type}) => type?.includes('video'));
+	const files = media?.filter(({type}) => type?.includes('application'));
+    return media.length !== 0  && (
+        <ChatInfoDrawerMediaListWrapper>
+            <Divider/>
+            {photos.length > 0 && <ChatInfoDrawerMediaItem>
+                <InsertPhotoOutlinedIcon sx={{mr: '25px'}}/>{photos.length} {photos.length === 1 ? 'photo' : 'photos'}
+            </ChatInfoDrawerMediaItem>}
+
+            {videos.length > 0 && <ChatInfoDrawerMediaItem>
+                <VideocamRoundedIcon sx={{mr: '25px'}}/>{videos.length} {videos.length === 1 ? 'video' : 'videos'}
+            </ChatInfoDrawerMediaItem>}
+
+            {files.length > 0 && <ChatInfoDrawerMediaItem>
+                <InsertDriveFileRoundedIcon sx={{mr: '25px'}}/>{files.length} {files.length === 1 ? 'file' : 'files'}
+            </ChatInfoDrawerMediaItem>}
+
+            {audioFiles.length > 0 && <ChatInfoDrawerMediaItem>
+                <HeadsetRoundedIcon sx={{mr: '25px'}}/>{audioFiles.length} {audioFiles.length === 1 ? 'audio file' : 'audio files'}
+            </ChatInfoDrawerMediaItem>}
+            
+        </ChatInfoDrawerMediaListWrapper>
+    )
+}
+

+ 17 - 0
src/components/ChatInfoDrawerMediaList/ChatInfoDrawerMediaList.style.js

@@ -0,0 +1,17 @@
+import styled from "styled-components";
+
+export const ChatInfoDrawerMediaListWrapper = styled.ul`
+    list-style-type: none;
+    padding: 0;
+`;
+
+export const ChatInfoDrawerMediaItem = styled.li`
+    padding: 5px 20px;
+    display: flex;
+    justify-content: left;
+    align-items: center;
+    cursor: pointer;
+    &:hover{
+        background-color: #f3f3f3;
+    }
+`;

+ 11 - 15
src/components/ChatInfoModal/ChatInfoModal.jsx

@@ -13,8 +13,9 @@ import {actionUpsertChat } from "../../actions/actionsForChats";
 import DropZoneAvatar from "../DropZone/DropZoneAvatar";
 import { actionSetChatAvatar } from "../../actions/actionsMedia";
 import  ChatInfoOptions  from "../ChatInfoOptions/ChatInfoOptions";
+import { ChatMembersAmount } from "../ChatPageHeader/ChatPageHeader.style";
 
-const CDropZoneAvatarForChat = connect(null, {onLoad: actionSetChatAvatar})(DropZoneAvatar);
+export const CDropZoneAvatarForChat = connect(null, {onLoad: actionSetChatAvatar})(DropZoneAvatar);
 
 const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
     const [,route, histId] = history.location.pathname.split('/');
@@ -24,8 +25,6 @@ const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
     const [isOpenSearch, setIsOpenSearch] = useState();
     // old members + new 
     const [users, setUsers] = useState();
-    
-    
     const amount = chats[histId]?.members?.length;
     const isOwner = userId === chats[histId]?.owner?._id;
     //current memberd (old)
@@ -33,7 +32,6 @@ const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
         let {_id} = item; 
         return {_id}
     });
-    
 
     return(!isOpenSearch) ? 
         <ChatInfoModalWrapper>
@@ -42,29 +40,30 @@ const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
                 <div style={{paddingLeft: "150px"}}>
                     <ChatInfoOptions setIsOpenSearch={setIsOpenSearch} isOwner={isOwner} chatId={histId}/>
                 </div>
-                
                 <IconButton onClick={handleClose}>
                     <img style={{cursor: 'pointer'}} src={CloseIcon}/>
                 </IconButton>
-                
             </ChatInfoModalHeader>
             <ChatInfoModalMain>
                 <ChatInfoModalAvatar>
                     <Avatar
                         src={`${backendURL}/${chats[histId]?.avatar?.url || ''}`}
-                        alt="chatavatar"
+                        alt={chats[histId]?.title || "chat"}
                         sx={{ width: 75, height: 75}}
                     />
                     {isOwner && <CDropZoneAvatarForChat component={"dropAvatarPopupComponent"}/>}
                 </ChatInfoModalAvatar>
-                
-                <ChatInfoTitle>{chats[histId]?.title}</ChatInfoTitle>
+                <div>
+                    <ChatInfoTitle>{chats[histId]?.title}</ChatInfoTitle>
+                    <ChatMembersAmount>{amount} {amount === 1 ? 'member' : 'members'}</ChatMembersAmount>
+                </div>
             </ChatInfoModalMain>
             <Divider/>
             <MemberListHeader>
-                <img src={MembersIcon} style={{height: "20px", marginRight: "30px"}}/>
-                <MemberAmount>{amount} {amount === 1 ? 'member' : 'members'}</MemberAmount>
-
+                <div style={{display: 'flex', justifyContent: 'left'}}>
+                    <img src={MembersIcon} style={{height: "20px", marginRight: "30px"}}/>
+                    <MemberAmount>{amount} {amount === 1 ? 'member' : 'members'}</MemberAmount>
+                </div>
                 {isOwner && <img src={AddUserIcon} style={{height: "20px", cursor: "pointer"}} onClick={() => setIsOpenSearch(true)}/>}
             </MemberListHeader>
             <MemberList members={chats[histId]?.members}/>
@@ -79,10 +78,7 @@ const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
             <Button variant="text" onClick={handleClose} >Cancel</Button>
             <Button variant="text" onClick={() => {updateChat(histId, null, users); handleClose()}}>Add</Button>
         </div>    
-
      </>
-     
-     
 }
 
 export default connect(state => ({

+ 1 - 2
src/components/ChatInfoModal/ChatInfoModal.style.js

@@ -46,7 +46,7 @@ export const Divider = styled.div`
 
 export const MemberListHeader = styled.div`
     display: flex;
-    justify-content: left;
+    justify-content: space-between;
     padding: 10px 30px;
 `;
 
@@ -54,7 +54,6 @@ export const MemberAmount = styled.div`
     font-size: 15px;
     text-transform: uppercase;
     font-weight: 500;
-    margin-right: 137px;
     
 `;
 

+ 1 - 3
src/components/ChatInfoOptions/ChatInfoOptions.jsx

@@ -4,18 +4,17 @@ import EditIcon from '@mui/icons-material/Edit';
 import { useState } from "react";
 import AddUserIcon from "../ChatInfoModal/add-user.png";
 import { connect } from "react-redux";
-import { actionOpenModal } from "../../reducers/modalReducer";
 import Exit from "./icons8_exit.png";
 import Basket from "./icons8_basket.svg";
 import { actionUpsertUser } from "../../actions/actionsForUser";
 import { actionUpdateChat } from "../../actions/actionsForChats";
+import { actionOpenModal } from "../../actions/actionsForModal";
 
 const ChatInfoOptions = ({setIsOpenSearch, setOpen, leaveChat, chatId, isOwner, deleteChat}) => {
     const ImgStyle = {
       height: "20px", 
       marginRight: "20px"
     }
-
     const [anchorEl, setAnchorEl] = useState(null);
     const open = Boolean(anchorEl);
     const handleClick = (event) => {
@@ -61,7 +60,6 @@ const ChatInfoOptions = ({setIsOpenSearch, setOpen, leaveChat, chatId, isOwner,
                 </MenuItem>
                 <Divider sx={{margin: "0px"}}/>
             </div>}
-                  
                 <MenuItem onClick={() => {handleClose(); leaveChat(null, null, null, chatId)}} disableRipple>
                     <img src={Exit} alt="exit" style={ImgStyle}/>
                     Leave chat

+ 4 - 10
src/components/ChatList/ChatList.jsx

@@ -1,32 +1,26 @@
 import { List } from "@mui/material"
 import { useEffect, useState } from "react"
-import { connect } from "react-redux"
 import { history } from "../../App"
 import ChatListItem from '../ChatListItem/ChatListItem';
 import { ChatListWrapper } from "./ChatList.style"
-
 import SimpleBar from 'simplebar-react';
 import 'simplebar/dist/simplebar.min.css';
 
-const ChatList = ({chats}) => {
+export const ChatList = ({chats, clearInput}) => {
 	const [, route, histId] = history.location.pathname.split('/');
-	const [stateId, setStateId] = useState(histId);
+	const [stateId, setStateId] = useState();
 	
 	useEffect(() => {
 		setStateId(histId)
 	},[histId])
 
 	return (
-		
 		<ChatListWrapper>
 			<SimpleBar  style={{ maxHeight: '100%'}}>
 				<List sx={{p: '0'}}>
-					{Object.values(chats).map((item, index) => <ChatListItem key={item._id} isActive={stateId === item._id} chat={item} handleSetId={setStateId} />)}
+					{chats.map((item) => <ChatListItem clearInput={clearInput} key={item._id} isActive={stateId === item._id} chat={item} handleSetId={setStateId} />)}
 				</List>
 			</SimpleBar>
 		</ChatListWrapper>
-		
 	)
-}
-
-export default connect(state => ({chats: state.chats || []}))(ChatList);
+}

+ 34 - 22
src/components/ChatListItem/ChatListItem.jsx

@@ -1,22 +1,37 @@
-import { Avatar, ListItem } from "@mui/material";
-import { useEffect } from "react";
-import { useContext, useState } from "react";
+import { Avatar} from "@mui/material";
+import { memo, useEffect } from "react";
 import { connect } from "react-redux";
-import { Link } from "react-router-dom";
+import { actionSetInputMessageValue } from "../../actions/actionsForChats";
 import { backendURL } from "../../helpers/gql";
-import { ContextID } from "../../pages/MainPage";
 import { TimeLastMessage } from "../Time/Time";
-import { ChatListItemLink, ChatListItemContent, ChatListItemTitle, ChatListItemWrapper, ChatListLastMessage, ChatListItemHeader, ChatListItemFooter } from "./ChatListItem.style";
+import { ChatListItemLink, ChatListItemContent, ChatListItemTitle, ChatListItemWrapper, ChatListLastMessage, ChatListItemHeader, ChatListItemFooter, MessageDraftChatListItem } from "./ChatListItem.style";
 import OwnerStar from "./icons8_star.png";
 import OnwerStarActive from "./icons8_star_white.png";
-const ChatListItem = ({chat, handleSetId, isActive, currUserId}) => {
 
-    return (
-            <ChatListItemWrapper isActive={isActive} onClick={() => handleSetId(chat._id)}>
+const ChatListItem = ({chat, handleSetId, isActive, currUserId, setDraftValue, clearInput}) => {
+    const inputValue = chat?.draft?.mainInputValue?.value || '';
+    const draftValue = chat?.draft?.draftValue?.value;
+
+    let timer;
+    function showDraftMessage(){
+        timer = setTimeout(() => {
+            setDraftValue(chat._id, inputValue, 'draftValue')
+        }, [10000])
+    }
+
+    useEffect(() => {
+        showDraftMessage()
+        return (() => {
+            clearTimeout(timer);
             
+        })
+    },[inputValue])
+
+    return (
+            <ChatListItemWrapper isActive={isActive} onClick={() => {handleSetId(chat._id); clearInput()}}>
                 <ChatListItemLink to={`/main/${chat._id}`}>
                     <Avatar
-                        alt={chat.title}
+                        alt={chat?.title || 'chat'}
                         src={`${backendURL}/${chat.avatar?.url || ''}`}
                         sx={{ width: 45, height: 45, mr: '20px'}}
                     />
@@ -27,23 +42,20 @@ const ChatListItem = ({chat, handleSetId, isActive, currUserId}) => {
                             </ChatListItemTitle>
                             <TimeLastMessage isActive={isActive} timestamp={ + chat.lastMessage?.createdAt}/>
                         </ChatListItemHeader>
-                        
                         <ChatListItemFooter>
-                            {chat.lastMessage ?
-                                <ChatListLastMessage isActive={isActive}>
-                                    <span>{(currUserId == chat.lastMessage?.owner._id) ? 'You' : chat.lastMessage?.owner.nick}:</span>
-                                    {chat.lastMessage?.text?.replace(/ /g, "\u00a0") || ''}
-                                </ChatListLastMessage> 
-                            : <div></div>}
-                            {/* <div>2</div> */}
+                            {!draftValue ? <div>
+                                {chat.lastMessage ?
+                                    <ChatListLastMessage isActive={isActive}>
+                                        <span>{(currUserId == chat.lastMessage?.owner._id) ? 'You' : chat.lastMessage?.owner.nick}:</span>
+                                        {chat.lastMessage?.text?.replace(/ /g, "\u00a0") || ''}
+                                    </ChatListLastMessage> 
+                                 : <div></div>} 
+                            </div> : <MessageDraftChatListItem isActive={isActive}><span>Draft:</span>{draftValue?.replace(/ /g, "\u00a0") || ''}</MessageDraftChatListItem>}
                         </ChatListItemFooter>
-                        
-
                     </ChatListItemContent>
                 </ChatListItemLink> 
-            
             </ChatListItemWrapper>
     )
 }
 
-export default connect(state => ({currUserId: state.auth?.payload?.sub?.id}))(ChatListItem);
+export default connect(state => ({currUserId: state.auth?.payload?.sub?.id}), { setDraftValue: actionSetInputMessageValue})(memo(ChatListItem));

+ 14 - 6
src/components/ChatListItem/ChatListItem.style.js

@@ -1,9 +1,6 @@
-import { ListItem } from "@mui/material";
 import styled from "styled-components";
 import { Link } from "react-router-dom";
 
-
-
 export const ChatListItemWrapper = styled.li`
     padding: 0;
     background-color: ${(props) => props.isActive ? "#00a6dd" : "#fff"};
@@ -51,7 +48,7 @@ export const ChatListItemTitle = styled.h3`
 export const ChatListItemFooter = styled.div`
     display: flex;
     justify-content: space-between;
-    height: 13px;
+    // height: 13px;
     line-height: 0.5;
     width: 100%;
 `;
@@ -59,13 +56,24 @@ export const ChatListItemFooter = styled.div`
 export const ChatListLastMessage = styled.div`
     color: ${props => props.isActive ? "#fff" : "#a1a1a1"};
     font-size: 13px;
-    line-height: 1;
+    line-height: 1.2;
     overflow: hidden !important; 
     text-overflow: ellipsis;
     span{
         color: ${props => props.isActive ? "#fff" : "#00a6dd"};
         margin-right: 5px;
-        
     }
 `;
 
+export const MessageDraftChatListItem = styled.div`
+    color: ${props => props.isActive ? "#fff" : "#a1a1a1"};
+    font-size: 13px;
+    line-height: 1.2;
+    overflow: hidden !important; 
+    text-overflow: ellipsis;
+    span{
+        color: ${props => props.isActive ? "#DDDDDD" : "#FF2109"};
+        margin-right: 5px;
+        font-size: 14px;
+    }
+`;

+ 3 - 14
src/components/ChatPageData/ChatPageData.jsx

@@ -1,35 +1,24 @@
 import React, { useEffect } from "react";
 import { useState } from "react";
 import { connect } from "react-redux";
-import { actionGetMessageForChat } from "../../actions/actionGetMessageForChat";
-import { actionSentOrUpdateMSG } from "../../actions/actionsMessages";
+import { actionGetMessageForChat, actionSentOrUpdateMSG } from "../../actions/actionsMessages";
 import ChatPageHeader from "../ChatPageHeader/ChatPageHeader";
 import InputArea from "../InputArea/InputArea";
 import { ChatPageMain, ChatPageWrapper } from "./ChatPageData.style";
 import MessagesArea from "../MessagesArea/MessagesArea";
 
-
-
-///////////////////////////////////////////////////////////////////
-
-
-
 const ChatPageData = ({match: {params: {_id}}, getData}) => {
 	useEffect(() => {
-		getData(_id, true)
+		getData(_id, true);
 	}, [_id])
 
 	return (
 		<ChatPageWrapper>
 			<ChatPageHeader chatId={_id}/>
-			
 			<ChatPageMain>
-				
-					<MessagesArea chatId={_id}/>
-				
+				<MessagesArea chatId={_id}/>
 				<InputArea chatId={_id}/>
 			</ChatPageMain>
-			
 		</ChatPageWrapper>
 	)
 }

+ 0 - 1
src/components/ChatPageData/ChatPageData.style.js

@@ -5,7 +5,6 @@ export const ChatPageWrapper = styled.div`
     height: 100vh;
     display: flex;
     flex-direction: column;
-    
 `; 
 
 export const ChatPageMain = styled.div`

+ 15 - 11
src/components/ChatPageHeader/ChatPageHeader.jsx

@@ -1,16 +1,15 @@
 import { connect } from "react-redux"
-import { actionRemoveChat } from "../../actions/actionsForChats";
-import { actionIsOpen, actionOpenModal } from "../../reducers/modalReducer";
-
+import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined';
 import { ChatListItemTitle } from "../ChatListItem/ChatListItem.style"
 import { ChatMembersAmount, ChatPageHeaderWrap } from "./ChatPageHeader.style"
+import  ChatInfoDrawer  from "../ChatInfoDrawer/ChatInfoDrawer";
+import { memo, useState } from "react";
+import IconButton from '@mui/material/IconButton';
+import { actionOpenModal } from "../../actions/actionsForModal";
 
-
-
-
-const ChatPageHeader = ({ chats, chatId, onclick, openModal}) => {
-
-    let amount = chats[chatId]?.members?.length;
+const ChatPageHeader = ({ chats, chatId, openModal}) => {
+    const [isOpen, setOpen] = useState(false);
+    const amount = chats[chatId]?.members?.length;
 
     return (
         <ChatPageHeaderWrap>
@@ -18,9 +17,14 @@ const ChatPageHeader = ({ chats, chatId, onclick, openModal}) => {
                 <ChatListItemTitle>{chats[chatId]?.title}</ChatListItemTitle>
                 <ChatMembersAmount>{amount} {amount === 1 ? 'member' : 'members'}</ChatMembersAmount>
             </div>
-            <div onClick={() => onclick(chatId)}>leave chat</div>
+            <div>
+                <IconButton sx={{ ...(isOpen && { display: 'none' }) }} onClick={() => setOpen(true)}>
+                    <TableChartOutlinedIcon />
+                </IconButton>
+                <ChatInfoDrawer open={isOpen} closeChatMenu={() => setOpen(false)}/>
+            </div>
         </ChatPageHeaderWrap>
     )
 }
 
-export default connect(state => ({chats : state?.chats || []}), {onclick: actionRemoveChat, openModal: actionOpenModal})(ChatPageHeader);
+export default connect(state => ({chats : state?.chats || []}), {openModal: actionOpenModal})(memo(ChatPageHeader));

+ 0 - 2
src/components/ChatPageHeader/ChatPageHeader.style.js

@@ -7,12 +7,10 @@ export const ChatPageHeaderWrap = styled.div`
     width: 100%;
     height: 54px;
     background-color: #fff;
-    // border-left: 1px solid #e9e9e9;
     border-bottom: 1px solid #e9e9e9;
     display: flex;
     justify-content: space-between;
     padding: 8px 12px;
-    // box-shadow: 0 0 10px -1px #BDBDBD;
 `;
 
 export const ChatMembersAmount = styled.div`

+ 9 - 24
src/components/Drawer/UserMenu.jsx

@@ -1,29 +1,15 @@
-import { Accordion, AccordionDetails, AccordionSummary, Avatar, Button, Divider, Drawer, IconButton, List, ListItem, ListItemAvatar, Paper, Typography } from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { Avatar, Divider, Drawer, List } from '@mui/material';
 import { connect } from 'react-redux';
-import { Box } from '@mui/system';
-import { backendURL, gql } from '../../helpers/gql';
-import { actionPromise } from '../../actions/actionsPromise';
+import { backendURL } from '../../helpers/gql';
 import React from 'react';
-import {useDropzone} from 'react-dropzone';
-import { useEffect} from 'react';
-import { uploadFile } from '../../helpers/uploadFile';
-import DropZoneAvatar from '../DropZone/DropZoneAvatar';
-
-import { actionAuthLogout } from '../../actions/actionLogin';
-import { store } from '../../App';
-import NewChatButton from '../NewChatButton';
-import { actionSetUserAvatar } from '../../actions/actionsMedia';
+import { actionLogout } from '../../actions/actionLogin';
 import { ListItemWrap, StyledSpan, UserMenuHeader, UserMenuTitle } from './UserMenu.style';
 import ChatIcon from "./icons8-chat.png";
-import { actionOpenModal } from '../../reducers/modalReducer';
 import Exit from "../ChatInfoOptions/icons8_exit.png";
 import EditIcon from '@mui/icons-material/Edit';
+import { actionOpenModal } from '../../actions/actionsForModal';
 
-// const actionUploadFiles = (files) => 
-// actionPromise('filesUpload', Promise.all(files.map((file) => uploadFile(file))))
-
-const UserMenu = ({open, setOpen, closeUserMenu, userInfo: {nick, avatar} }) => {
+const UserMenu = ({open, setOpen, closeUserMenu, userInfo: {nick, avatar}, logout }) => {
 	
 	return (
 		<Drawer 
@@ -43,15 +29,14 @@ const UserMenu = ({open, setOpen, closeUserMenu, userInfo: {nick, avatar} }) =>
 					</UserMenuTitle>
 				</UserMenuHeader>
 				<Divider/>
-				
 				<List>
-					<ListItemWrap onClick={() => setOpen('createChatModal')}>
+					<ListItemWrap onClick={() => {setOpen('chatCreatorModal'); closeUserMenu()}}>
 						<img src={ChatIcon} alt="chatIcon" style={{height: "22px"}}/> <StyledSpan>New chat</StyledSpan>
 					</ListItemWrap>
-					<ListItemWrap onClick={() => setOpen('userEditorModal')}>
+					<ListItemWrap onClick={() => {setOpen('userEditorModal'); closeUserMenu()}}>
 						<EditIcon/> <StyledSpan>Edit profile</StyledSpan>
 					</ListItemWrap>
-					<ListItemWrap onClick={() => store.dispatch(actionAuthLogout())}>
+					<ListItemWrap onClick={() => logout()}>
 						<img src={Exit} alt="chatIcon" style={{height: "22px"}}/> <StyledSpan>Log out</StyledSpan>
 					</ListItemWrap>
 				</List>
@@ -60,4 +45,4 @@ const UserMenu = ({open, setOpen, closeUserMenu, userInfo: {nick, avatar} }) =>
 	)
 }
 
-export default connect(state => ({userInfo: state.promise?.aboutMe?.payload || {}}), {setOpen : actionOpenModal})(UserMenu)
+export default connect(state => ({userInfo: state.promise?.aboutMe?.payload || {}}), {setOpen : actionOpenModal, logout: actionLogout})(UserMenu)

+ 1 - 0
src/components/Drawer/UserMenu.style.js

@@ -20,6 +20,7 @@ export const ListItemWrap = styled(ListItem)`
     padding: 0;
     height: 40px;
     padding-left: 25px;
+    cursor: pointer;
     &:hover{
         background-color: #f3f3f3;
     }

+ 2 - 5
src/components/DropMediaFilesOptions/DropMediaFilesOptions.jsx

@@ -1,8 +1,8 @@
 import { useEffect } from "react";
 import { useDropzone } from "react-dropzone";
 import { connect } from "react-redux";
-import { actionChangeFile, actionDeleteOneMediaFile } from "../../reducers/chatReducer";
-import { addUploadDate } from "../utils/addUploadDate";
+import { actionChangeFile, actionDeleteOneMediaFile } from "../../actions/actionsForChats";
+import { addUploadDate } from "../../helpers/addUploadDate";
 import { DropMediaFilesOptionsWrap } from "./DropMediaFilesOptions.style"
 import ArrowsIcon from './icons8-arrows.png';
 import Basket from './icons8_basket.svg';
@@ -21,14 +21,11 @@ const DropMediaFilesOptions = ({deleteFile, mediaKey, changeFile, chatId}) => {
 
     return (
         <DropMediaFilesOptionsWrap>
-            
                 <div style={{height: '19px'}}
                 {...getRootProps({className: 'dropzone'})}>
                 <input {...getInputProps()} />
                     <img src={ArrowsIcon}/>
                 </div>
-            
-            
             <img src={Basket} onClick={() => deleteFile(chatId, mediaKey)}/>
         </DropMediaFilesOptionsWrap>
     )

+ 0 - 6
src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.jsx

@@ -5,8 +5,6 @@ import { DropAudioPlayer, DropAudioTitle, DropAudioWrap, DropImgWrap, DropVideoW
 import FileIcon from './icons8-File.svg';
 import MusicIcon from './icons8-music.png';
 
-
-
 export const DropImg = ({url}) => {
     const image = useRef(null);
 
@@ -24,10 +22,8 @@ export const DropVideo = ({url}) => {
 export const DropAudio = ({url, name, size}) => {
 
     const [isOpen, setIsOpen] = useState(false);
-
     const handleOpen = () => setIsOpen(!isOpen);
     
-
     return (
         <DropAudioWrap>
             <DropFileWrap>
@@ -51,12 +47,10 @@ export const DropFile = ({size, name}) => {
             <DropFileIconWrap>
                 <DropFileIcon src={FileIcon}/>
             </DropFileIconWrap>
-            
             <DropFileInfo>
                 <DropFileTitle>{name}</DropFileTitle>
                 <DropFileSize>{size}</DropFileSize>
             </DropFileInfo>
-            
         </DropFileWrap>
     )
 }

+ 0 - 2
src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.style.js

@@ -13,7 +13,6 @@ export const DropVideoWrap = styled.video`
 
 //////////////////////////////////////audio
 export const DropAudioWrap = styled.div`
-    
 `;
 
 export const DropAudioPlayer = styled.audio`
@@ -23,7 +22,6 @@ export const DropAudioPlayer = styled.audio`
 `;
 
 export const DropAudioTitle = styled.div`
-
 `;
 
 export const DropFileHeader = styled.div`

+ 1 - 6
src/components/DropMediaItem/DropMediaItem.jsx

@@ -1,13 +1,11 @@
 import React, { memo, useEffect, useState } from "react";
-import { connect } from "react-redux"
 import { backendURL } from "../../helpers/gql";
 import DropMediaFilesOptions from "../DropMediaFilesOptions/DropMediaFilesOptions";
-import { calculateFileSize } from "../utils/calculateFileSize";
+import { calculateFileSize } from "../../helpers/calculateFileSize";
 import { DropImg, DropVideo, DropAudio, DropFile } from "./DropMediaFiles/DropMediaFiles";
 import { DropMediaItemWrap } from "./DropMediaItem.style"
 
 export const DropMediaItem = ({mediaItem, chatId}) => {
-
     
     const [url, setUrl] = useState('')
     const [name, setName] = useState('');
@@ -20,7 +18,6 @@ export const DropMediaItem = ({mediaItem, chatId}) => {
                 (mediaItem?.name || mediaItem?.originalFileName))
         setSize(calculateFileSize(mediaItem?.size))
     },[])
-    
 
     const checkType = (type) => {
         if(type.includes("image")){
@@ -34,12 +31,10 @@ export const DropMediaItem = ({mediaItem, chatId}) => {
         }
     }
     
-    
     return(
         <DropMediaItemWrap>
             {checkType(mediaItem?.type)}
             <DropMediaFilesOptions chatId={chatId} mediaKey={mediaItem.uploadDate}/>
-            
         </DropMediaItemWrap>
     )
 }

+ 0 - 3
src/components/DropZone/DropZoneAvatar.jsx

@@ -10,8 +10,6 @@ import { AvatarWrap, AvatarOverlay } from './DropZoneAvatar.style';
 import { BadgeComponent } from '../UserEditorModal/UserEditorModal.style';
 import Photo from "../UserEditorModal/icons8photo.png";
 
-// import { actionSetUserAvatar } from './UserMenu';
-
 const DropAvatarComponent = ({url}) => {
   return(
     <AvatarWrap>
@@ -57,7 +55,6 @@ export default function DropZoneAvatar({onLoad, component, url }) {
     });
     
     useEffect(()=>{
-        console.log(acceptedFiles)
         acceptedFiles[0] && onLoad(acceptedFiles[0])
     }, [acceptedFiles])
 

+ 3 - 5
src/components/DropZone/MessageDropZone/MessageDropZone.jsx

@@ -2,13 +2,11 @@ import {useDropzone} from 'react-dropzone';
 import {useEffect} from 'react';
 import DropIcon from './icons8-dropIcon.png';
 import { connect } from 'react-redux';
-import { actionOpenModal } from '../../../reducers/modalReducer';
-// import { actionSetDropMedia } from '../../../reducers/messageCreatorReducer';
-import { addUploadDate } from '../../utils/addUploadDate';
-import { actionSetDropMedia } from '../../../reducers/chatReducer';
+import { addUploadDate } from '../../../helpers/addUploadDate';
+import { actionOpenModal } from '../../../actions/actionsForModal';
+import { actionSetDropMedia } from '../../../actions/actionsForChats';
 
 function MessageDropZone({onLoad, openModal, url, chatId }) {
-    
 
     const {acceptedFiles, getRootProps, getInputProps} = useDropzone({
       noDrag: true, 

+ 36 - 37
src/components/InputArea/InputArea.jsx

@@ -1,15 +1,13 @@
-import { useEffect, useRef, useState } from "react"
 import { connect } from "react-redux"
-import { actionSendMessage, actionUpsertMSG } from "../../actions/actionsMessages"
+import { actionSendMessage} from "../../actions/actionsMessages"
 import { InputAreaWrapper, TextArea} from "./InputArea.style";
-import Plane from "./icons8-plane.png";
 import MessageDropZone from "../DropZone/MessageDropZone/MessageDropZone";
-import { actionAddDraftMessage, actionDeleteDraftMessageId, actionDeleteReplyMessageId, actionSetInputMessageValue } from "../../reducers/chatReducer";
-
 import SendRoundedIcon from '@mui/icons-material/SendRounded';
 import InputAreaMessageEditor from "./InputAreaMessageEditor";
 import MessageReplyForwarded from "../MessageDraft/MessageReplyForwarded";
-
+import  RecordViewAudio, { MemoRecordViewAudio } from "../Recorder/RecorderAudio";
+import { MemoRecordViewVideo } from "../Recorder/RecorderVideo";
+import { actionAddDraftMessage, actionSetInputMessageValue } from "../../actions/actionsForChats";
 
 const InputArea = ({sendMessage, deleteDraftMessage, chatId, chats, setInputValue, modal: {content, isOpen}}) => {
     const message = chats[chatId]?.draft?.mainInputValue?.message;
@@ -17,38 +15,39 @@ const InputArea = ({sendMessage, deleteDraftMessage, chatId, chats, setInputValu
     const  replyMessageId =  message && message.hasOwnProperty('reply') ? message.reply?._id : null;
     const forwardedMessageId = message && message.hasOwnProperty('forwarded') ? message.forwarded?._id : null;
 
-	return (chats[chatId]?.draft?.messageEditor && !chats[chatId]?.draft?.messageEditor?.message?.media) ? 
-        <InputAreaMessageEditor chatId={chatId}/> 
-        :
-		<InputAreaWrapper>
-            <MessageReplyForwarded chat={chats[chatId]}>
-                
-            </MessageReplyForwarded>
-            
-            <div style={{display: 'flex', alignItems: 'center', padding: '10px 0'}}>
-                <MessageDropZone chatId={chatId}/>
-                <TextArea
-                    value={content === 'messageMediaModal' && isOpen ? '' : inputValue}         
-                    onChange={e=>{setInputValue(chatId, e.target.value, 'mainInputValue')}}
-                    maxRows={8}
-                    disabled={forwardedMessageId ? true : false}
-                    placeholder="Write a message..." 
-                />
-                {inputValue || forwardedMessageId ? 
-                <SendRoundedIcon
-                    style={{margin: '0 16px', cursor: "pointer"}}
-                    color="primary"
-                    onClick={
-                        async() => {
-                            let val = await sendMessage(null, chatId, forwardedMessageId ? 'forwarded message' : inputValue?.replace(/^\s+|\s+$/g, ''), null, replyMessageId, forwardedMessageId);
-                            (val?.replyTo?._id || val?.forwarded?._id) && deleteDraftMessage(chatId, null)
-                            val && !val?.forwarded?._id && setInputValue(chatId, "", 'mainInputValue')
+	return (
+        <div style={{display: 'flex', borderTop: '1px solid #e9e9e9', alignItems: 'center'}}>
+            {(chats[chatId]?.draft?.messageEditor && !chats[chatId]?.draft?.messageEditor?.message?.media) ? 
+            <InputAreaMessageEditor chatId={chatId}/> 
+            :
+            <InputAreaWrapper>
+                <MessageReplyForwarded chat={chats[chatId]}></MessageReplyForwarded>
+                <div style={{display: 'flex', alignItems: 'center', padding: '10px 0'}}>
+                    <MessageDropZone chatId={chatId}/>
+                    <TextArea
+                        value={content === 'messageMediaModal' && isOpen ? '' : inputValue}         
+                        onChange={e=>{setInputValue(chatId, e.target.value, 'mainInputValue')}}
+                        maxRows={8}
+                        disabled={forwardedMessageId ? true : false}
+                        placeholder="Write a message..." 
+                    />
+                    {inputValue || forwardedMessageId ? 
+                    <SendRoundedIcon
+                        style={{margin: '0 16px', cursor: "pointer"}}
+                        color="primary"
+                        onClick={
+                            async() => {
+                                let val = await sendMessage(null, chatId, forwardedMessageId ? 'forwarded message' : inputValue?.replace(/^\s+|\s+$/g, ''), null, replyMessageId, forwardedMessageId);
+                                (val?.replyTo?._id || val?.forwarded?._id) && deleteDraftMessage(chatId, null)
+                                val && !val?.forwarded?._id && setInputValue(chatId, "", 'mainInputValue') && setInputValue(chatId, "", 'draftValue')
+                            }
                         }
-                    }
-                /> : <div></div>}
-            </div>
-        </InputAreaWrapper>
-	
+                    /> : <div></div>}
+                </div>
+            </InputAreaWrapper>}
+            <MemoRecordViewAudio sendMessage={sendMessage} chatId={chatId}/>
+        </div> 
+    )
 }
 
 export default connect(state => ({chats: state.chats, modal: state?.modal}), {

+ 1 - 3
src/components/InputArea/InputArea.style.js

@@ -4,14 +4,12 @@ import TextareaAutosize from 'react-textarea-autosize';
 export const InputAreaWrapper = styled.div`
     width: 100%;
     background-color: #fff;
-    border-top: 1px solid #e9e9e9;
-    
+    // border-top: 1px solid #e9e9e9;
     // display: flex;
     // align-items: center;
     img{
         margin: 0 20px;
         cursor: pointer;
-
     }
 `;
 

+ 2 - 6
src/components/InputArea/InputAreaMessageEditor.jsx

@@ -1,12 +1,10 @@
-import { useEffect, useRef, useState } from "react"
 import { connect } from "react-redux"
-import { actionSendMessage, actionUpsertMSG } from "../../actions/actionsMessages"
+import { actionSendMessage } from "../../actions/actionsMessages";
 import { InputAreaWrapper, TextArea} from "./InputArea.style";
-import Plane from "./icons8-plane.png";
 import MessageDropZone from "../DropZone/MessageDropZone/MessageDropZone";
-import { actionDeleteDraftMessageId, actionDeleteReplyMessageId, actionSetInputMessageValue, actionSetMessageEditor } from "../../reducers/chatReducer";
 import SendRoundedIcon from '@mui/icons-material/SendRounded';
 import {MessageEditor} from "../MessageDraft/MessageEditor";
+import { actionSetInputMessageValue, actionSetMessageEditor } from "../../actions/actionsForChats";
 
 const InputAreaMessageEditor = ({chats, chatId, deleteMessageEditor, sendMessage, setInputValue}) => {
     
@@ -17,7 +15,6 @@ const InputAreaMessageEditor = ({chats, chatId, deleteMessageEditor, sendMessage
     return (
         <InputAreaWrapper>
             <MessageEditor deleteMessageEditor={deleteMessageEditor} chatId={chatId} message={message}/>
-            
             <div style={{display: 'flex', alignItems: 'center', padding: '10px 0'}}>
                 <MessageDropZone chatId={chatId}/>
                 <TextArea
@@ -35,7 +32,6 @@ const InputAreaMessageEditor = ({chats, chatId, deleteMessageEditor, sendMessage
                         async() => {
                             let val = await sendMessage(messageId, null, inputValue.replace(/^\s+|\s+$/g, ''), null, null);
                             val && deleteMessageEditor(chatId, null);
-                            
                         }
                     }
                 /> : <div></div>}

+ 12 - 8
src/components/MemberList/MemberList.jsx

@@ -1,18 +1,22 @@
 import { Avatar } from "@mui/material"
 import { backendURL } from "../../helpers/gql"
 import { MemberItem, MemberListWrapper, MemberName } from "./MemberList.style"
+import SimpleBar from 'simplebar-react';
+import 'simplebar/dist/simplebar.min.css';
 
 export const MemberList = ({members}) => {
     return (
         <MemberListWrapper>
-            {members?.map((member) => <MemberItem key={member?._id}>
-                <Avatar
-                    src={`${backendURL}/${member?.avatar?.url || ''}`}  
-                    sx={{width: 45, height: 45, mr: '20px'}}
-                    alt="member"
-                />
-                <MemberName>{member?.nick}</MemberName>
-            </MemberItem>)}
+            <SimpleBar style={{ maxHeight: '100%'}}>
+                {members?.map((member) => <MemberItem key={member?._id}>
+                    <Avatar
+                        src={`${backendURL}/${member?.avatar?.url || ''}`}  
+                        sx={{width: 45, height: 45, mr: '20px'}}
+                        alt="member"
+                    />
+                    <MemberName>{member?.nick}</MemberName>
+                </MemberItem>)}
+            </SimpleBar>
         </MemberListWrapper>
     )
 }

+ 2 - 0
src/components/MemberList/MemberList.style.js

@@ -1,9 +1,11 @@
 import styled from "styled-components";
 
 export const MemberListWrapper = styled.ul`
+    max-height: 300px;
     margin: 0;
     padding: 0;
     list-style-type: none;
+    overflow: hidden;
 `;
 
 export const MemberItem = styled.li`

+ 15 - 26
src/components/Message/Message.jsx

@@ -1,22 +1,14 @@
-import { useEffect, useRef, useState } from "react";
-import { Link } from "react-router-dom";
-import { backendURL } from "../../helpers/gql";
+import { memo, useState } from "react";
 import MessageOptions from "../MessageOptions/MessageOptions";
 import { ReplyMessage } from "../ReplyMessage/ReplyMessage";
 import { convert } from "../Time/Time";
 import { ForwardedMessageHeader, MessageContainer, MessageFooter, MessageOwner, MessageText, MessageWrapper, TimeMessage } from "./Message.style";
-import { saveAs } from "file-saver";
-import { MemoMessageMediaContainer, MessageMediaContainer } from "../MessageMediaContainer/MessageMediaContainer";
+import { MemoMessageMediaContainer } from "../MessageMediaContainer/MessageMediaContainer";
 
-
-
-
-export const Message = ({mes, chatId}) => {
-
-	const owner = true;
+const Message = ({mes, chatId, currUser, lastElem}) => {
+	const isOwner = currUser === mes?.owner?._id ? true : false;
 	const checkedMessage = mes.forwarded ? mes.forwarded : mes; //check message (it has forwarded message or not, if it has return forwarded mes)
-  
-    const [anchorEl, setAnchorEl] = useState(null);
+	const [anchorEl, setAnchorEl] = useState(null);
     const open = Boolean(anchorEl);
     const handleClick = (event) => {
       setAnchorEl(event.currentTarget);
@@ -24,25 +16,22 @@ export const Message = ({mes, chatId}) => {
     const handleClose = () => {
       setAnchorEl(null)
     };
-
 	
 	return (
-        <div>
 			<MessageWrapper >
-					<MessageContainer onClick={(e) => {handleClick(e)}}>
-						{mes.forwarded && <ForwardedMessageHeader>{`Forwarded from ${checkedMessage?.owner?.nick || ''}`}</ForwardedMessageHeader>}
-						{checkedMessage?.replyTo && <ReplyMessage owner={owner} message={checkedMessage.replyTo}/>}
+					<MessageContainer isOwner={isOwner} onContextMenu={(e) => {e.preventDefault(); handleClick(e)}} lastElem={lastElem}>
+						{mes.forwarded && <ForwardedMessageHeader owner={isOwner}>{`Forwarded from ${checkedMessage?.owner?.nick || ''}`}</ForwardedMessageHeader>}
+						{checkedMessage?.replyTo && <ReplyMessage owner={isOwner} message={checkedMessage.replyTo}/>}
 						{checkedMessage?.media && checkedMessage?.media?.length !==0 && <MemoMessageMediaContainer media={checkedMessage.media}/>}
 						<MessageText>{checkedMessage?.text?.replace(/ /g, "\u00a0") || ''}</MessageText>
 						<MessageFooter>
-							<MessageOwner>{checkedMessage.owner.nick || 'nick'}</MessageOwner>
-							<TimeMessage owner={owner}>{convert(checkedMessage.createdAt).getTime()}</TimeMessage>
+							<MessageOwner>{isOwner ? 'You' : mes.owner.nick || 'nick'}</MessageOwner>
+							<TimeMessage owner={isOwner}>{convert(mes.createdAt).getTime()}</TimeMessage>
 						</MessageFooter>
 					</MessageContainer>
+					<MessageOptions message={checkedMessage} chatId={chatId} handleClose={handleClose} open={open} anchorEl={anchorEl}/>
 			</MessageWrapper>
-            <MessageOptions message={checkedMessage} chatId={chatId} handleClose={handleClose} open={open} anchorEl={anchorEl}/>
-         </div>)
-			
-			
-	
-}
+        )
+}
+
+export default memo(Message)

+ 19 - 4
src/components/Message/Message.style.js

@@ -2,13 +2,13 @@ import styled from "styled-components";
 
 export const MessageWrapper = styled.div`
     display: flex;
-    margin:30px;
+    margin: 3px;
 `;
 
 export const MessageContainer = styled.div`
 color: #000;
 max-width: 400px;
-background-color: #E2DCC2;
+background-color: ${props => props.isOwner ? '#E2DCC2' : '#CCCCCC'};
 padding: 12px 16px 8px;
 position: relative;
 display: block;
@@ -18,11 +18,22 @@ border-radius: 5px 5px 5px 0px;
     width: 15px;
     height: 15px;
     position: absolute;
-    background-color: #E2DCC2;
+    background-color: ${props => props.isOwner ? '#E2DCC2' : '#CCCCCC'};
     bottom: 0;
     left: -14px;
     clip-path: polygon(75% 63%, 83% 53%, 90% 41%, 94% 31%, 98% 15%, 100% 0, 100% 100%, 22% 100%, 34% 94%, 43% 88%, 55% 80%, 65% 72%);
 };
+
+${props => props.lastElem ? `
+border-radius: 5px 5px 5px 5px;
+&::before{
+    display: none;
+}
+` : ` border-radius: 5px 5px 5px 5px
+        &::before{
+    } `
+} 
+
 `;
 
 export const MessageText = styled.div`
@@ -51,5 +62,9 @@ export const MessageOwner = styled.div`
 `;
 
 export const ForwardedMessageHeader = styled.div`
-    
+    color: ${props => props.owner ? '#E67E22' : '#3498DB'};
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 1;
 `;
+

+ 16 - 0
src/components/MessageDate/MessagesDate.jsx

@@ -0,0 +1,16 @@
+import { memo } from "react"
+import { convert } from "../Time/Time"
+import { MessagesDateWrapper, MsgDate } from "./MessagesDate.style"
+
+const MessagesDate = ({timestamp, visible}) => {
+
+    return(
+        visible && <MessagesDateWrapper>
+            <MsgDate>
+                <div>{convert(timestamp).getDateMonthName()}</div>
+            </MsgDate>
+        </MessagesDateWrapper>
+    )
+}
+
+export default memo(MessagesDate)

+ 19 - 0
src/components/MessageDate/MessagesDate.style.js

@@ -0,0 +1,19 @@
+import styled from "styled-components";
+
+export const MessagesDateWrapper = styled.div`
+    margin-top: 10px;
+    margin-left: 86px;
+`;
+
+export const MsgDate = styled.div`
+    width: 400px;
+    display: flex;
+    justify-content: center;
+    div{
+        padding: 2px 12px;
+        background-color: rgba(0, 0, 0, .2);
+        font-size: 13px;
+        color: #fff;
+        border-radius: 12px;
+    }
+`;

+ 2 - 1
src/components/MessageDraft/MessageDraft.style.js

@@ -40,4 +40,5 @@ export const MessageDraftText = styled.div`
 
 export const MessageDraftBoxWrapper = styled.div`
    
-`;
+`;
+

+ 1 - 5
src/components/MessageDraft/MessageEditor.jsx

@@ -1,10 +1,8 @@
 import { MessageDraftContainer, MessageDraftInfo, MessageDraftText, MessageDraftWrapper, MessageOwnerDraftName } from "./MessageDraft.style";
 import EditRoundedIcon from '@mui/icons-material/EditRounded';
 import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
-import { connect } from "react-redux";
 
  export const MessageEditor = ({chatId, message, deleteMessageEditor}) => {
-    
 
     return (
         <MessageDraftWrapper>
@@ -22,6 +20,4 @@ import { connect } from "react-redux";
             <CloseRoundedIcon style={{cursor: 'pointer'}} color="primary" onClick={() => {deleteMessageEditor(chatId, null)}}/>
         </MessageDraftWrapper>
     )
-}
-
-// export connect(null, {})(MessageEditor);
+}

+ 2 - 4
src/components/MessageDraft/MessageReplyForwarded.jsx

@@ -2,14 +2,12 @@ import ReplyIcon from '@mui/icons-material/Reply';
 import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
 import ForwardRoundedIcon from '@mui/icons-material/ForwardRounded';
 import { connect } from "react-redux";
-import { actionAddDraftMessage, actionDeleteDraftMessageId} from "../../reducers/chatReducer";
 import { MessageDraftContainer, MessageDraftInfo, MessageDraftText, MessageDraftWrapper, MessageOwnerDraftName } from './MessageDraft.style';
 import { checkTypeFileForReply } from '../ReplyMessageMediaIcon/ReplyMessageMediaIcon';
-import { useEffect, useRef, useState } from 'react';
+import { actionAddDraftMessage } from '../../actions/actionsForChats';
 
 const MessageReplyForwarded = ({chat, deleteDraftMessage}) => {
     const message = chat?.draft?.mainInputValue?.message || null;
-    // const message = typeof mes !== 'object' ?  chat?.messages?.[mes] : mes;
     const typeMessage = message && Object.keys(message)
     return (message) ? 
         <MessageDraftWrapper>
@@ -21,7 +19,7 @@ const MessageReplyForwarded = ({chat, deleteDraftMessage}) => {
                         {message?.[typeMessage]?.owner?.nick || ''}
                     </MessageOwnerDraftName>
                     <MessageDraftText>
-                        {message?.[typeMessage]?.text.replace(/ /g, "\u00a0") || ''}
+                        {message?.[typeMessage]?.text?.replace(/ /g, "\u00a0") || ''}
                     </MessageDraftText>
                 </MessageDraftInfo>
             </MessageDraftContainer>

+ 2 - 3
src/components/MessageEditorMediaModal/MessageEditorMediaModal.jsx

@@ -5,16 +5,15 @@ import 'simplebar/dist/simplebar.min.css';
 import { MemoizedDropMediaItem } from "../DropMediaItem/DropMediaItem";
 import { Button, TextField } from "@mui/material";
 import { actionSendMessage } from "../../actions/actionsMessages";
-import { actionDeleteDropMedia, actionSetInputMessageValue, actionSetMessageEditor } from "../../reducers/chatReducer";
 import { useEffect } from "react";
 import { connect } from "react-redux";
+import { actionDeleteDropMedia, actionSetInputMessageValue, actionSetMessageEditor } from "../../actions/actionsForChats";
 
 const MessageEditorMediaModal = ({chats, handleClose, deleteMessageEditor, open, chatId, sendMessage, setInputValue, deleteMedia}) => {
     const media = chats[chatId].draft?.media || [];
     const inputValue = chats[chatId].draft?.messageEditor?.value || '';
     const messageId = chats[chatId].draft?.messageEditor?.message?._id;
 
-
     useEffect(() => {
         open || deleteMedia(chatId);
         open || deleteMessageEditor(chatId, null);
@@ -62,5 +61,5 @@ export default connect(state => ({
     deleteMedia: actionDeleteDropMedia, 
     deleteMessageEditor: actionSetMessageEditor,
     sendMessage: actionSendMessage,
-    setInputValue: actionSetInputMessageValue,
+    setInputValue: actionSetInputMessageValue
 })(MessageEditorMediaModal);

+ 2 - 3
src/components/MessageMediaItem/MessageMediaItem.jsx

@@ -9,15 +9,14 @@ export const MessageMediaItem = ({file}) => {
     const name = file?.originalFileName?.length > 22 ? 
     file.originalFileName.slice(0,10)  + '...' + file.originalFileName.slice(-10) : 
     file.originalFileName
-    
 
     const checkType = (type) => {
         if (type.includes("image")){
-            return <MessageImgItem url={url}/>
+            return <MessageImgItem url={url} name={name}/>
         }else if (type.includes("audio")){
             return <AudioItem url={url} name={name}/>
         }else if(type.includes("video")){
-            return <MessageVideoItem url={url}/>
+            return <MessageVideoItem url={url} name={name}/>
         }else {
             return <FileItem url={url} name={name}/>
         }

+ 1 - 1
src/components/MessageMediaItems/MessageAudioItem/AudioItem.jsx

@@ -3,7 +3,7 @@ import MusicIcon from '../../DropMediaItem/DropMediaFiles/icons8-music.png';
 import { useState } from "react";
 import { backendURL } from "../../../helpers/gql";
 import DownloadIcon from './icons8-download.png';
-import { saveFile } from "../../utils/saveFile";
+import { saveFile } from "../../../helpers/saveFile";
 
 export const AudioItem = ({url, name}) => {
 

+ 0 - 1
src/components/MessageMediaItems/MessageAudioItem/AudioItem.style.js

@@ -16,7 +16,6 @@ export const FileIconWrap = styled.div`
     justify-content: center;
     align-items: center;
     margin-right: 10px;
-    
     cursor: pointer;
 `;
 

+ 1 - 2
src/components/MessageMediaItems/MessageFileItem/MessageFileItem.jsx

@@ -1,5 +1,5 @@
 import { backendURL } from "../../../helpers/gql"
-import { saveFile } from "../../utils/saveFile"
+import { saveFile } from "../../../helpers/saveFile"
 import { DownloaderFile, FileIcon, FileIconWrap, FileTitle, FileWrap } from "../MessageAudioItem/AudioItem.style";
 import File from '../../DropMediaItem/DropMediaFiles/icons8-File.svg';
 import DownloadIcon from '../MessageAudioItem/icons8-download.png';
@@ -17,7 +17,6 @@ export const FileItem = ({url, name}) => {
                     <div style={{display: 'flex', justifyContent: 'right'}}>
                         <DownloaderFile onClick={() => saveFile(`${backendURL}/${url}`, name)} src={DownloadIcon}></DownloaderFile>
                     </div>
-                    
                 </div>
             </FileWrap>
         </div>

+ 4 - 2
src/components/MessageMediaItems/MessageImgItem/MessageImgItem.jsx

@@ -1,12 +1,14 @@
 import { backendURL } from "../../../helpers/gql"
 import { DownloadImg, ImageItem, MessageImgItemWrap } from "./MessageImgItem.style"
 import DownloadIcon from '../MessageAudioItem/icons8-download.png';
+import { saveFile } from "../../../helpers/saveFile";
+import { DownloaderFile } from "../MessageAudioItem/AudioItem.style";
 
-export const MessageImgItem = ({url}) => {
+export const MessageImgItem = ({url, name}) => {
     return(
         <MessageImgItemWrap>
             <ImageItem src={`${backendURL}/${url || ''}`}/>
-            <DownloadImg src={DownloadIcon}/>
+            <DownloadImg onClick={() => saveFile(`${backendURL}/${url}`, name)} src={DownloadIcon}></DownloadImg>
         </MessageImgItemWrap>
     )
 }

+ 2 - 1
src/components/MessageMediaItems/MessageImgItem/MessageImgItem.style.js

@@ -12,5 +12,6 @@ export const ImageItem = styled.img`
 export const DownloadImg = styled.img`
     position: absolute;
     top: 0;
-    right: 0
+    right: 0;
+    cursor: pointer;
 `;

+ 3 - 2
src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.jsx

@@ -1,12 +1,13 @@
 import { backendURL } from "../../../helpers/gql"
 import { DownloadVideo, MessageVideoItemWrap, VideoItem } from "./MessageVideoItem.style";
 import DownloadIcon from '../MessageAudioItem/icons8-download.png';
+import { saveFile } from "../../../helpers/saveFile";
 
-export const MessageVideoItem = ({url}) => {
+export const MessageVideoItem = ({url, name}) => {
     return(
         <MessageVideoItemWrap>
             <VideoItem controls src={`${backendURL}/${url || ''}`} />
-            <DownloadVideo src={DownloadIcon}/>
+            <DownloadVideo onClick={() => saveFile(`${backendURL}/${url}`, name)} src={DownloadIcon}></DownloadVideo>
         </MessageVideoItemWrap>
     )
 }

+ 3 - 6
src/components/MessageMediaModal/MessageMediaModal.jsx

@@ -2,14 +2,13 @@ import { Button, TextField } from "@mui/material"
 import { useEffect, useRef, useState } from "react";
 import { useDropzone } from "react-dropzone";
 import { connect } from "react-redux";
-import { actionAddDraftMessage, actionDeleteDraftMessageId, actionDeleteDropMedia, actionSetDropMedia, actionSetInputMessageValue, actionSetMessageEditor } from "../../reducers/chatReducer";
 import {DropMediaItem, MemoizedDropMediaItem}  from "../DropMediaItem/DropMediaItem";
-import { addUploadDate } from "../utils/addUploadDate";
+import { addUploadDate } from "../../helpers/addUploadDate";
 import SimpleBar from 'simplebar-react';
 import 'simplebar/dist/simplebar.min.css';
 import { DropMediaList, MediaBox, MediaModalHeader, MessageMediaFooter, MessageMediaModalWrapper } from "./MessageMediaModal.style"
-import { actionUploadFiles } from "../../actions/actionsMedia";
 import { actionSendMessage } from "../../actions/actionsMessages";
+import { actionAddDraftMessage, actionDeleteDropMedia, actionSetDropMedia, actionSetInputMessageValue } from "../../actions/actionsForChats";
  
 
 const MessageMediaModal = ({chatId, handleClose, deleteMedia, deleteDraftMessage, chats, open, onload, sendMessage, setInputValue}) => {
@@ -58,7 +57,6 @@ const MessageMediaModal = ({chatId, handleClose, deleteMedia, deleteDraftMessage
                 onChange = {(e) => setInputValue(chatId, e.target.value, 'mainInputValue')}
                 value={inputValue}
             />
-            
             <MessageMediaFooter>
                 <div 
                     {...getRootProps({className: 'dropzone'})}>
@@ -92,6 +90,5 @@ export default connect(state => ({
         onload: actionSetDropMedia, 
         sendMessage: actionSendMessage,
         setInputValue: actionSetInputMessageValue,
-        deleteDraftMessage: actionAddDraftMessage,
-        
+        deleteDraftMessage: actionAddDraftMessage        
     })(MessageMediaModal);

+ 4 - 9
src/components/MessageOptions/MessageOptions.jsx

@@ -1,18 +1,14 @@
 import { Menu, MenuItem } from '@mui/material';
-import List from '@mui/material/List';
-import ListItem from '@mui/material/ListItem';
-import ListItemButton from '@mui/material/ListItemButton';
-import ListItemIcon from '@mui/material/ListItemIcon';
-import ListItemText from '@mui/material/ListItemText';
 import ReplyIcon from "./icons8-reply.png";
 import ForwardIcon from "./icons8-forward.png";
 import { ImgIcon } from './MessageOptions.style';
 import EditIcon from '@mui/icons-material/Edit';
 import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
-import { actionAddDraftMessage, actionSetMessageEditor } from '../../reducers/chatReducer';
 import { connect } from 'react-redux';
-import { actionModalDraft, actionOpenModal } from '../../reducers/modalReducer';
 import { actionEditMessage } from '../../actions/actionsMessages';
+import { memo } from 'react';
+import { actionModalDraft, actionOpenModal } from '../../actions/actionsForModal';
+import { actionAddDraftMessage } from '../../actions/actionsForChats';
 
 const MessageOptions = ({userId, addDraftMessageEditor, handleClose, addMessageId, open, anchorEl, chatId, message, addDraftMessage, openModal}) => {
     const isOwner = userId === message.owner?._id;
@@ -41,7 +37,6 @@ const MessageOptions = ({userId, addDraftMessageEditor, handleClose, addMessageI
                     <EditIcon style={{height: '18px', width: '18px', marginRight: '18px'}}/>
                     Edit
                 </MenuItem>}
-                
 
                 <MenuItem onClick={() => {addMessageId(message, 'forwardedMessage'); openModal('searchChatModal'); handleClose()}} disableRipple> 
                     <ImgIcon src={ForwardIcon} />
@@ -56,4 +51,4 @@ const MessageOptions = ({userId, addDraftMessageEditor, handleClose, addMessageI
     )
 }
 
-export default connect(state => ({userId: state?.auth?.payload?.sub?.id}), {addDraftMessage: actionAddDraftMessage, addDraftMessageEditor: actionEditMessage, openModal: actionOpenModal, addMessageId: actionModalDraft})(MessageOptions)
+export default connect(state => ({userId: state?.auth?.payload?.sub?.id}), {addDraftMessage: actionAddDraftMessage, addDraftMessageEditor: actionEditMessage, openModal: actionOpenModal, addMessageId: actionModalDraft})(memo(MessageOptions))

+ 0 - 2
src/components/MessageOptions/MessageOptions.style.js

@@ -6,8 +6,6 @@ export const MessageOptionsWrapper = styled.ul`
 
 export const MessageOptionsItem = styled.li`
 
-
-
 `;
 
 export const ImgIcon = styled.img`

+ 63 - 44
src/components/MessagesArea/MessagesArea.jsx

@@ -1,62 +1,62 @@
 import { connect } from "react-redux"
-import { Message } from "../Message/Message"
-import { DropContainer, MessageAreaDrop, MessagesAreaWrapper, SimpleBarWrapper } from "./MessagesArea.style"
+import  Message  from "../Message/Message"
+import { DropContainer, MessageGroup, MessagesAreaWrapper, ScrollBottom, SimpleBarWrapper } from "./MessagesArea.style"
 import 'simplebar/dist/simplebar.min.css';
-import InfiniteScroll from 'react-infinite-scroll-component';
-import { useEffect, useMemo, useRef, useState } from "react";
-import { actionGetMessageForChat } from "../../actions/actionGetMessageForChat";
+import { memo, useEffect, useRef, useState } from "react";
 import { useDropzone } from "react-dropzone";
-import { actionOpenModal } from "../../reducers/modalReducer";
-import { actionSetDropMedia } from "../../reducers/chatReducer";
-import { addUploadDate } from "../utils/addUploadDate";
-// import { ClassNames } from "@emotion/react";
+import { addUploadDate } from "../../helpers/addUploadDate";
+import { convert } from "../Time/Time";
+import  MessagesDate  from "../MessageDate/MessagesDate";
+import { Avatar } from "@mui/material";
+import { backendURL } from "../../helpers/gql";
+import ScrollBtn from './icons8-scrollBottom.png';
+import { actionGetMessageForChat } from "../../actions/actionsMessages";
+import { actionOpenModal } from "../../actions/actionsForModal";
+import { actionSetDropMedia } from "../../actions/actionsForChats";
 
-// const Test = ({messagesByChat}) => {
-// 	useEffect(() => {
-// 		console.log('i am from body')
-// 		return () => {
-// 			console.log('i was unMount')
-// 		}
-// 	}, [])
-
-// 	return (
-// 		<div style={{display: 'flex', flexDirection: 'column-reverse'}}>
-			
-// 		</div>
-// 	)
-// }
-
-
-
-const MessagesArea = ({chats, chatId, getMessages, openModal, onLoad}) => {
-	
-	// console.log(chats[chatId].messages)
+const MessagesArea = ({currUser, chats, chatId, getMessages, openModal, onLoad}) => {
+	const [isVisible, setIsVisible] = useState(false);
 	const messagesByChat = Object.values(chats[chatId]?.messages || []);
+	const mess = messagesByChat.reduce((p,c)=>{
+	  if( p[p.length-1][p[p.length-1].length - 1 ]?.createdAt - c.createdAt > 600000 
+			||   (p[p.length-1][p[p.length-1].length - 1 ]?.createdAt ? convert(p[p.length-1][p[p.length-1].length - 1 ]?.createdAt).getDateMonthName() !== convert(c.createdAt).getDateMonthName() : false )
+			|| (p[p.length-1][p[p.length-1].length - 1 ]?.owner ? p[p.length-1][p[p.length-1].length - 1 ]?.owner?._id !== c?.owner?._id : false)  ){
+		p.push([]);
+	  }
+	  
+	  p[p.length-1].push(c);
+	  return p;
+	}, [[]]);
+	const OFFSET = 600;
+	
 	const ref = useRef();
+	
 	let timer
 	function infinityScroll () {
-		// console.log('количество скроллов')
 		clearTimeout(timer);
 		timer = setTimeout(async() => {
-			// console.log('количество таймеров')
-			const offset = 600;
 			const scrollElem = ref.current?.contentWrapperEl;
-			if(scrollElem.scrollHeight - (scrollElem.clientHeight - scrollElem.scrollTop)  < offset){
+			if(scrollElem.scrollHeight - (scrollElem.clientHeight - scrollElem.scrollTop)  < OFFSET){
 				let data = await getMessages(chatId);
-				if (data.length < 20) ref.current?.contentWrapperEl.removeEventListener("scroll", infinityScroll);
-				// console.log('вызов')
+				console.log(data)
+				if (data?.length < 20) ref.current?.contentWrapperEl.removeEventListener("scroll", infinityScroll);
 			}
 		},30)
 	}
-	
+	function scrollBottom (){
+		const scrollElem = ref.current?.contentWrapperEl;
+		if(Math.abs(scrollElem.scrollTop) > OFFSET){
+			setIsVisible(true)
+		} else {setIsVisible(false)}
+	}
+
 	useEffect(() => {
 		ref.current?.contentWrapperEl.addEventListener("scroll", infinityScroll);
-		// Remove listener (like componentWillUnmount)
-		// console.log('i am working from useEffect');
+		ref.current?.contentWrapperEl.addEventListener("scroll", scrollBottom);
 		return () => {
 			ref.current.contentWrapperEl.scrollTop = 0;
-			ref.current?.contentWrapperEl.removeEventListener("scroll", infinityScroll);
-			console.log("unMount from parent")
+			ref?.current?.contentWrapperEl?.removeEventListener("scroll", infinityScroll);
+			ref?.current?.contentWrapperEl?.removeEventListener("scroll", scrollBottom);
 		};
 	  }, [chatId]);
 
@@ -77,19 +77,38 @@ const MessagesArea = ({chats, chatId, getMessages, openModal, onLoad}) => {
         let files = addUploadDate(acceptedFiles)
         acceptedFiles[0] && onLoad(chatId, files)
     }, [acceptedFiles])
-	
+
 	return (
 			<DropContainer {...getRootProps({ isDragAccept, isFocused})}>
 				<div className="drop">drop files here</div>
         		<input {...getInputProps()} />
 				<MessagesAreaWrapper >
+					<ScrollBottom isVisible={isVisible}><img style={{width: '50px', height: '50px'}} onClick={() => ref.current.contentWrapperEl.scrollTop = 0} src={ScrollBtn}/></ScrollBottom>
 					<SimpleBarWrapper  ref={ref}> 
-					{messagesByChat.map((item) => <Message key={item._id} chatId={chatId} mes={item}/>)}
+					<div style={{height: '20px', fontSize: '6px', opacity: '0'}}>1</div>
+					{mess[0].length > 0 && mess.map((item, i, arr) => 
+						<div key={i}>
+							<MessagesDate visible={
+								convert(item[item.length-1]?.createdAt).getDateMonthName() !== convert(arr[i + 1]?.[0]?.createdAt).getDateMonthName() ? true : false
+							} timestamp={item[item.length-1]?.createdAt}/> 
+							<MessageGroup>
+								<div style={{display: 'flex', flexDirection: 'column-reverse', position: "relative"}}>
+									<Avatar
+										alt={item[0]?.nick || item[0]?.login ||  'avatar'}
+										src={`${backendURL}/${item[0]?.owner?.avatar?.url || ''}`}
+										sx={{ width: 45, height: 45, mr: '20px', position: 'sticky', bottom: 0, marginBottom: '5px'}}
+									/>
+								</div>
+								<div style={{display: 'flex', flexDirection: 'column-reverse'}}>
+									{item.map((mes, i, arr) => <Message key={mes._id} lastElem={i == 0 ? false : true} currUser={currUser} chatId={chatId} mes={mes}/> )}
+								</div>
+							</MessageGroup>
+						</div>
+						)}
 					</SimpleBarWrapper>
 				</MessagesAreaWrapper>
-				
       		</DropContainer>
 	)
 }
 
-export default connect(state=>({chats: state.chats || {}}), {getMessages : actionGetMessageForChat, openModal: actionOpenModal, onLoad: actionSetDropMedia})(MessagesArea)
+export default connect(state=>({chats: state.chats || {}, currUser: state.auth?.payload?.sub?.id}), {getMessages : actionGetMessageForChat, openModal: actionOpenModal, onLoad: actionSetDropMedia})(memo(MessagesArea))

+ 21 - 14
src/components/MessagesArea/MessagesArea.style.js

@@ -9,29 +9,16 @@ export const MessagesAreaWrapper = styled.div`
     position: relative;
 `;
 
+
 export const SimpleBarWrapper = styled(SimpleBar)`
     height: 100%;
     overflow-x: hidden;
     div.simplebar-content-wrapper, div.simplebar-content, div.simplebar-vertical{
         display: flex;
         flex-direction: column-reverse;
-        
     }
 `;
 
-const getColor = (props) => {
-    if (props.isDragAccept) {
-        return '#00e676';
-    }
-    if (props.isDragReject) {
-        return '#ff1744';
-    }
-    if (props.isFocused) {
-        return '#2196f3';
-    }
-    return '#eeeeee';
-  }
-
 export const DropContainer = styled.div`
     height: 100%;
     overflow: hidden; 
@@ -53,4 +40,24 @@ export const DropContainer = styled.div`
         left: 50%;
         transform: translate(-50%, -50%);
     }
+`;
+
+export const MessageGroup = styled.div`
+    display: flex;
+    margin-top: 10px;
+    margin-left: 20px;
+`;
+
+export const ScrollBottom = styled.div`
+    position: absolute;
+    transition: all 0.2s ease-out;
+    right: 50px;
+    bottom: ${props => props.isVisible ? '50px' : '-50px'};
+    border-radius: 50%;
+    background-color: #00a6dd;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 100;
+    cursor: pointer;
 `;

BIN
src/components/MessagesArea/icons8-scrollBottom.png


+ 13 - 15
src/components/Modal.jsx

@@ -1,20 +1,18 @@
-import { useState } from "react"
 import Box from '@mui/material/Box';
 import Modal from '@mui/material/Modal';
 import Fade from '@mui/material/Fade';   
-import { Fab} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
 import Backdrop from '@mui/material/Backdrop';
-import CreateNewChat, { FindUserModal, MainChatModal } from "./CreateNewChat";
 import { connect } from "react-redux";
-import { actionIsOpen } from "../reducers/modalReducer";
-import ChatInfoModal from "./ChatInfoModal/ChatInfoModal";
-import ChatEditorModal  from "./ChatEditorModal/ChatEditorModal";
-import UserEditorModal  from "./UserEditorModal/UserEditorModal";
-import MessageMediaModal from "./MessageMediaModal/MessageMediaModal";
-import { history } from "../App";
-import  SearchChatModal  from "./SearchChatModal/SearchChatModal";
-import MessageEditorMediaModal from "./MessageEditorMediaModal/MessageEditorMediaModal";
+import ChatInfoModal from "../ChatInfoModal/ChatInfoModal";
+import ChatEditorModal  from "../ChatEditorModal/ChatEditorModal";
+import UserEditorModal  from "../UserEditorModal/UserEditorModal";
+import MessageMediaModal from "../MessageMediaModal/MessageMediaModal";
+import { history } from "../../App";
+import  SearchChatModal  from "../SearchChatModal/SearchChatModal";
+import MessageEditorMediaModal from "../MessageEditorMediaModal/MessageEditorMediaModal";
+import SearchUserModal from "../SearchUserModal/SearchUserModal";
+import ChatCreatorModal from '../ChatCreator/ChatCreator';
+import { actionIsOpen } from '../../actions/actionsForModal';
 
 const ModalComponent = ({modal, setOpen}) => {
     const [,route, histId] = history.location.pathname.split('/');
@@ -22,13 +20,14 @@ const ModalComponent = ({modal, setOpen}) => {
     const handleClose = () => setOpen(false);
     
     const modalMap = {
-        createChatModal: <CreateNewChat handleClose={handleClose}/>,
+        chatCreatorModal: <ChatCreatorModal handleClose={handleClose}/>,
         chatInfoModal: <ChatInfoModal handleClose={handleClose}/>, 
         chatEditorModal: <ChatEditorModal handleClose={handleClose}/>,
         userEditorModal: <UserEditorModal handleClose={handleClose}/>, 
         messageMediaModal: <MessageMediaModal chatId={histId} open={open} handleClose={handleClose}/>,
         searchChatModal: <SearchChatModal handleClose={handleClose}/>,
-        messageEditorMediaModal: <MessageEditorMediaModal chatId={histId} open={open} handleClose={handleClose}/>
+        messageEditorMediaModal: <MessageEditorMediaModal chatId={histId} open={open} handleClose={handleClose}/>,
+        searchUserModal: <SearchUserModal chatId={histId} handleClose={handleClose}/>
     }
 
 	return (
@@ -49,7 +48,6 @@ const ModalComponent = ({modal, setOpen}) => {
                         {modalMap[modal.content]}
                     </Box>
                 </Fade>
-
         </Modal>
 	)
 }

+ 0 - 19
src/components/NewChatButton.jsx

@@ -1,19 +0,0 @@
-import { actionOpenModal } from "../reducers/modalReducer";
-import Box from '@mui/material/Box';
-import { Fab} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import { connect } from "react-redux";
-
-const NewChatButton = ({setOpen}) => {
-	return (
-	  <Box sx={{ '& > :not(style)': { m: 1 } }}>
-		
-		<Fab  color="secondary" aria-label="add">
-		  <AddIcon onClick={() => setOpen('createChatModal')}/>
-		</Fab>
-		
-	  </Box>
-	)
-  }
-
-export default connect(null,{setOpen : actionOpenModal})(NewChatButton)

+ 0 - 0
src/components/Recorder/Recorder.style.js


+ 22 - 0
src/components/Recorder/RecorderAudio.jsx

@@ -0,0 +1,22 @@
+import { useReactMediaRecorder } from "react-media-recorder";
+import SettingsVoiceIcon from '@mui/icons-material/SettingsVoice';
+import { memo, useEffect, useState } from "react";
+
+const RecordViewAudio = ({chatId, sendMessage}) => {
+  const { status, startRecording, stopRecording, mediaBlobUrl, clearBlobUrl,  } =
+    useReactMediaRecorder({ audio: true, 
+      onStart: () => console.log('start'), 
+      onStop: (_,media) => { sendMessage(null, chatId, null, [media] , null, null); clearBlobUrl()} 
+    });
+  return (
+    <div style={{paddingRight: '20px', cursor: 'pointer'}}>
+      	<SettingsVoiceIcon
+       				color={status === "recording" ? "primary" : "disabled"}
+					onMouseDown={startRecording} 
+					onMouseUp={stopRecording}/>
+    </div>
+  );
+};
+
+
+export const MemoRecordViewAudio = memo(RecordViewAudio)

+ 24 - 0
src/components/Recorder/RecorderVideo.jsx

@@ -0,0 +1,24 @@
+import VideocamRoundedIcon from '@mui/icons-material/VideocamRounded';
+import { useReactMediaRecorder } from "react-media-recorder";
+import { memo, useEffect, useState } from "react";
+
+const RecordViewVideo = ({chatId, sendMessage}) => {
+    const { status, startRecording, stopRecording, mediaBlobUrl, clearBlobUrl,  } =
+      useReactMediaRecorder({ video: true, 
+        blobPropertyBag: {type: 'video/mp4'},
+        onStart: () => console.log('start'), 
+        onStop: (_,media) => { sendMessage(null, chatId, null, [media] , null, null); clearBlobUrl()} 
+        
+      });
+    return (
+      <div style={{paddingRight: '20px'}}>
+            <VideocamRoundedIcon 
+                        color={status === "recording" ? "primary" : "disabled"}
+                        onMouseDown={startRecording} 
+                        onMouseUp={stopRecording}/> 
+      </div>
+    );
+  };
+  
+  
+  export const MemoRecordViewVideo = memo(RecordViewVideo)

+ 3 - 3
src/components/ReplyMessage/ReplyMessage.jsx

@@ -5,10 +5,10 @@ export const ReplyMessage = ({message, owner}) => {
     return (
         <ReplyMessageWrapper>
             <ReplyMessageDivider owner={owner}/>
-            {message.media && message.media.length !== 0 ? <ReplyMessageMediaIcon owner={owner} mediaFile={message.media[0]}/> : <></>}
+            {message.media && message?.media?.length !== 0 ? <ReplyMessageMediaIcon owner={owner} mediaFile={message.media[0]}/> : <></>}
             <ReplyMessageContent>
-                <ReplyMessageOwner owner={owner}>{message.owner.nick}</ReplyMessageOwner>
-                <ReplyMessageText>{message.text.replace(/ /g, "\u00a0")}</ReplyMessageText>
+                <ReplyMessageOwner owner={owner}>{message?.owner?.nick}</ReplyMessageOwner>
+                <ReplyMessageText>{message?.text?.replace(/ /g, "\u00a0")}</ReplyMessageText>
             </ReplyMessageContent>
         </ReplyMessageWrapper>
     )

+ 0 - 3
src/components/ReplyMessage/ReplyMessage.style.js

@@ -11,7 +11,6 @@ export const ReplyMessageDivider = styled.div`
     height: 100%;
     width: 3px;
     background-color: ${props => props.owner ? '#E67E22' : '#3498DB'} ;
-    // background-color: #00a6dd;
     margin-right: 10px;
     border-radius: 2px;
 `;
@@ -21,9 +20,7 @@ export const ReplyMessageContent = styled.div`
     // flex-direction: column;
     // justify-content: space-around;
     width: 100%;
-
     overflow: hidden;
-
 `;
 
 export const ReplyMessageOwner = styled.div`

+ 18 - 0
src/components/Routes/Routes.jsx

@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import { Route, Switch, Redirect} from 'react-router-dom';
+import LoginPage from '../../pages/LoginPage';
+import MainPage from '../../pages/MainPage';
+import RegisterPage from '../../pages/RegisterPage';
+
+const Routes = ({auth}) => {
+	return (
+				<Switch>
+					<Route path="/login" component={LoginPage}/>
+					<Route path="/register" component={RegisterPage}/>
+					<Route path="/main" component={MainPage} />
+					<Route exact path="/">{auth ? <Redirect to="/main"/> : <Redirect to="/login" /> }</Route>
+				</Switch>
+			)
+}
+
+export default connect(state => ({auth : state.auth?.payload}))(Routes);

+ 0 - 1
src/components/SearchChatListItem/SearchChatListItem.style.js

@@ -7,7 +7,6 @@ export const SearchChatItemWrapper = styled.li`
     &:hover{
         background-color: #f3f3f3;
     }
-    
 `;
 
 export const SearchChatItemName = styled.div`

+ 2 - 8
src/components/SearchChatModal/SearchChatModal.jsx

@@ -10,17 +10,14 @@ import { connect } from "react-redux";
 import { SearchChatListItem } from "../SearchChatListItem/SearchChatListItem";
 import SimpleBar from 'simplebar-react';
 import 'simplebar/dist/simplebar.min.css';
-import { actionAddDraftMessage } from "../../reducers/chatReducer";
-import { Link } from "react-router-dom";
+import { actionAddDraftMessage } from "../../actions/actionsForChats";
 
 const SearchChatModal = ({handleClose, chats, forwardedMessage, addForwardedMessageToChat}) => {
 
     const [value, setValue] = useState('');
-
     const [chatsArr, setChatsArr] = useState([]);
-
     useEffect(() => {
-        setChatsArr(Object.values(chats).filter((chat) => chat.title.toLowerCase().includes(value)));
+        setChatsArr(Object.values(chats).filter((chat) => chat?.title?.toLowerCase().includes(value)));
     }, [value]);
 
     return (
@@ -30,16 +27,13 @@ const SearchChatModal = ({handleClose, chats, forwardedMessage, addForwardedMess
                 <img src={SearchIcon}/>
                 <SearchChatInput placeholder='Search' value={value} onChange={(e) => setValue(e.target.value)}/>
             </SearchChatInputWrapper>
-
             <Divider sx={{mb: '8px'}}/>
-
             <SearchChatListWrapper>
                 <SimpleBar style={{ maxHeight: '100%'}}>
                     {chatsArr.map((chat) => <SearchChatListItem key={chat?._id} handleClose={handleClose} chat={chat} addForwardedMessageToChat={addForwardedMessageToChat} forwardedMessage={forwardedMessage}/>)}
                 </SimpleBar>
             </SearchChatListWrapper>
             <Divider/>
-            
             <div style={{display: "flex",
     				padding: "10px 15px",
     				justifyContent: "right",

+ 0 - 2
src/components/SearchChatModal/SearchChatModal.style.js

@@ -1,6 +1,5 @@
 import styled from 'styled-components'
 
-
 export const SearchChatWrap = styled.div`
     width: 100%;
     // height: 560px;
@@ -11,7 +10,6 @@ export const SearchChatHeader = styled.div`
     padding: 20px 20px 10px;
     font-weight: 500;
     font-size: 18px;
-    
 `;
 
 export const SearchChatFooter = styled.div`

+ 1 - 3
src/components/SearchUserInput/SearchUserInput.jsx

@@ -1,6 +1,6 @@
 import { useEffect, useState } from "react"
 import { connect } from "react-redux";
-import { actionFindUser } from "../../actions/actionFindUser";
+import { actionFindUser } from "../../actions/actionsForUser";
 import { SUInput, SearchUserInputWrapper } from "./SearchUserInput.style"
 import SearchIcon from "./search_icon.svg";
 
@@ -8,9 +8,7 @@ const SearchUserInput = ({findUsers}) => {
     const [value, setValue] = useState('');
 
     useEffect(() => {
-        
             findUsers(value);
-        
     }, [value])
 
     return (

+ 0 - 2
src/components/SearchUserItem/SearchUserItem.jsx

@@ -2,8 +2,6 @@ import { Avatar, Badge } from "@mui/material"
 import { backendURL } from "../../helpers/gql"
 import { SearchUserItemName, SearchUserItemWrapper, BadgeImgWrapper, AvatarWrapper } from "./SearchUserItem.style"
 import CheckBox from "./icons8_checkbox.svg";
-import styled from "styled-components";
-
 
 export const SearchUserItem = ({user, handleSetUser, invisible}) => {
     return (

+ 0 - 2
src/components/SearchUserItem/SearchUserItem.style.js

@@ -2,7 +2,6 @@ import styled from "styled-components";
 
 export const SearchUserItemWrapper = styled.li`
     width: 100%;
-    
     height: 60px;
     &:hover{
         background-color: #f3f3f3;
@@ -33,5 +32,4 @@ export const AvatarWrapper = styled.div`
         border: 2px solid #00a6dd;
         padding: 2px;
     `}
-    
 `;

+ 28 - 0
src/components/SearchUserModal/SearchUserModal.jsx

@@ -0,0 +1,28 @@
+import { Button } from "@mui/material";
+import { useState } from "react";
+import { connect } from "react-redux";
+import { actionUpsertChat } from "../../actions/actionsForChats";
+import { SearchUserForChat } from "../SearchUserForChat/SearchUserForChat";
+
+const SearchUserModal = ({chatId, handleClose, updateChat, chats}) => {
+    const [users, setUsers] = useState();
+    const currMembers = chats[chatId]?.members?.map(item => {
+        let {_id} = item; 
+        return {_id}
+    });
+
+    return(
+        <>
+            <SearchUserForChat getUsers={setUsers} members={currMembers}/>
+            <div style={{display: "flex",
+                        padding: "10px 15px",
+                        justifyContent: "right",
+                        alignItems: "center"}}>
+                <Button variant="text" onClick={handleClose} >Cancel</Button>
+                <Button variant="text" onClick={() => {updateChat(chatId, null, users); handleClose()}}>Add</Button>
+            </div> 
+        </>
+    )
+}
+
+export default connect(state => ({chats: state?.chats || []}), {updateChat: actionUpsertChat})(SearchUserModal)

+ 12 - 1
src/components/Time/Time.jsx

@@ -3,6 +3,9 @@ import { useEffect } from "react";
 import { TimeWrapper } from "./Time.style"
 
 export function convert(timestamp){
+    const monthNames = ["January", "February", "March", "April", "May", "June",
+  "July", "August", "September", "October", "November", "December"
+];
     let dateOfTimestamp = new Date(+timestamp);
     const getTime = () => {
         let hours = dateOfTimestamp .getHours();
@@ -17,9 +20,17 @@ export function convert(timestamp){
         
         return `${date < 10 ? '0' + date : date}.${month < 10 ? '0' + month : month}.${year}`
     }
+
+    const getDateMonthName = () => {
+        let date = dateOfTimestamp.getDate();
+        let month = dateOfTimestamp.getMonth();
+        let year = dateOfTimestamp.getFullYear();
+
+        return `${date} ${monthNames[month]} ${year}`
+    }
     
     return {
-        getTime, getFullDate
+        getTime, getFullDate, getDateMonthName
     }
         
 }

+ 0 - 3
src/components/UserEditorModal/UserEditorModal.jsx

@@ -3,8 +3,6 @@ import CloseIcon from "../ChatInfoModal/icons8-close.svg";
 import { Avatar, Badge, IconButton, Modal } from "@mui/material";
 import { connect } from "react-redux";
 import { backendURL } from "../../helpers/gql";
-
-import Photo from "./icons8photo.png";
 import PasswordIcon from "./icons8_password.png";
 import LoginIcon from "./icons8_userlogin.png";
 import NickIcon from "./icons8_usernick.png";
@@ -84,7 +82,6 @@ const UserEditorModal = ({user, handleClose}) => {
                 <ModalChildContent>
                     {modalMap[subModalContent]}
                 </ModalChildContent>
-
             </Modal>
 
         </UserEditorModalWrapper>

+ 31 - 2
src/components/UserEditorModal/UserEditorSubModals/UserPasswordEditor.jsx

@@ -10,12 +10,39 @@ const UserPasswordEditor = ({login, disabled, handleClose, updateUser, checkPass
     const [currPassword, setCurrPassword] = useState('');
     const [newPassword, setNewPassword] = useState('');
     
-
+    const [errorP, setErrorP] = useState('')
     useEffect(() => {
         checkPassword(login, currPassword);
         
     }, [currPassword])
 
+    useEffect(() => {
+        if(errorP === false) {
+            handleClose()
+            updateUser(null, null, newPassword, null)
+        }
+    }, [errorP]);
+
+    function validatePassword(p){
+        
+        let errorsP = [];
+
+        if (p.length < 4 || p.length > 12) {
+            errorsP.push("Your password must be at least 4 and no more than 12 characters"); 
+        }
+        if (!/[a-z]/.test(p)) {
+            errorsP.push("Your password must contain at least one letter.");
+        }
+        if (!/[0-9]/.test(p))  {
+            errorsP.push("Your password must contain at least one digit."); 
+        }
+        if (errorsP.length > 0) {
+            setErrorP(errorsP.join('/'))
+        } else {
+            setErrorP(false)
+        }
+    }
+
     return(
         <UserEditorSubModalsWrap>
             <UserEditorSubModalsTitle>Edit your password</UserEditorSubModalsTitle>
@@ -30,6 +57,8 @@ const UserPasswordEditor = ({login, disabled, handleClose, updateUser, checkPass
             />
             <TextField
                 autoFocus
+                error={!!errorP}
+                helperText={errorP}
                 sx={{width: '100%', mt: '20px'}}
                 id="standard-basic" 
                 label="New Password" 
@@ -40,7 +69,7 @@ const UserPasswordEditor = ({login, disabled, handleClose, updateUser, checkPass
             />
             <UserEditorSubModalsFooter>
                 <Button variant="text" onClick={handleClose}>Cancel</Button>
-                <Button variant="text" disabled={newPassword ? false : true} onClick={() => {updateUser(null, null, newPassword, null); handleClose()}}>Save</Button>
+                <Button variant="text" disabled={newPassword ? false : true} onClick={() => {validatePassword(newPassword); }}>Save</Button>
             </UserEditorSubModalsFooter>
         </UserEditorSubModalsWrap>
     )

src/components/utils/addUploadDate.js → src/helpers/addUploadDate.js


src/components/utils/calculateFileSize.js → src/helpers/calculateFileSize.js


+ 9 - 0
src/helpers/jwt.js

@@ -0,0 +1,9 @@
+export const jwtDecode = token => {
+	try{
+		return JSON.parse(atob(token.split('.')[1]));
+	}
+	catch(e){
+		console.log(e.name, e.message);
+	}
+  }
+  

src/components/utils/saveFile.js → src/helpers/saveFile.js


+ 5 - 1
src/helpers/uploadFile.js

@@ -3,7 +3,11 @@ import  {backendURL} from "./gql";
 export const uploadFile = (file) => {
     console.log(file)
     const fd = new FormData;
-    fd.append('media', file)
+    if (file.name){
+        fd.append('media', file)
+    } else {
+        fd.append('media', file, 'record.wav')
+    }
 
     return fetch(`${backendURL}/upload`, {
         method: "POST",

+ 3 - 2
src/index.js

@@ -5,9 +5,10 @@ import App from './App';
 import reportWebVitals from './reportWebVitals';
 
 ReactDOM.render(
-  <React.StrictMode>
+  // <React.StrictMode>
     <App />
-  </React.StrictMode>,
+  // </React.StrictMode> 
+  ,
   document.getElementById('root')
 );
 

+ 60 - 42
src/pages/LoginPage.jsx

@@ -1,19 +1,11 @@
 import { actionFullLogin } from '../actions/actionLogin';
-
-
 import TextField from '@mui/material/TextField';
-import { Grid, IconButton, InputAdornment, Typography  } from '@mui/material';
-import {BrowserRouter as Router, Route, Link, Routes, Navigate} from 'react-router-dom';
-
-import { useState, useEffect, useRef } from 'react';
+import { IconButton, InputAdornment, Typography  } from '@mui/material';
+import {BrowserRouter as Router, Link,} from 'react-router-dom';
+import { useState, useEffect } from 'react';
 import { Visibility, VisibilityOff } from '@mui/icons-material';
 import Button from '@mui/material/Button';
 import { connect } from 'react-redux';
-import { history } from '../App';
-
-
-
-
 
 function LoginPage({onLogin }){
 
@@ -21,21 +13,56 @@ function LoginPage({onLogin }){
         login: '', 
         password: '',
         showPassword: false, 
-        error: false
     })
 
-    const [errorLogin, setErrorLogin] = useState(false);
-    const [errorPass, setErrorPass] = useState(false);
-
+    const [errorValues, setErrorValues] = useState({
+        login: '',
+        firstPassword: '',
+    })
 
-    const validate = () => {
-        values.login.length < 3 ? setErrorLogin(true) : setErrorLogin(false);
-        values.password.length < 3 ? setErrorPass(true) : setErrorPass(false);
-        values.password.length < 3 || values.login.length < 3 ? setValues({...values, error: true}) : setValues({...values, error: false})
+    useEffect(() => {
+        if(Object.values(errorValues).every(item => item === false)) {
+            onLogin(values.login, values.password)
+        }
+    }, [errorValues])
+
+    function validatePassword(fp,l) {
+        let e = {
+            login: '',
+            firstPassword: '',
+        }
+        let errorsP = [];
+        let errorsL = [];
         
+    
+        if (fp.length < 4 || fp.length > 12) {
+            errorsP.push("Your password must be at least 4 and no more than 12 characters"); 
+        }
+        if (!/[a-z]/.test(fp)) {
+            errorsP.push("Your password must contain at least one letter.");
+        }
+        if (!/[0-9]/.test(fp))  {
+            errorsP.push("Your password must contain at least one digit."); 
+        }
+        if (errorsP.length > 0) {
+            e = {...e, firstPassword: errorsP.join('/')}
+        } else {
+            e = {...e, firstPassword: false}
+        }
+        if(!/[a-z]/.test(l)){
+            errorsL.push("Your login must contain at least one letter");
+        }
+        if (l.length < 4 || l.length > 8) {
+            errorsL.push("Your login must be at least 4 and no more than 8 characters"); 
+        }
+        if (errorsL.length > 0){
+            e = {...e, login: errorsL.join('/')}
+        } else {
+            e = {...e, login: false}
+        }
+        setErrorValues({...errorValues, ...e})
     }
 
-
     const handleChange = (prop) => (event) => {
         setValues({...values, [prop] : event.target.value})
     }
@@ -47,30 +74,30 @@ function LoginPage({onLogin }){
         })
     }
 
-
     return (
-        <div className='LoginForm'>
+        <div style={{height: '100vh', display: 'flex', alignItems: 'center'}}>
             <form className='Form' noValidate autoComplete='off'>
                 
-                <Typography sx={{mb: '30px'}} variant='h4'>Вход</Typography>
-                <TextField 
-                    error={errorLogin}
+                <Typography sx={{mb: '30px'}} color='primary' variant='h4'>Login</Typography>
+
+                <TextField
+                    error={!!errorValues.login}
                     sx={{width: '100%', mb: '30px'}} 
                     required  
-                    label="Логин" 
+                    label="Login" 
                     variant="outlined" 
-                    helperText="минимум 3" 
+                    helperText={errorValues.login}
                     value={values.login}
                     autoFocus
                     onChange={handleChange('login')}/>
 
                 <TextField
-                    error={errorPass}
+                    error={!!errorValues.firstPassword}
                     sx={{width: '100%'}}
-                    helperText="минимум 3" 
+                    helperText={errorValues.firstPassword} 
                     required  
                     variant="outlined" 
-                    label="Пароль"
+                    label="Password"
                     type={values.showPassword ? 'text' : 'password'}
                     value={values.password}
                     onChange={handleChange('password')}
@@ -88,24 +115,15 @@ function LoginPage({onLogin }){
                             </InputAdornment>
                         )
                     }}
-                    
                 />
-
-                {values.error && <div>неверный {errorLogin && <span>логин </span>}{errorPass && <span>пароль</span>}</div>}
                 <Button 
-                onClick={
-                    () => {
-                        validate();
-                        if (!values.error){
-                            onLogin(values.login, values.password);
-                        }
-                    }
+                onClick={() => {validatePassword( values.password, values.login)}
                 } 
                 sx={{width: '200px', mt: '30px'}} 
                 variant="contained">
-                    Войти
+                    Submit
                 </Button><br/>
-                <Link className='RegisterBtn' to='/register'>Зарегистрироваться</Link>
+                <Link className='RegisterBtn' to='/register'>Registration</Link>
             </form>
         </div>
     )

+ 12 - 26
src/pages/MainPage.jsx

@@ -1,46 +1,32 @@
-// import { Grid } from "@mui/material";
-import React, { createContext, useContext, useEffect, useState } from "react";
-// import { Route, Switch } from "react-router-dom";
-import { actionAboutMe } from "../actions/actionAboutMe";
-import { actionGetMessageForChat } from "../actions/actionGetMessageForChat";
-import { store} from './../App';
-
-import {Avatar, Grid, List, ListItem} from '@mui/material';
-import {Router, Route, Link,  Navigate, Switch, Redirect} from 'react-router-dom';
-import { Provider,connect } from 'react-redux';
-import { actionSentOrUpdateMSG } from "../actions/actionsMessages";
-import CreateNewChat from "../components/CreateNewChat";
-
-import { backendURL } from "../helpers/gql";
-
-import { Aside } from "../components/Aside/Aside";
+import React, { useEffect } from "react";
+import { Grid } from '@mui/material';
+import { Route, Switch } from 'react-router-dom';
+import  Aside  from "../components/Aside/Aside";
 import ChatPageData from "../components/ChatPageData/ChatPageData";
-import ModalComponent, { CModalComponent } from '../components/Modal';
-
-///////////////////////////////////////////////////////////////////////////////////////////////
+import { CModalComponent } from '../components/Modal/Modal';
+import { connect } from "react-redux";
+import { actionAboutMe } from "../actions/actionAboutMe";
 
-export const MainPage = () => {
+const MainPage = ({actionAboutMe}) => {
     useEffect(() => {
-        store.dispatch(actionAboutMe())
+        actionAboutMe()
     },[])
     
 	return(
 			<main className='Main'>
-				
 				<Grid container columns={12}>
 					<Grid item xs={4} >
 							<Aside/>
 					</Grid>
-
 					<Grid item xs={8}>
 							<Switch>
 								<Route path="/main/:_id" exact component={ChatPageData}/>
-								
 							</Switch>
 					</Grid>
 				</Grid>
-
 				<CModalComponent/>
 			</main>
 	)	
-}
+}
+
+export default connect(null, {actionAboutMe: actionAboutMe})(MainPage)

+ 100 - 41
src/pages/RegisterPage.jsx

@@ -1,9 +1,8 @@
 import { TextField, Typography, IconButton, InputAdornment, Grid, Button } from "@mui/material"
-import { Visibility, VisibilityOff } from '@mui/icons-material';
-import { useState } from "react"
+import { useEffect, useState } from "react"
 import { actionFullRegister } from "../actions/actionLogin";
 import { connect } from "react-redux";
-
+import { Link } from "react-router-dom";
 
 function RegisterPage ({onRegister}) {
 
@@ -11,84 +10,144 @@ function RegisterPage ({onRegister}) {
         login: '',
         firstPassword: '',
         secondPassword: '',
-        nickname: '',
+        nick: '',
         showPassword: ''
     })
 
-    const [errorLogin, setErrorLogin] = useState(false);
-    const [errorPass, setErrorPass] = useState(false);
+    const [errorValues, setErrorValues] = useState({
+        login: '',
+        firstPassword: '',
+        secondPassword: '',
+        nick: ''
+    })
 
-    const handleChange = (prop) => (e) => {
-        setValues({...values, [prop] : e.target.value})
+    useEffect(() => {
+        if(Object.values(errorValues).every(item => item === false)) {
+            onRegister(values.login, values.firstPassword, values.nick)
+        }
+    }, [errorValues])
+
+
+function validatePassword(fp,l,n,sp) {
+    let e = {
+        login: '',
+        firstPassword: '',
+        secondPassword: '',
+        nick: ''
+    }
+    let errorsP = [];
+    let errorsL = [];
+    let errorsN = [];
+    let errorsSP = [];
+
+    if (fp.length < 4 || fp.length > 12) {
+        errorsP.push("Your password must be at least 4 and no more than 12 characters"); 
     }
+    if (!/[a-z]/.test(fp)) {
+        errorsP.push("Your password must contain at least one letter.");
+    }
+    if (!/[0-9]/.test(fp))  {
+        errorsP.push("Your password must contain at least one digit."); 
+    }
+    if (errorsP.length > 0) {
+        e = {...e, firstPassword: errorsP.join('/')}
+    } else {
+        e = {...e, firstPassword: false}
+    }
+    if(!/[a-z]/.test(l)){
+        errorsL.push("Your login must contain at least one letter");
+    }
+    if (l.length < 4 || l.length > 8) {
+        errorsL.push("Your login must be at least 4 and no more than 8 characters"); 
+    }
+    if (errorsL.length > 0){
+        e = {...e, login: errorsL.join('/')}
+    } else {
+        e = {...e, login: false}
+    }
+    if(!/[a-z]/.test(n)){
+        errorsN.push("Your nick must contain at least one letter");
+    }
+    if (n.length < 3 || n.length > 15) {
+        errorsN.push("Your nick must be at least 3 and no more than 15 characters"); 
+    }
+    if (errorsN.length > 0){
+        e = {...e, nick: errorsN.join('/')}
+    } else {
+        e = {...e, nick: false}
+    }
+    if(fp !== sp){
+        errorsSP.push("Password not match")
+    }
+    if (errorsSP.length > 0){
+        e = {...e, secondPassword: errorsSP.join('/')}
+    } else {
+        e = {...e, secondPassword: false}
+    }
+    setErrorValues({...errorValues, ...e})
+}
 
-    const handleClickShowPassword = () => {
-        setValues({
-            ...values, showPassword : !values.showPassword
-        })
+    const handleChange = (prop) => (e) => {
+        setValues({...values, [prop] : e.target.value})
     }
 
     return (
         <div className="RegisterPage">
             <form className="Form" noValidate autoComplete="off">
-                    <Typography sx={{mb: '20px'}} variant='h4'>Регистрация</Typography>
+                    <Typography sx={{mb: '20px'}} color='primary' variant='h4'>Registration form</Typography>
                     <TextField 
-                        error={errorLogin}
-                        sx={{width: '100%', mb: '30px'}} 
+                        error={!!errorValues.login}
+                        sx={{width: '100%', mb: (!!errorValues.login) ? '10px' : '30px'}} 
                         required  
-                        label="Логин" 
+                        label="Login" 
                         variant="outlined" 
-                        helperText="минимум 3" 
+                        helperText={errorValues.login}
                         value={values.login}
                         autoFocus
                         onChange={handleChange('login')}
                     />
                     <TextField
-                        sx={{width: '100%', mb: '30px'}}
-                        label="Ник"
+                        error={!!errorValues.nick}
+                        sx={{width: '100%', mb: !!errorValues.nick ? '10px' : '30px'}}
+                        label="Nick"
+                        helperText={errorValues.nick}
+                        required
                         variant="outlined"
-                        value={values.nickname}
+                        value={values.nick}
                         autoFocus
-                        onChange={handleChange('nickname')}
+                        onChange={handleChange('nick')}
                     />
 
                     <TextField
-                        error={errorPass}
-                        sx={{width: '100%'}}
-                        helperText="минимум 3" 
+                        error={!!errorValues.firstPassword}
+                        sx={{width: '100%', mb: !!errorValues.firstPassword ? '10px' : '30px'}}
+                        helperText={errorValues.firstPassword} 
                         required  
                         variant="outlined" 
-                        label="Пароль"
+                        label="Password"
                         type={'password'}
                         value={values.firstPassword}
                         onChange={handleChange('firstPassword')}
                         
                     />
                     <TextField
-                        error={errorPass}
-                        sx={{width: '100%'}}
-                        helperText="минимум 3" 
+                        error={!!errorValues.secondPassword}
+                        sx={{width: '100%', mb: !!errorValues.secondPassword ? '10px' : '30px'}}
+                        helperText={errorValues.secondPassword} 
                         required  
                         variant="outlined" 
-                        label="Повторите пароль"
+                        label="Confirm password "
                         type={'password'}
                         value={values.secondPassword}
                         onChange={handleChange('secondPassword')}
                     />
-            <Button                        //проверять payload чтобы понимать зарегался пользователь или имя уже существует
-                onClick={
-                    () => {
-                            onRegister(values.login, values.firstPassword, values.nickname);
-                    }
-                } 
+            <Button 
+                onClick={() => { validatePassword(values.firstPassword, values.login, values.nick, values.secondPassword)}} 
                 sx={{width: '200px', mt: '30px'}} 
                 variant="contained">
-                    Войти
-            </Button>
-                
-                
-                
-                
+                    Submit
+            </Button><br/>
+            <Link className='RegisterBtn' to='/login'>Login</Link>
             </form>
         </div>
     )

+ 2 - 10
src/reducers/authReducer.js

@@ -1,13 +1,5 @@
-const jwtDecode = token => {
-	try{
-		return JSON.parse(atob(token.split('.')[1]));
-		
-	}
-	catch(e){
-		console.log(e.name, e.message);
-	}
-  }
-  
+import { jwtDecode } from "../../helpers/jwt";
+
 export function authReducer(state, {type, token}){                                 
     if (state === undefined){
         if(localStorage.authToken){

+ 25 - 36
src/reducers/chatReducer.js

@@ -1,47 +1,24 @@
-export const actionSetDropMedia = (id, data) => ({type: 'SETMEDIA', data, id});
 
-export const actionDeleteDropMedia = (id) => ({type: 'CLEARMEDIA', id});
-
-export const actionDeleteOneMediaFile = (id, mediaKey) => ({type: 'DELETEMEDIA', id, mediaKey});
-
-export const actionChangeFile = (id, data, mediaKey) => ({type: 'CHANGE', id, data, mediaKey}); 
-
-export const actionSetInputMessageValue = (id, data, name) => ({type: 'SETINPUTMESSAGE', id, data, name});
-
-export const actionAddDraftMessage = (id, data) => ({type: 'ADDMESSAGEDRAFT', id, data});
-
-
-export const actionSetMessageEditor = (id, data) => ({type: 'MESEDITOR', id, data})
 
 export function chatReducer (state={}, {type, data, id, mediaKey, name}){
 	if(type === 'CHATS'){
-
-
 		let chats                               ////new chats
-
 		for(const value of data){
 			chats = {
 				...chats, 
 				[value._id]:{...value}
 			}
 		}
-
         let newState
-
-        	
         for (let prop in chats){
-			
             newState = {
                 ...state, ...newState, [prop] : {...state[prop], ...chats[prop]}
             }
         }
-
         let arr = Object.entries(newState || {});
-		
 		return {
 			...Object.fromEntries(arr.sort((a,b) => a[1].lastModified > b[1].lastModified ? -1 : 1))
 		}
-
 	}
 
     if(type === 'MSG'){
@@ -52,26 +29,25 @@ export function chatReducer (state={}, {type, data, id, mediaKey, name}){
 				[value._id]:{...value}
 			}
 		}
-
-		
-		
 		return {
 			...state, [id] : {...(state[id] || {_id: id, title: "loading"}),
              messages: Object.fromEntries(Object.entries({...(state[id]?.messages || {}), ...newMessages}).sort((a,b) => a[0] < b[0] ? 1 : -1))}
 		}
 	}
 
-
 	if(type === 'LEFTCHAT'){
         let newState = {...state};
         let arr = Object.entries(newState);
-
         return {
             ...Object.fromEntries(arr.filter((chat) => chat[0] != data._id))
         }
-	
-		 
 	}
+///
+    if(type === 'CLEARCHATS'){
+        return {
+            
+        }
+    }
 
 	if (type === 'SETMEDIA'){
         return {
@@ -104,12 +80,6 @@ export function chatReducer (state={}, {type, data, id, mediaKey, name}){
             ...state, [id]: {...state[id], draft: {...state[id].draft, [name]: {...(state[id]?.draft?.[name] || {}), value: data}}}
         }
     }
-
-    // if (type === 'CLEARINPUTMESSAGE'){
-    //     return {
-    //         ...state, [id]: {...state[id], draft: {...state[id].draft, [name]: ''}}
-    //     }
-    // }
 	
 	if (type === 'ADDMESSAGEDRAFT'){
 		return {
@@ -124,5 +94,24 @@ export function chatReducer (state={}, {type, data, id, mediaKey, name}){
 		}
 	}
 
+    if (type === 'ALLMEDIACHAT'){
+        let newMedia
+        for(const value of data){
+			newMedia = {
+				...newMedia, 
+				[value._id]:{...value}
+			}
+		}
+        return {
+            ...state, [id]: {...state[id], chatMedia: {...state[id].chatMedia, ...newMedia}}
+        }
+    }
+    
+    if (type === 'MEDIACHAT'){
+        return {
+            ...state,
+        }
+    }
+
 	return state 
 }

+ 0 - 14
src/reducers/modalReducer.js

@@ -1,17 +1,3 @@
-export const actionIsOpen = (value) => ({type: 'OPEN', value});
-
-export const actionModalContent = (value) => ({type: 'CONTENT', value});
-
-export const actionModalDraft = (value, draftName) => ({type: 'MODALDRAFT', value, draftName});
-
-
-
-export const actionOpenModal = (content) => 
-    dispatch => {
-        dispatch(actionModalContent(content));
-        dispatch(actionIsOpen(true));
-    }
-
 export function modalReducer(state = {}, {type, value, draftName}){
     if(type === 'OPEN'){
         return {

src/reducers/promiseReducer.js → src/redux/reducers/promiseReducer.js


+ 10 - 0
src/redux/store.js

@@ -0,0 +1,10 @@
+import { applyMiddleware, combineReducers, createStore } from 'redux';
+import thunk from 'redux-thunk';
+import { authReducer } from './reducers/authReducer';
+import { chatReducer } from './reducers/chatReducer';
+import { modalReducer } from './reducers/modalReducer';
+import { promiseReducer } from './reducers/promiseReducer';
+
+export const store = createStore(combineReducers({
+    promise: promiseReducer, auth: authReducer, chats: chatReducer, modal: modalReducer
+}), applyMiddleware(thunk))