Browse Source

commit 12.07 add Messeges

DimaBondarenko 1 year ago
parent
commit
94d2feed39
100 changed files with 3193 additions and 589 deletions
  1. 3 0
      .gitignore
  2. 256 54
      package-lock.json
  3. 8 2
      package.json
  4. 1 0
      public/index.html
  5. 30 334
      src/App.js
  6. 62 46
      src/App.scss
  7. 19 1
      src/actions/actionAboutMe.jsx
  8. 24 0
      src/actions/actionFindUser.jsx
  9. 80 0
      src/actions/actionGetMessageForChat.jsx
  10. 38 3
      src/actions/actionLogin.jsx
  11. 98 1
      src/actions/actionsForChats.jsx
  12. 36 0
      src/actions/actionsForUser.jsx
  13. 64 0
      src/actions/actionsMedia.jsx
  14. 165 0
      src/actions/actionsMessages.jsx
  15. 0 1
      src/actions/actionsPromise.jsx
  16. 0 37
      src/components/AppBar.jsx
  17. 36 0
      src/components/AppBar/AppBar.jsx
  18. 28 0
      src/components/AppBar/AppBar.style.js
  19. 19 0
      src/components/Aside/Aside.jsx
  20. 10 0
      src/components/Aside/Aside.style.js
  21. 38 0
      src/components/ChatEditorModal/ChatEditorModal.jsx
  22. 14 0
      src/components/ChatEditorModal/ChatEditorModal.style.js
  23. 91 0
      src/components/ChatInfoModal/ChatInfoModal.jsx
  24. 76 0
      src/components/ChatInfoModal/ChatInfoModal.style.js
  25. BIN
      src/components/ChatInfoModal/add-user.png
  26. 1 0
      src/components/ChatInfoModal/icons8-close.svg
  27. BIN
      src/components/ChatInfoModal/user.png
  28. 74 0
      src/components/ChatInfoOptions/ChatInfoOptions.jsx
  29. 1 0
      src/components/ChatInfoOptions/icons8_basket.svg
  30. BIN
      src/components/ChatInfoOptions/icons8_exit.png
  31. 32 0
      src/components/ChatList/ChatList.jsx
  32. 6 0
      src/components/ChatList/ChatList.style.js
  33. 49 0
      src/components/ChatListItem/ChatListItem.jsx
  34. 71 0
      src/components/ChatListItem/ChatListItem.style.js
  35. BIN
      src/components/ChatListItem/icons8_star.png
  36. BIN
      src/components/ChatListItem/icons8_star_white.png
  37. 35 0
      src/components/ChatPageData/ChatPageData.jsx
  38. 17 0
      src/components/ChatPageData/ChatPageData.style.js
  39. 26 0
      src/components/ChatPageHeader/ChatPageHeader.jsx
  40. 22 0
      src/components/ChatPageHeader/ChatPageHeader.style.js
  41. 71 0
      src/components/CreateNewChat.jsx
  42. 63 0
      src/components/Drawer/UserMenu.jsx
  43. 26 0
      src/components/Drawer/UserMenu.style.js
  44. BIN
      src/components/Drawer/icons8-chat.png
  45. 37 0
      src/components/DropMediaFilesOptions/DropMediaFilesOptions.jsx
  46. 18 0
      src/components/DropMediaFilesOptions/DropMediaFilesOptions.style.js
  47. BIN
      src/components/DropMediaFilesOptions/icons8-arrows.png
  48. 1 0
      src/components/DropMediaFilesOptions/icons8_basket.svg
  49. 63 0
      src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.jsx
  50. 74 0
      src/components/DropMediaItem/DropMediaFiles/DropMediaFiles.style.js
  51. 9 0
      src/components/DropMediaItem/DropMediaFiles/icons8-File.svg
  52. BIN
      src/components/DropMediaItem/DropMediaFiles/icons8-music.png
  53. 47 0
      src/components/DropMediaItem/DropMediaItem.jsx
  54. 7 0
      src/components/DropMediaItem/DropMediaItem.style.js
  55. 76 0
      src/components/DropZone/DropZoneAvatar.jsx
  56. 30 0
      src/components/DropZone/DropZoneAvatar.style.js
  57. 37 0
      src/components/DropZone/MessageDropZone/MessageDropZone.jsx
  58. BIN
      src/components/DropZone/MessageDropZone/icons8-dropIcon.png
  59. BIN
      src/components/DropZone/icons8_camera.png
  60. 13 0
      src/components/ForwardedMessage/ForwardedMessage.jsx
  61. 9 0
      src/components/ForwardedMessage/ForwardedMessage.style.js
  62. 60 0
      src/components/InputArea/InputArea.jsx
  63. 33 0
      src/components/InputArea/InputArea.style.js
  64. 55 0
      src/components/InputArea/InputAreaMessageEditor.jsx
  65. BIN
      src/components/InputArea/icons8-plane.png
  66. 0 110
      src/components/LoginForm.jsx
  67. 19 0
      src/components/MemberList/MemberList.jsx
  68. 21 0
      src/components/MemberList/MemberList.style.js
  69. 56 0
      src/components/Message/Message.jsx
  70. 52 0
      src/components/Message/Message.style.js
  71. 43 0
      src/components/MessageDraft/MessageDraft.style.js
  72. 16 0
      src/components/MessageDraft/MessageDraftBox.jsx
  73. 27 0
      src/components/MessageDraft/MessageEditor.jsx
  74. 31 0
      src/components/MessageDraft/ReplyMessageDraft.jsx
  75. 66 0
      src/components/MessageEditorMediaModal/MessageEditorMediaModal.jsx
  76. 19 0
      src/components/MessageEditorMediaModal/MessageEditorMediaModal.style.js
  77. 16 0
      src/components/MessageMediaContainer/MessageMediaContainer.jsx
  78. 11 0
      src/components/MessageMediaContainer/MessageMediaContainer.style.js
  79. 31 0
      src/components/MessageMediaItem/MessageMediaItem.jsx
  80. 5 0
      src/components/MessageMediaItem/MessageMediaItem.style.js
  81. 32 0
      src/components/MessageMediaItems/MessageAudioItem/AudioItem.jsx
  82. 41 0
      src/components/MessageMediaItems/MessageAudioItem/AudioItem.style.js
  83. BIN
      src/components/MessageMediaItems/MessageAudioItem/icons8-download.png
  84. 25 0
      src/components/MessageMediaItems/MessageFileItem/MessageFileItem.jsx
  85. 0 0
      src/components/MessageMediaItems/MessageFileItem/MessageFileItem.style.js
  86. 12 0
      src/components/MessageMediaItems/MessageImgItem/MessageImgItem.jsx
  87. 16 0
      src/components/MessageMediaItems/MessageImgItem/MessageImgItem.style.js
  88. 12 0
      src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.jsx
  89. 15 0
      src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.style.js
  90. 97 0
      src/components/MessageMediaModal/MessageMediaModal.jsx
  91. 31 0
      src/components/MessageMediaModal/MessageMediaModal.style.js
  92. 59 0
      src/components/MessageOptions/MessageOptions.jsx
  93. 16 0
      src/components/MessageOptions/MessageOptions.style.js
  94. BIN
      src/components/MessageOptions/icons8-forward.png
  95. BIN
      src/components/MessageOptions/icons8-reply.png
  96. 69 0
      src/components/MessagesArea/MessagesArea.jsx
  97. 20 0
      src/components/MessagesArea/MessagesArea.style.js
  98. 57 0
      src/components/Modal.jsx
  99. 19 0
      src/components/NewChatButton.jsx
  100. 0 0
      src/components/ReplyMessage/ReplyMessage.jsx

+ 3 - 0
.gitignore

@@ -21,3 +21,6 @@
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
+
+
+mongodb+srv://dimabondarenko2404:24041993@graphql-chat.0xizk.mongodb.net/graphql-chat?retryWrites=true&w=majority

+ 256 - 54
package-lock.json

@@ -10,22 +10,28 @@
       "dependencies": {
         "@emotion/react": "^11.7.1",
         "@emotion/styled": "^11.6.0",
-        "@mui/icons-material": "^5.4.1",
+        "@mdi/js": "^6.6.96",
+        "@mdi/react": "^1.5.0",
+        "@mui/icons-material": "^5.8.4",
         "@mui/material": "^5.4.1",
         "@mui/styled-engine-sc": "^5.4.1",
         "@testing-library/jest-dom": "^5.16.2",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "file-saver": "^2.0.5",
         "node-sass": "^7.0.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-dropzone": "^12.0.4",
+        "react-infinite-scroll-component": "^6.1.0",
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
         "react-scripts": "5.0.0",
+        "react-textarea-autosize": "^8.3.4",
         "redux": "^4.1.2",
         "redux-thunk": "^2.4.1",
-        "styled-components": "^5.3.3",
+        "simplebar-react": "^2.3.7",
+        "styled-components": "^5.3.5",
         "web-vitals": "^2.1.4"
       }
     },
@@ -1790,9 +1796,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
-      "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
+      "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
       "dependencies": {
         "regenerator-runtime": "^0.13.4"
       },
@@ -2864,6 +2870,21 @@
         "@jridgewell/sourcemap-codec": "^1.4.10"
       }
     },
+    "node_modules/@juggle/resize-observer": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
+      "integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
+    },
+    "node_modules/@mdi/js": {
+      "version": "6.6.96",
+      "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz",
+      "integrity": "sha512-ke9PN5DjPCOlMfhioxeZYADz8Yiz6v47W0IYRza01SSJD7y1EwESVpwFnnFUso+eCoWtE1CO9cTIvQF6sEreuA=="
+    },
+    "node_modules/@mdi/react": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.5.0.tgz",
+      "integrity": "sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w=="
+    },
     "node_modules/@mui/base": {
       "version": "5.0.0-alpha.68",
       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.68.tgz",
@@ -2896,11 +2917,11 @@
       }
     },
     "node_modules/@mui/icons-material": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.4.1.tgz",
-      "integrity": "sha512-koiq9q2GfjXRUWcC5fEi1b+EA4vfJHgIaAdBHlkOrBx2cnmmazQcyib501eodPfaZGx9BikrhivODaNQYQq8hA==",
+      "version": "5.8.4",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
+      "integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
       "dependencies": {
-        "@babel/runtime": "^7.17.0"
+        "@babel/runtime": "^7.17.2"
       },
       "engines": {
         "node": ">=12.0.0"
@@ -2911,8 +2932,8 @@
       },
       "peerDependencies": {
         "@mui/material": "^5.0.0",
-        "@types/react": "^16.8.6 || ^17.0.0",
-        "react": "^17.0.0"
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -5675,6 +5696,11 @@
       "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
       "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
     },
+    "node_modules/can-use-dom": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/can-use-dom/-/can-use-dom-0.1.0.tgz",
+      "integrity": "sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo="
+    },
     "node_modules/caniuse-api": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -8170,6 +8196,11 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "node_modules/file-selector": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
@@ -11825,6 +11856,11 @@
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
       "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
     },
+    "node_modules/lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+    },
     "node_modules/lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -14665,6 +14701,17 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
       "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
     },
+    "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",
+      "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
+      "dependencies": {
+        "throttle-debounce": "^2.1.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0"
+      }
+    },
     "node_modules/react-is": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -14829,6 +14876,22 @@
         }
       }
     },
+    "node_modules/react-textarea-autosize": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz",
+      "integrity": "sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.10.2",
+        "use-composed-ref": "^1.3.0",
+        "use-latest": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/react-transition-group": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
@@ -15828,6 +15891,32 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
     },
+    "node_modules/simplebar": {
+      "version": "5.3.6",
+      "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-5.3.6.tgz",
+      "integrity": "sha512-FJUMbV+hNDd/m+1/fvD41TXKd5mSdlI5zgBygkaQIV3SffNbcLhSbJT6ufTs8ZNRLJ6i+qc/KCFMqWmvlGWMhA==",
+      "dependencies": {
+        "@juggle/resize-observer": "^3.3.1",
+        "can-use-dom": "^0.1.0",
+        "core-js": "^3.0.1",
+        "lodash.debounce": "^4.0.8",
+        "lodash.memoize": "^4.1.2",
+        "lodash.throttle": "^4.1.1"
+      }
+    },
+    "node_modules/simplebar-react": {
+      "version": "2.3.7",
+      "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-2.3.7.tgz",
+      "integrity": "sha512-C+yPcLq6W/Sqiexx89Jw0Z4RrWtjqXsWh/OHhZv7893HMZ96c32JZWWu8mfVlZxZaZ/8u9h66FzWCs3atzeu7g==",
+      "dependencies": {
+        "prop-types": "^15.6.1",
+        "simplebar": "^5.3.6"
+      },
+      "peerDependencies": {
+        "react": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0",
+        "react-dom": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0"
+      }
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -16318,13 +16407,14 @@
       }
     },
     "node_modules/styled-components": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
-      "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
+      "version": "5.3.5",
+      "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
+      "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
+      "hasInstallScript": true,
       "dependencies": {
         "@babel/helper-module-imports": "^7.0.0",
         "@babel/traverse": "^7.4.5",
-        "@emotion/is-prop-valid": "^0.8.8",
+        "@emotion/is-prop-valid": "^1.1.0",
         "@emotion/stylis": "^0.8.4",
         "@emotion/unitless": "^0.7.4",
         "babel-plugin-styled-components": ">= 1.12.0",
@@ -16346,19 +16436,6 @@
         "react-is": ">= 16.8.0"
       }
     },
-    "node_modules/styled-components/node_modules/@emotion/is-prop-valid": {
-      "version": "0.8.8",
-      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
-      "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
-      "dependencies": {
-        "@emotion/memoize": "0.7.4"
-      }
-    },
-    "node_modules/styled-components/node_modules/@emotion/memoize": {
-      "version": "0.7.4",
-      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
-      "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
-    },
     "node_modules/stylehacks": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.3.tgz",
@@ -16811,6 +16888,14 @@
       "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
       "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w=="
     },
+    "node_modules/throttle-debounce": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
+      "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/thunky": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
@@ -17164,6 +17249,43 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/use-composed-ref": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
+      "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/use-latest": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
+      "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
+      "dependencies": {
+        "use-isomorphic-layout-effect": "^1.1.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -19400,9 +19522,9 @@
       }
     },
     "@babel/runtime": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
-      "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
+      "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
       "requires": {
         "regenerator-runtime": "^0.13.4"
       }
@@ -20202,6 +20324,21 @@
         "@jridgewell/sourcemap-codec": "^1.4.10"
       }
     },
+    "@juggle/resize-observer": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
+      "integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
+    },
+    "@mdi/js": {
+      "version": "6.6.96",
+      "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.6.96.tgz",
+      "integrity": "sha512-ke9PN5DjPCOlMfhioxeZYADz8Yiz6v47W0IYRza01SSJD7y1EwESVpwFnnFUso+eCoWtE1CO9cTIvQF6sEreuA=="
+    },
+    "@mdi/react": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.5.0.tgz",
+      "integrity": "sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w=="
+    },
     "@mui/base": {
       "version": "5.0.0-alpha.68",
       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.68.tgz",
@@ -20217,11 +20354,11 @@
       }
     },
     "@mui/icons-material": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.4.1.tgz",
-      "integrity": "sha512-koiq9q2GfjXRUWcC5fEi1b+EA4vfJHgIaAdBHlkOrBx2cnmmazQcyib501eodPfaZGx9BikrhivODaNQYQq8hA==",
+      "version": "5.8.4",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
+      "integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
       "requires": {
-        "@babel/runtime": "^7.17.0"
+        "@babel/runtime": "^7.17.2"
       }
     },
     "@mui/material": {
@@ -22218,6 +22355,11 @@
       "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
       "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
     },
+    "can-use-dom": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/can-use-dom/-/can-use-dom-0.1.0.tgz",
+      "integrity": "sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo="
+    },
     "caniuse-api": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -24061,6 +24203,11 @@
         "schema-utils": "^3.0.0"
       }
     },
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "file-selector": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
@@ -26698,6 +26845,11 @@
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
       "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
     },
+    "lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+    },
     "lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -28639,6 +28791,14 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
       "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
     },
+    "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",
+      "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
+      "requires": {
+        "throttle-debounce": "^2.1.0"
+      }
+    },
     "react-is": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -28768,6 +28928,16 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-textarea-autosize": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz",
+      "integrity": "sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==",
+      "requires": {
+        "@babel/runtime": "^7.10.2",
+        "use-composed-ref": "^1.3.0",
+        "use-latest": "^1.2.1"
+      }
+    },
     "react-transition-group": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
@@ -29523,6 +29693,28 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
     },
+    "simplebar": {
+      "version": "5.3.6",
+      "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-5.3.6.tgz",
+      "integrity": "sha512-FJUMbV+hNDd/m+1/fvD41TXKd5mSdlI5zgBygkaQIV3SffNbcLhSbJT6ufTs8ZNRLJ6i+qc/KCFMqWmvlGWMhA==",
+      "requires": {
+        "@juggle/resize-observer": "^3.3.1",
+        "can-use-dom": "^0.1.0",
+        "core-js": "^3.0.1",
+        "lodash.debounce": "^4.0.8",
+        "lodash.memoize": "^4.1.2",
+        "lodash.throttle": "^4.1.1"
+      }
+    },
+    "simplebar-react": {
+      "version": "2.3.7",
+      "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-2.3.7.tgz",
+      "integrity": "sha512-C+yPcLq6W/Sqiexx89Jw0Z4RrWtjqXsWh/OHhZv7893HMZ96c32JZWWu8mfVlZxZaZ/8u9h66FzWCs3atzeu7g==",
+      "requires": {
+        "prop-types": "^15.6.1",
+        "simplebar": "^5.3.6"
+      }
+    },
     "sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -29903,13 +30095,13 @@
       "requires": {}
     },
     "styled-components": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
-      "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
+      "version": "5.3.5",
+      "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
+      "integrity": "sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==",
       "requires": {
         "@babel/helper-module-imports": "^7.0.0",
         "@babel/traverse": "^7.4.5",
-        "@emotion/is-prop-valid": "^0.8.8",
+        "@emotion/is-prop-valid": "^1.1.0",
         "@emotion/stylis": "^0.8.4",
         "@emotion/unitless": "^0.7.4",
         "babel-plugin-styled-components": ">= 1.12.0",
@@ -29917,21 +30109,6 @@
         "hoist-non-react-statics": "^3.0.0",
         "shallowequal": "^1.1.0",
         "supports-color": "^5.5.0"
-      },
-      "dependencies": {
-        "@emotion/is-prop-valid": {
-          "version": "0.8.8",
-          "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
-          "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
-          "requires": {
-            "@emotion/memoize": "0.7.4"
-          }
-        },
-        "@emotion/memoize": {
-          "version": "0.7.4",
-          "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
-          "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
-        }
       }
     },
     "stylehacks": {
@@ -30256,6 +30433,11 @@
       "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
       "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w=="
     },
+    "throttle-debounce": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
+      "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ=="
+    },
     "thunky": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
@@ -30529,6 +30711,26 @@
         "punycode": "^2.1.0"
       }
     },
+    "use-composed-ref": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
+      "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
+      "requires": {}
+    },
+    "use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "requires": {}
+    },
+    "use-latest": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
+      "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
+      "requires": {
+        "use-isomorphic-layout-effect": "^1.1.1"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

+ 8 - 2
package.json

@@ -5,22 +5,28 @@
   "dependencies": {
     "@emotion/react": "^11.7.1",
     "@emotion/styled": "^11.6.0",
-    "@mui/icons-material": "^5.4.1",
+    "@mdi/js": "^6.6.96",
+    "@mdi/react": "^1.5.0",
+    "@mui/icons-material": "^5.8.4",
     "@mui/material": "^5.4.1",
     "@mui/styled-engine-sc": "^5.4.1",
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "file-saver": "^2.0.5",
     "node-sass": "^7.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-dropzone": "^12.0.4",
+    "react-infinite-scroll-component": "^6.1.0",
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
+    "react-textarea-autosize": "^8.3.4",
     "redux": "^4.1.2",
     "redux-thunk": "^2.4.1",
-    "styled-components": "^5.3.3",
+    "simplebar-react": "^2.3.7",
+    "styled-components": "^5.3.5",
     "web-vitals": "^2.1.4"
   },
   "scripts": {

+ 1 - 0
public/index.html

@@ -12,6 +12,7 @@
     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
     <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
+    <link  rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap">
     <!--
       manifest.json provides metadata used when your web app is installed on a
       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

+ 30 - 334
src/App.js

@@ -11,361 +11,70 @@ import {Router, Route, Link,  Navigate, Switch, Redirect} from 'react-router-dom
 import { createBrowserHistory } from 'history'
 
 
-import LoginForm from './components/LoginForm';
+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 SearchAppBar from './components/AppBar';
-import UserMenu from './components/UserMenu';
-import {Avatar, Grid, List, ListItem} from '@mui/material';
-import { messageReducer } from './reducers/messageReducer';
-import {chatReducer} from './reducers/chatReducer';
-
-// import { uploadFile } from '../helpers/uploadFile';
-
-export const history = createBrowserHistory();
-
-
-
 
-// function DropZMessage({onLoad, nick, url}) {
-// 	const {acceptedFiles, getRootProps, getInputProps} = useDropzone();
-
-// 	useEffect(()=>{
-// 		console.log(acceptedFiles)
-// 		acceptedFiles[0] && onLoad(acceptedFiles[0])
-		
-// 	}, [acceptedFiles])
-
-// 	return (
-// 		<div className="DropZMessage">
-// 			<div {...getRootProps({className: 'dropzone'})}>
-// 				<input {...getInputProps()} />
-// 			</div>
-// 		</div>
-// 	);
-//   }
-  
-//   <DropZ />
-
-
-
-export const actionAddChats = (data) => ({type: 'CHATS', data})
-const actionAddChat = (chat) => ({type: 'CHATS', data: [chat]})
-
-const actionAddMessages = (data, id) => ({type: 'MSG', data, id});
-const actionAddMessage = (message, id) => ({type: 'MSG', data: [message], id});
-const actionClearMessage = (data) => ({type: 'CLEARMSG', data})
-
-const actionAddLastMessage = (chat) => ({type: 'LASTMSG', data: [chat]})
-
-const actionLeftChat = (data) => ({type: 'LEFTCHAT', data})
-const actionFindChat = () => 
-actionPromise('Chat', gql(`query FindChat($q: String) {
-	MessageFind(query: $q) {
-	  _id  text
-	  
-	}
-  }`, {q: JSON.stringify([{}])}))
-
-const actionCreateChat = () => 
-actionPromise('newChat', gql(`mutation createChat($chat: ChatInput) {
-	ChatUpsert(chat: $chat) {
-	  _id
-	  members {
-		_id
-		login
-	  }
-	}
-  }`, ))
-
-
-// const actionFindChatsByUserId = (_id) => 
-// actionPromise('chatsByUserId', gql(`query findUserOne($q1: String) {
-// 	UserFindOne(query: $q1) {
-// 	  nick
-// 	  login
-// 	  _id
-// 	  chats {
-// 		_id
-// 		lastMessage {
-// 			text
-// 			createdAt
-// 		  }
-// 	  }
-	  
-// 	}
-//   }`, {q1: JSON.stringify([{_id}])}))
-
-
-// export const actionGetFullInfo = (_id) => 
-//  	async dispatch => {
-// 		 let user = await dispatch(
-// 			 actionFindChatsByUserId(_id)
-// 		 )
-// 		 if(user){
-// 			//  console.log(user.chats)
-// 			 dispatch(actionAddChats([...user.chats]))
-// 		 }
-// 	 }
-
-
-
-export const actionGetMessageForChat = (_id) => 
-	async (dispatch,getState) => {
-		let messages = await dispatch(
-			actionPromise('messages', gql(`query FindMessChat($chat: String) {
-				MessageFind(query: $chat) {
-				  _id
-				  text
-				  createdAt
-				  owner {
-					nick
-					avatar {
-					  url
-					}
-				  }
-				}
-			  }`, {chat: JSON.stringify([{"chat._id": _id}, {sort: [{_id: -1}]}])}))
-		)
-		if(messages){
-			//getState().
-
-			dispatch(actionAddMessages([...messages], _id))
-		}
-	}
-const actionGetOneChat = (_id) => 
-	async dispatch => {
-		let chat = await dispatch(
-			actionPromise('oneChat', gql(`query findChatById($chatId: String) {
-				ChatFindOne(query: $chatId) {
-				  _id
-				  title
-				  lastModified
-				  lastMessage {
-					text
-					createdAt
-				  }
-				}
-			  }`, {chatId: JSON.stringify([{_id}])}))
-		)
-		if(chat){
-			// dispatch(actionAddChat(chat))
-			console.log(chat)
-		}
-	}
-
-const actionSentOrUpdateMSG = (chatId, text, msgId) => 
-	actionPromise('updateMSG', gql(`mutation MessageUpsert($message: MessageInput) {
-		MessageUpsert(message: $message) {
-		  _id
-		  createdAt
-		  text
-		  owner {
-			nick
-			avatar {
-			  url
-			}
-		  }
-		  chat{
-			_id
-		  }
-		}
-	  }`, {
-		  message : {
-			  _id: msgId,
-			  chat: {_id: chatId},
-			  text
-		  }
-	  }))
-
-const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, chats: chatReducer}), applyMiddleware(thunk))
-console.log(store.getState())
-store.subscribe(() => console.log(store.getState()));
-// store.dispatch(actionFindChat())
-// store.dispatch(actionAuthLogout())
-
-// store.dispatch(actionGetFullInfo("6200314536a19525919c0402")) //////////////полная инфа после логина 
-
-// let newChat = {_id: '620214de36a1952', title: 'newc2', messages: null}
-// let newChat2  = {_id: '620214de36a19523', title: 'newc2', messages: null}
-// store.dispatch(actionAddChat(newChat))
-// store.dispatch(actionAddChat(newChat2))            //один чат с сокета
 
-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(msg); store.dispatch(actionAddMessage(msg, msg.chat._id)); store.dispatch(actionAddChat(msg.chat))})
-socket.on('chat', chat => {console.log(chat); store.dispatch(actionAddChat(chat))})
-socket.on('chat_left', data => {console.log(data); store.dispatch(actionLeftChat(data)); });
 
-// socket.on("connect", () => {
-// 	console.log(socket.id)
-// })
-// socket.disconnect(true)
-// socket.connect()
-
-
-
-
-const Message = ({mes}) => {
-	return (
-		<div className='Message'>
-			<div>{mes.text}</div>
-			<div>{mes.createdAt}</div>
-		</div>
-	)
-}
-
-const WrapperPageChat = ({children}) => {
-	return (
-		<div className='WrapperPageChat'>{children}</div>
-	)
-} 
-
-const InputMessage = ({onclick, chatId}) => {
+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';
 
-	const [inputValue, setValue] = useState('')
 
-	return (
-		<div className='InputMessage'>
-			<input value={inputValue} onChange={(e) => setValue(e.target.value)} type="text"/>	
-			<button onClick={() => onclick(chatId, inputValue)}>Отправить</button>
-		</div>
-	)
-}
+// import { uploadFile } from '../helpers/uploadFile';
 
-const CInputMessage = connect(null, {onclick: actionSentOrUpdateMSG})(InputMessage)
+export const history = createBrowserHistory();
 
-const PageChat = ({chats, chatId}) => {
-	
-	// console.log(chats[chatId].messages)
-	const messagesByChat = Object.values(chats[chatId]?.messages || {})
-	return (
-		<div className='PageChat'>
-			{messagesByChat.map((item) => <Message key={item._id} mes={item}/>)}
-		</div>
-	)
-}
 
-const CPageChat = connect(state=>({chats: state.chats || {}}))(PageChat)
 
 
-const ChatGetData = ({match:{params:{_id}}, getData}) => {
-	useEffect(() => {
-		getData(_id)
-	}, [_id])
 
-	return (
-		<WrapperPageChat>
-			
-			<CPageChat chatId={_id}/>
-			<CInputMessage chatId={_id}/>
-		</WrapperPageChat>
-	)
-}
 
-const CChatGetData = connect(null, {getData: actionGetMessageForChat})(ChatGetData)
 
-const ChatItem = ({chat, handleSet, activeElementId}) => {
-	
-	return (
-			<ListItem  className='ChatItem' onClick={() => handleSet(chat._id)}>
-				<Link className='ChatItem-Link' style={{display: 'flex', textDecoration: 'none', padding: '10px', backgroundColor: 'rgb(245, 245, 245)'}}  to={`/main/${chat._id}`}>
-					<div>
-						<Avatar
-							alt={chat.title}
-							// src={`${backendURL}/${chat}`}
-							sx={{ width: 50, height: 50, mr: '20px'}}
-						/>
-					</div>
-					<div>
-						<div >{chat.title}</div>
-						<div>{chat.lastMessage?.text || 'пусто'}</div>
-					</div>
-				</Link> 
-			</ListItem>
-	)
-}
 
-const Chats = ({chats}) => {
 
-	let [stateId, setStateId] = useState(null)
 
-	useEffect(() => {
-		console.log(stateId)
-	},[stateId])
 
-	
 
-	let chatsArr = Object.values(chats)
-	return (
-		<div className='Chats'>
-			<List sx={{p: '0'}}>
-				{chatsArr.map((item, index) => <ChatItem key={item._id} activeElementId={stateId} chat={item} handleSet={setStateId}/>)}
-			</List>
-		</div>
-	)
-}
+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()));
 
-const CChats = connect(state => ({chats: state.chats || []}))(Chats)
 
+if (localStorage.authToken) socket.emit('jwt', localStorage.authToken)
 
-const Aside = () => {
-	const [isOpen, setOpen] = useState(false);
-	
-	return (
-		<aside className='Aside'>
-			<SearchAppBar openUserMenu={() => setOpen(true)}/>
-			<CChats/>
-			<UserMenu open={isOpen} closeUserMenu={() => setOpen(false)}/>
-		</aside>
-	)
-}
+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));
 
-const BackgroundPage = ({children}) => 
-	<div className='BackgroundPage'>{children}</div> 
+socket.on("connect", () => {
+	console.log(socket.id)
+})
 
-const Alert = () => 
-<div>нет чата</div>
 
-const Main = () => {
-	
-useEffect(() => {
-	store.dispatch(actionAboutMe())
-},[])
-
-	return(
-		<main className='Main'>
-			<Grid container columns={12}>
-				<Grid item xs={4} >
-					<Aside/>
-				</Grid>
-				<Grid item xs={8}>
-					<BackgroundPage>
-						<Switch>
-							<Route path="/main/delete" exact component={Alert}/>
-							<Route path="/main/:_id" exact component={CChatGetData}/>
-						</Switch>
-					</BackgroundPage>
-				</Grid>
-			</Grid>
-		</main>
-	)	
-	
-}
 
 const AllRoutes = ({auth}) => {
 	return (
 				<Switch>
-					<Route path="/login" component={LoginForm} />
-					<Route path="/main" component={Main} />
-					{/* <Route exact path="/">{auth ? <Redirect to="/main"/> : <Redirect to="/login" /> }</Route> */}
+					<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)
@@ -379,11 +88,8 @@ function App() {
 	<Router history={history}>
 		<Provider store={store}>
 			<div className="App">
-				
 				<CAllRoutes />
 			</div>
-			<button onClick={()=> store.dispatch(actionAuthLogout())}>выйти</button>
-			<button >click</button>
 			
 		</Provider>
  	</Router>
@@ -392,13 +98,3 @@ function App() {
 
 export default App;
 
-
-
-
-
-
-
-
-
-
-// по промису чатов делать акшончат

+ 62 - 46
src/App.scss

@@ -1,56 +1,72 @@
+*{
+	box-sizing: border-box;
+}
+
 .App {
+  
   text-align: center;
-	
-	// background-color: rgb(201, 201, 201);
-	.LoginForm{
-		width: 400px;
-		margin: 0 auto;
-    padding: 100px;
+  font-family: 'Poppins', sans-serif;
+  color: #e2e2e2;
+	.RegisterPage{
+		height: 100vh;
+		display: flex;
+		align-items: center;
 	}
-	.Aside{
-		width: 100%;
-		height: calc(100vh - 64px);
-		.Chats{
-			width: 100%;
-			height: 100%;
-			overflow: hidden auto;
-			.ChatItem{
-				padding: 0;
-				&-Link{
-					width: 100%;
-				}
-			}
-			.ChatItem_active{
-				background-color: cornflowerblue;
-			}
-		}
+	// background-color: #a1a1a1;
+	.Form{
+		max-width: 400px;
+		margin: 0 auto;
+    	padding: 20px;
 	}
-	.Main{
-		.WrapperPageChat{
-			height: 100%;
-			.PageChat{
-				color: white;
-				width: 100%;
-				height: 100%;
-				overflow: hidden auto;
-				display: flex;
-				flex-direction: column-reverse;
-			}
-			.InputMessage{
-				height: 100px;
-			}
-		}
-		
-		
+	
+	
+}
+
+// .color{
+// 	background-color: rgb(138, 150, 95);
+// }
 
-		.BackgroundPage{
-			width: 100%;
-			height: 100vh;
-			background: url("img/20518677.jpg") center center/cover no-repeat;
-		}
+.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;
+		margin: 20px 10px 10px;
+	}
+	&>div:nth-child(2){
+		display: flex;
+		justify-content: end;
 	}
 }
 
+.ModalBox{
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	min-height: 50px;
+	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;
+// }

+ 19 - 1
src/actions/actionAboutMe.jsx

@@ -1,6 +1,6 @@
 import { actionPromise } from "./actionsPromise";
 import { gql } from "../helpers/gql";
-import { actionAddChats } from "../App";
+import { actionAddChats } from "./actionsForChats";
 
 export const actionFindUser = (_id) => (
     actionPromise('aboutMe', gql(`query findUserOne($q: String) {
@@ -18,10 +18,28 @@ export const actionFindUser = (_id) => (
              title
              createdAt
              lastModified
+             owner{
+                _id
+             }
              lastMessage {
                 text 
                 createdAt
+                owner{
+                  _id 
+                  nick
+                }
               }
+             avatar{
+                _id
+                url
+             }
+             members{
+                _id
+                nick
+                avatar{
+                   url
+                }
+             }
           }
        }     
     }`, { 

+ 24 - 0
src/actions/actionFindUser.jsx

@@ -0,0 +1,24 @@
+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}]
+              }
+          ])
+      } ))

+ 80 - 0
src/actions/actionGetMessageForChat.jsx

@@ -0,0 +1,80 @@
+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
+						owner {
+							nick
+						  }
+						  media {
+							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
+	}

+ 38 - 3
src/actions/actionLogin.jsx

@@ -3,11 +3,15 @@ import { gql } from "../helpers/gql";
 import { history } from '../App';
 import { actionAboutMe } from "./actionAboutMe";
 
-export const socket = window.io("ws://chat.fs.a-level.com.ua")
+export const socket = window.io("ws://chat.ed.asmer.org.ua/")
 
 
 const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
-export const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
+
+export const actionAuthLogout = () => {
+  history.push('/')
+  return ({type: 'AUTH_LOGOUT'})
+}
 
 export const actionFullLogin = (log, pass) => 
 async (dispatch) => {
@@ -23,4 +27,35 @@ async (dispatch) => {
 	  
   }
   return token
-}
+}
+
+export const actionCheckPassword = (log, pass) => 
+async (dispatch) => {
+  let token = await dispatch(
+	  actionPromise('checkedPassword', gql(`query login($login: String, $password: String) {
+	  login(login: $login, password: $password)
+	  }`, {login: log, password: pass}))
+  );
+  console.log(token)
+}
+
+export const actionFullRegister = (log, pass, nick) => 
+    async dispatch => {
+        let user = await dispatch(
+            actionPromise('register', gql( `mutation register($user: UserInput) {
+                UserUpsert(user: $user) {
+                   _id
+                   login
+                 }
+               }`, {
+                 user: {
+                    login: log,
+                    password: pass,
+                    nick: nick
+                 }
+               }))
+        )
+        if(user){
+            dispatch(actionFullLogin(log, pass));
+        }
+    }

+ 98 - 1
src/actions/actionsForChats.jsx

@@ -1,6 +1,23 @@
 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 actionLeftChat = (data) => {
+	const [,route, histId] = history.location.pathname.split('/');
+	if(histId  === data._id){
+		history.push('/')
+	}
+	return ({type: 'LEFTCHAT', data})
+}
 
 
 export const actionGetAllChats = (userId) => (
@@ -27,4 +44,84 @@ export const actionGetAllChats = (userId) => (
           q: JSON.stringify([ { 'members._id': userId } ])
           }
     ))
- )
+ )
+
+export const actionUpdateChat = (chat) => 
+   actionPromise('newChat', gql(`mutation createChat($chat: ChatInput) {
+      ChatUpsert(chat: $chat) {
+        _id
+        title
+        members {
+          login
+          _id
+        }
+      }
+    }`, {chat: chat}))
+
+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));
+      }
+   }
+
+   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 actionUpdateChat = (id, title, members) => 
+//    async (dispatch, getState) => {
+//       //remove all null
+//       let chat = Object.fromEntries(Object.entries({_id: id, title: title, members: members}).filter(([_, v]) => v != null));
+//       let newChat = await dispatch(actionUpsertChat(chat));
+//       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);
+}
+
+

+ 36 - 0
src/actions/actionsForUser.jsx

@@ -0,0 +1,36 @@
+import { gql } from "../helpers/gql";
+import { actionAboutMe } from "./actionAboutMe";
+import { actionLeftChat, actionUpdateChat } from "./actionsForChats";
+import { actionPromise } from "./actionsPromise";
+
+export const actionUpdateUser = (user) => 
+    actionPromise('updateUser', gql(`mutation leaveChatByUser($user: UserInput) {
+        UserUpsert(user: $user) {
+          login
+          chats {
+            title
+          }
+        }
+      }`, {user: user}));
+
+export const actionUpsertUser = (login, nick, password, chatId) => 
+    async (dispatch, getState) => {
+        const state = getState();
+        const currUserId = state.auth?.payload?.sub?.id;
+        //remove chat from chats array
+        const chats = chatId ? Object.values(state.chats).filter(chat => chat._id !== chatId).map(chat => {
+                    let {_id} = chat;
+                    //return only id
+                    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,
+                // because userUpsert doesn't work from socket
+                (dispatch(actionLeftChat(state.chats[chatId])) &&  dispatch(actionUpdateChat({_id: chatId, title: state.chats[chatId].title}))) : 
+                dispatch(actionAboutMe());
+    }

+ 64 - 0
src/actions/actionsMedia.jsx

@@ -0,0 +1,64 @@
+import { history } from "../App";
+import { gql } from "../helpers/gql";
+import { uploadFile } from "../helpers/uploadFile";
+import { actionAboutMe } from "./actionAboutMe";
+import { actionUpdateChat } from "./actionsForChats";
+import { actionPromise } from "./actionsPromise";
+
+const actionAddMedia = (typeId, mediaId, type) => 
+actionPromise('newMedia', gql(`mutation setMedia($media: MediaInput) {
+  MediaUpsert(media: $media) {
+    _id
+    
+  }
+}`, {media: {_id: mediaId, [type]: {_id: typeId}}}))
+
+
+export const actionUploadFile = (file) => 
+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;
+      let media =  await dispatch(actionUploadFile(file));
+      console.log(media)
+      if (media._id){
+         let dataId = await dispatch(actionAddMedia(userId, media._id, "userAvatar"))
+         if (dataId){
+             dispatch(actionAboutMe())
+         }
+      }
+  }
+
+export const actionSetChatAvatar = (file) => 
+  async (dispatch, getState) => {
+    const [,route, chatId] = history.location.pathname.split('/');
+    const chat = getState().chats[chatId];
+    
+    const media = await dispatch(actionUploadFile(file));
+    console.log(media)
+    if(media._id){
+      const dataId = await dispatch(actionAddMedia(chatId, 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}));
+        
+      }
+      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}))

+ 165 - 0
src/actions/actionsMessages.jsx

@@ -0,0 +1,165 @@
+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";
+
+export const actionAddMessages = (data, id) => ({type: 'MSG', data, id});
+const actionAddMessage = (message, id) => ({type: 'MSG', data: [message], id});
+
+const actionGetOneMessage = (_id) => 
+	actionPromise('oneMessage', gql(`query getOneMes($mesId: String){
+		MessageFindOne(query: $mesId){
+			_id
+			text
+			createdAt
+			chat{
+				_id
+			}
+			media {
+				url
+				_id
+				type
+				originalFileName
+			}
+			replyTo{
+				_id
+				text 
+				media {
+					url
+					_id
+					type
+				}
+				owner {
+					nick
+				}
+			}
+			replies{
+				_id text 
+				chat{
+					_id
+				}
+			}
+			forwarded{
+				_id
+				text
+				owner {
+					nick
+				  }
+				  media {
+					url
+					_id
+					type
+				  }
+			}
+			forwardWith{
+				_id
+				text
+				chat{
+					_id
+				}
+			}
+			owner {
+				_id
+				nick
+				avatar {
+				  url
+				}
+			}
+		}
+	}`, {mesId: JSON.stringify([{_id}])}))
+
+export const actionUpsertMSG = (message) => 
+	actionPromise('sendMSG', gql(`mutation MessageUpsert($message: MessageInput) {
+		MessageUpsert(message: $message) {
+		  _id
+		  createdAt
+		  text
+		  media {
+			url
+			_id
+			type
+		  }
+		  replyTo{
+			  _id
+		  }
+		  forwarded{
+			  _id
+		  }
+		  owner {
+			nick
+			avatar {
+			  url
+			}
+		  }
+		  chat{
+			_id
+		  }
+		}
+	  }`, {
+		  message 
+	  }))
+
+export const actionSendMessage = (messageId, chatId, text, media, replyTo, forwarded) => 
+	async (dispatch, getState) => {
+		let newMedia = null
+		if(media){
+			let checkedMedia = media.filter(item => !item._id && item)
+			console.log(media, checkedMedia)
+			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) => 
+	async (dispatch, getState) => {
+		let message = await dispatch(actionGetOneMessage(msg._id));
+		dispatch(actionAddMessage(message, message.chat._id));
+		dispatch(actionGetOneChat(msg.chat._id));
+		////////check all replies and forwardWith (if message has them to send changes)
+		const {replies, forwardWith} = message;
+		const state = getState();
+		// const [,route, histId] = history.location.pathname.split('/');
+		if(replies){
+			replies.forEach(async ({_id, chat}) => {
+				if(state.chats[chat._id].messages?.hasOwnProperty(_id)){
+					let mes = await dispatch(actionGetOneMessage(_id))
+					dispatch(actionAddMessage(mes, chat._id));
+				}
+				
+			})
+		}
+		if(forwardWith){
+			forwardWith.forEach(async ({_id, chat}) => {
+				if(state.chats[chat._id].messages?.hasOwnProperty(_id)){
+					let mes = await dispatch(actionGetOneMessage(_id))
+					dispatch(actionAddMessage(mes, chat._id));
+				}
+				
+			})
+		}
+	}
+
+export const actionEditMessage = (chatId, message) => 
+	(dispatch, getState) => {
+		dispatch(actionSetMessageEditor(chatId, {message: message, value: message.text}))
+		if (message.media){
+			let files = addUploadDate([...message.media])
+			console.log(files)
+			dispatch(actionSetDropMedia(chatId, files))
+			dispatch(actionOpenModal('messageEditorMediaModal'))
+		}
+	}

+ 0 - 1
src/actions/actionsPromise.jsx

@@ -8,7 +8,6 @@ export const actionPromise = (name, promise) =>
 	try {
 		let payload = await promise;
 		dispatch(actionFulfilled(name, payload));
-		
 		return payload
 	}
 	catch(error){

+ 0 - 37
src/components/AppBar.jsx

@@ -1,37 +0,0 @@
-import * as React from 'react';
-import { styled, alpha } from '@mui/material/styles';
-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 Typography from '@mui/material/Typography';
-import InputBase from '@mui/material/InputBase';
-import MenuIcon from '@mui/icons-material/Menu';
-import SearchIcon from '@mui/icons-material/Search';
-
-
-
-
-
-export default function SearchAppBar({openUserMenu}) {
-  return (
-    <Box sx={{ flexGrow: 1 }}>
-      <AppBar position="static">
-        <Toolbar>
-          <IconButton
-            onClick={openUserMenu}
-            size="large"
-            edge="start"
-            color="inherit"
-            aria-label="open drawer"
-            sx={{ mr: 2 }}
-          >
-            <MenuIcon />
-          </IconButton>
-          
-          
-        </Toolbar>
-      </AppBar>
-    </Box>
-  );
-}

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

@@ -0,0 +1,36 @@
+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>
+          
+          
+          
+     
+  );
+}

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

@@ -0,0 +1,28 @@
+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;
+`;

+ 19 - 0
src/components/Aside/Aside.jsx

@@ -0,0 +1,19 @@
+import { useState } from "react";
+import SearchAppBar from "../AppBar/AppBar";
+import ChatList from "../ChatList/ChatList";
+import UserMenu from "../Drawer/UserMenu";
+
+import { AsideWrap } from "./Aside.style";
+
+
+export const Aside = () => {
+	const [isOpen, setOpen] = useState(false);
+	
+	return (
+		<AsideWrap>
+			<SearchAppBar openUserMenu={() => setOpen(true)}/>
+			<ChatList/>
+			<UserMenu open={isOpen} closeUserMenu={() => setOpen(false)}/>
+		</AsideWrap>
+	)
+}

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

@@ -0,0 +1,10 @@
+import styled from "styled-components";
+
+export const AsideWrap = styled.aside`
+    position: relative;
+    z-index: 2;
+    width: 100%;
+    height: 100vh;
+    // box-shadow: 0 0 10px -1px #BDBDBD;
+    border-right: 1px solid #e9e9e9;
+`;

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

@@ -0,0 +1,38 @@
+import { Button, TextField } from "@mui/material"
+import { useState } from "react"
+import { connect } from "react-redux"
+import { actionUpsertChat } from "../../actions/actionsForChats"
+import { history } from "../../App"
+import { backendURL } from "../../helpers/gql"
+import DropZoneAvatar from "../DropZone/DropZoneAvatar"
+import { ChatEditorModalWrapper } from "./ChatEditorModal.style"
+
+const ChatEditorModal = ({handleClose, chats, updateChat}) => {
+    const [,route, histId] = history.location.pathname.split('/');
+    const [chatNameValue, setChatNameValue] = useState(chats[histId].title);
+    const [avatar, setAvatar] = useState();
+    
+    return (
+        <ChatEditorModalWrapper>
+            <div>
+				<DropZoneAvatar component={"dropAvatarComponent"} onLoad={setAvatar} url={avatar ? URL.createObjectURL(avatar) : `${backendURL}/${chats[histId]?.avatar?.url || ''}`}/>
+				<TextField
+                    autoFocus
+					sx={{width: '100%'}}
+					id="standard-basic" 
+					label="Chat Name" 
+					variant="standard" 
+					color='primary'
+					value={chatNameValue}
+					onChange={(e) =>setChatNameValue(e.target.value)}
+				/>
+			</div>
+			<div>
+				<Button variant='text' onClick={handleClose}>Cancel</Button>
+				<Button variant='text' disabled={chatNameValue ? false : true} onClick={() => {updateChat(histId, chatNameValue, null, avatar);handleClose()}}>Save</Button>
+			</div>
+        </ChatEditorModalWrapper>
+    )
+}
+
+export default connect(state => ({chats: state?.chats || []}), {updateChat: actionUpsertChat})(ChatEditorModal);

+ 14 - 0
src/components/ChatEditorModal/ChatEditorModal.style.js

@@ -0,0 +1,14 @@
+import styled from "styled-components";
+
+export const ChatEditorModalWrapper = styled.div`
+    padding: 10px;
+    &>div:nth-child(1){
+		display: flex;
+		justify-content: space-between;
+		margin: 20px 10px 10px;
+	}
+	&>div:nth-child(2){
+		display: flex;
+		justify-content: end;
+	}
+`;

+ 91 - 0
src/components/ChatInfoModal/ChatInfoModal.jsx

@@ -0,0 +1,91 @@
+import { Avatar, Button, IconButton } from "@mui/material";
+import { connect } from "react-redux";
+import { history } from "../../App";
+import { backendURL } from "../../helpers/gql";
+import { MemberList } from "../MemberList/MemberList";
+import { ChatInfoModalHeader, ChatInfoModalWrapper, ChatInfoModalName, ChatInfoTitle, ChatInfoModalMain, Divider, MemberListHeader, MemberAmount, ChatInfoModalAvatar, Drop } from "./ChatInfoModal.style";
+import CloseIcon from "./icons8-close.svg";
+import MembersIcon from "./user.png";
+import AddUserIcon from "./add-user.png";
+import { useEffect, useState } from "react";
+import { SearchUserForChat } from "../SearchUserForChat/SearchUserForChat";
+import {actionUpsertChat } from "../../actions/actionsForChats";
+import DropZoneAvatar from "../DropZone/DropZoneAvatar";
+import { actionSetChatAvatar } from "../../actions/actionsMedia";
+import  ChatInfoOptions  from "../ChatInfoOptions/ChatInfoOptions";
+
+const CDropZoneAvatarForChat = connect(null, {onLoad: actionSetChatAvatar})(DropZoneAvatar);
+
+const ChatInfoModal = ({handleClose, chats, userId, updateChat}) => {
+    const [,route, histId] = history.location.pathname.split('/');
+    useEffect(() => {
+        chats[histId] || handleClose()
+    }, [chats[histId]])
+    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)
+    const currMembers = chats[histId]?.members?.map(item => {
+        let {_id} = item; 
+        return {_id}
+    });
+    
+
+    return(!isOpenSearch) ? 
+        <ChatInfoModalWrapper>
+            <ChatInfoModalHeader>
+                <ChatInfoModalName>Chat Info</ChatInfoModalName>
+                <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"
+                        sx={{ width: 75, height: 75}}
+                    />
+                    {isOwner && <CDropZoneAvatarForChat component={"dropAvatarPopupComponent"}/>}
+                </ChatInfoModalAvatar>
+                
+                <ChatInfoTitle>{chats[histId]?.title}</ChatInfoTitle>
+            </ChatInfoModalMain>
+            <Divider/>
+            <MemberListHeader>
+                <img src={MembersIcon} style={{height: "20px", marginRight: "30px"}}/>
+                <MemberAmount>{amount} {amount === 1 ? 'member' : 'members'}</MemberAmount>
+
+                {isOwner && <img src={AddUserIcon} style={{height: "20px", cursor: "pointer"}} onClick={() => setIsOpenSearch(true)}/>}
+            </MemberListHeader>
+            <MemberList members={chats[histId]?.members}/>
+        </ChatInfoModalWrapper>
+     : 
+     <>
+        <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(histId, null, users); handleClose()}}>Add</Button>
+        </div>    
+
+     </>
+     
+     
+}
+
+export default connect(state => ({
+    chats: state?.chats || [], 
+    userId: state?.auth?.payload?.sub?.id
+}), {updateChat: actionUpsertChat})(ChatInfoModal);

+ 76 - 0
src/components/ChatInfoModal/ChatInfoModal.style.js

@@ -0,0 +1,76 @@
+import styled from "styled-components";
+
+export const ChatInfoModalWrapper = styled.div`
+    width: 100%;
+    min-height: 400px;
+`;
+
+export const ChatInfoModalHeader = styled.div`
+    display: flex;
+    justify-content: space-between;
+    padding: 18px 16px 20px;
+    background-color: #fff;
+    border-radius: 10px;
+    align-items: center;
+`;
+
+export const ChatInfoModalName = styled.h3`
+    margin: 0;
+    font-weight: 500;
+    font-size: 18px;
+    padding-left: 10px;
+`;
+
+export const ChatInfoModalMain = styled.div`
+    display: flex;
+    align-items: center;
+    padding: 0 20px 10px;
+    background-color: #fff;
+    
+    
+`;
+
+export const ChatInfoTitle = styled.div`
+    font-size: 18px;
+    font-weight: 500;
+`;
+
+export const Divider = styled.div`
+    height: 12px;
+    background-color: #f3f3f3;
+    border-top: 1px solid #e1e1e1;
+    border-bottom: 1px solid #ececec;
+    margin-bottom: 8px;
+    margin-right: 1px;
+`;
+
+export const MemberListHeader = styled.div`
+    display: flex;
+    justify-content: left;
+    padding: 10px 30px;
+`;
+
+export const MemberAmount = styled.div`
+    font-size: 15px;
+    text-transform: uppercase;
+    font-weight: 500;
+    margin-right: 137px;
+    
+`;
+
+export const ChatInfoModalAvatar = styled.div`
+    margin-right: 30px;
+    width: 75px;
+    height: 75px;
+    overflow: hidden;
+    border-radius: 50%;
+    &:hover section{
+        transform: translateY(-30px);
+        transition: transform 0.5s ease-out;
+    }
+    section{
+        transform: translateY(0px);
+        transition: transform 0.5s ease-out;
+    }
+`;
+

BIN
src/components/ChatInfoModal/add-user.png


+ 1 - 0
src/components/ChatInfoModal/icons8-close.svg

@@ -0,0 +1 @@
+<svg fill="#a1a1a1" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 24 24" width="20px" height="20px"><path d="M 4.7070312 3.2929688 L 3.2929688 4.7070312 L 10.585938 12 L 3.2929688 19.292969 L 4.7070312 20.707031 L 12 13.414062 L 19.292969 20.707031 L 20.707031 19.292969 L 13.414062 12 L 20.707031 4.7070312 L 19.292969 3.2929688 L 12 10.585938 L 4.7070312 3.2929688 z"/></svg>

BIN
src/components/ChatInfoModal/user.png


+ 74 - 0
src/components/ChatInfoOptions/ChatInfoOptions.jsx

@@ -0,0 +1,74 @@
+import { Divider, IconButton, Menu, MenuItem } from "@mui/material";
+import MoreVertIcon from '@mui/icons-material/MoreVert';
+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";
+
+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) => {
+      setAnchorEl(event.currentTarget);
+    };
+    const handleClose = () => {
+      setAnchorEl(null);
+    };
+
+    return(
+        <div>
+            <IconButton onClick={handleClick}>
+                <MoreVertIcon />
+            </IconButton>
+            <Menu 
+                id="demo-positioned-menu"
+                aria-labelledby="demo-positioned-button"
+                anchorEl={anchorEl}
+                open={open}
+                onClose={handleClose}
+                anchorOrigin={{
+                    vertical: 'bottom',
+                    horizontal: 'center',
+                    }} 
+                transformOrigin={{
+                    vertical: 'top',
+                    horizontal: 'center',
+                }}
+            > 
+
+            {isOwner && <div>
+                <MenuItem onClick={() => {handleClose(); setOpen('chatEditorModal') }} disableRipple>
+                    <EditIcon sx={{mr: "20px"}}/>
+                    Edit
+                </MenuItem>
+                <MenuItem onClick={() => {handleClose(); setIsOpenSearch(true)}} disableRipple>
+                    <img src={AddUserIcon} style={ImgStyle}/>
+                    Add members
+                </MenuItem>
+                <MenuItem onClick={() => {handleClose(); deleteChat({_id: chatId, members: []})}} disableRipple>
+                    <img src={Basket} style={ImgStyle}/>
+                    Delete chat
+                </MenuItem>
+                <Divider sx={{margin: "0px"}}/>
+            </div>}
+                  
+                <MenuItem onClick={() => {handleClose(); leaveChat(null, null, null, chatId)}} disableRipple>
+                    <img src={Exit} alt="exit" style={ImgStyle}/>
+                    Leave chat
+                </MenuItem>
+            </Menu>
+        </div>
+    )
+}
+
+export default connect(null, {setOpen: actionOpenModal, leaveChat: actionUpsertUser, deleteChat: actionUpdateChat})(ChatInfoOptions);

File diff suppressed because it is too large
+ 1 - 0
src/components/ChatInfoOptions/icons8_basket.svg


BIN
src/components/ChatInfoOptions/icons8_exit.png


+ 32 - 0
src/components/ChatList/ChatList.jsx

@@ -0,0 +1,32 @@
+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}) => {
+	const [, route, histId] = history.location.pathname.split('/');
+	const [stateId, setStateId] = useState(histId);
+	
+	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} />)}
+				</List>
+			</SimpleBar>
+		</ChatListWrapper>
+		
+	)
+}
+
+export default connect(state => ({chats: state.chats || []}))(ChatList);

+ 6 - 0
src/components/ChatList/ChatList.style.js

@@ -0,0 +1,6 @@
+import styled from "styled-components";
+
+export const ChatListWrapper = styled.div`
+    height: calc(100% - 54px);
+    overflow: hidden;
+`;

+ 49 - 0
src/components/ChatListItem/ChatListItem.jsx

@@ -0,0 +1,49 @@
+import { Avatar, ListItem } from "@mui/material";
+import { useEffect } from "react";
+import { useContext, useState } from "react";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+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 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)}>
+            
+                <ChatListItemLink to={`/main/${chat._id}`}>
+                    <Avatar
+                        alt={chat.title}
+                        src={`${backendURL}/${chat.avatar?.url || ''}`}
+                        sx={{ width: 45, height: 45, mr: '20px'}}
+                    />
+                    <ChatListItemContent>
+                        <ChatListItemHeader>
+                            <ChatListItemTitle isActive={isActive}>{chat.title}
+                                {currUserId === chat?.owner?._id && <img src={isActive ? OnwerStarActive : OwnerStar} alt="ownerStar"/>}
+                            </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> */}
+                        </ChatListItemFooter>
+                        
+
+                    </ChatListItemContent>
+                </ChatListItemLink> 
+            
+            </ChatListItemWrapper>
+    )
+}
+
+export default connect(state => ({currUserId: state.auth?.payload?.sub?.id}))(ChatListItem);

+ 71 - 0
src/components/ChatListItem/ChatListItem.style.js

@@ -0,0 +1,71 @@
+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"};
+    height: 62px;
+    width: 100%;
+    &:hover{
+        background-color: ${props => props.isActive ? "#00a6dd" : "#f3f3f3"};
+    }
+`;
+
+export const ChatListItemLink = styled(Link)`
+    display: flex;
+    text-decoration: none;
+    padding: 8px 13px 8px 8px;
+`;
+
+export const ChatListItemContent = styled.div`
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    width: 100%;
+    overflow: hidden;
+`;
+
+export const ChatListItemHeader = styled.div`
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+`;
+
+export const ChatListItemTitle = styled.h3`
+    margin: 0; 
+    font-weight: 500;
+    font-size: 14px;
+    letter-spacing: 0.3px;
+    line-height: 1.5;
+    text-align: left;
+    color:${(props) => props.isActive ? "#fff" : "#000"} ;
+    img{
+        height: 12px;
+        margin-left: 5px;
+    }
+`;
+
+export const ChatListItemFooter = styled.div`
+    display: flex;
+    justify-content: space-between;
+    height: 13px;
+    line-height: 0.5;
+    width: 100%;
+`;
+
+export const ChatListLastMessage = styled.div`
+    color: ${props => props.isActive ? "#fff" : "#a1a1a1"};
+    font-size: 13px;
+    line-height: 1;
+    overflow: hidden !important; 
+    text-overflow: ellipsis;
+    span{
+        color: ${props => props.isActive ? "#fff" : "#00a6dd"};
+        margin-right: 5px;
+        
+    }
+`;
+

BIN
src/components/ChatListItem/icons8_star.png


BIN
src/components/ChatListItem/icons8_star_white.png


+ 35 - 0
src/components/ChatPageData/ChatPageData.jsx

@@ -0,0 +1,35 @@
+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 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)
+	}, [_id])
+
+	return (
+		<ChatPageWrapper>
+			<ChatPageHeader chatId={_id}/>
+			
+			<ChatPageMain>
+				<MessagesArea chatId={_id}/>
+				<InputArea chatId={_id}/>
+			</ChatPageMain>
+			
+		</ChatPageWrapper>
+	)
+}
+
+export default connect(null, {getData: actionGetMessageForChat})(ChatPageData);

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

@@ -0,0 +1,17 @@
+import styled from "styled-components";
+
+export const ChatPageWrapper = styled.div`
+    width: 100%;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+    
+`; 
+
+export const ChatPageMain = styled.div`
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-end;
+    backgraund-color: azure;
+    height: calc(100vh - 54px);
+`;

+ 26 - 0
src/components/ChatPageHeader/ChatPageHeader.jsx

@@ -0,0 +1,26 @@
+import { connect } from "react-redux"
+import { actionRemoveChat } from "../../actions/actionsForChats";
+import { actionIsOpen, actionOpenModal } from "../../reducers/modalReducer";
+
+import { ChatListItemTitle } from "../ChatListItem/ChatListItem.style"
+import { ChatMembersAmount, ChatPageHeaderWrap } from "./ChatPageHeader.style"
+
+
+
+
+const ChatPageHeader = ({ chats, chatId, onclick, openModal}) => {
+
+    let amount = chats[chatId]?.members?.length;
+
+    return (
+        <ChatPageHeaderWrap>
+            <div style={{cursor: "pointer"}} onClick={() => openModal("chatInfoModal")}>
+                <ChatListItemTitle>{chats[chatId]?.title}</ChatListItemTitle>
+                <ChatMembersAmount>{amount} {amount === 1 ? 'member' : 'members'}</ChatMembersAmount>
+            </div>
+            <div onClick={() => onclick(chatId)}>leave chat</div>
+        </ChatPageHeaderWrap>
+    )
+}
+
+export default connect(state => ({chats : state?.chats || []}), {onclick: actionRemoveChat, openModal: actionOpenModal})(ChatPageHeader);

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

@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+export const ChatPageHeaderWrap = styled.div`
+    box-sizing: border-box;
+    position: relative;
+    z-index: 2;
+    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`
+    position: relative;
+    color: #a1a1a1;
+    font-size: 13px;
+`;

+ 71 - 0
src/components/CreateNewChat.jsx

@@ -0,0 +1,71 @@
+
+
+import { Button, Paper, 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 { useEffect, useState } from 'react';
+import {actionUpsertChat } from '../actions/actionsForChats';
+
+export const NewChatModal = ({url, handleClose, handleSetAvatar, inputValue, setChatNameValue, setShowNextModal}) => {
+
+	useEffect(() =>{
+		console.log(url)
+	}, [url])
+	return(
+		<div className='MainChatModal'>
+			<div>
+				<DropZoneAvatar component={"dropAvatarComponent"} onLoad={handleSetAvatar} url={url}/>
+				<TextField
+					sx={{width: '100%'}}
+					id="standard-basic" 
+					label="Chat Name" 
+					variant="standard" 
+					color='primary'
+					value={inputValue}
+					onChange={(e) =>setChatNameValue(e.target.value)}
+				/>
+			</div>
+			<div>
+				<Button variant='text' onClick={handleClose}>Cancel</Button>
+				<Button variant='text' disabled={inputValue ? false : true} onClick={()=> setShowNextModal(true)}>Next</Button>
+			</div>
+		</div>
+	)
+} 
+
+function CreateNewChat({createChat, handleClose}) {
+   
+    const [avatar, setAvatar] = useState('');
+	const [chatNameValue, setChatNameValue] = useState('');
+	const [showNextModal, setShowNextModal] = useState(false);
+	const [users, setUsers] = useState();
+
+	return (!showNextModal) ?  
+		<NewChatModal
+		handleSetAvatar={setAvatar}
+		url={avatar && URL.createObjectURL(avatar)}
+		handleClose={handleClose}
+		inputValue={chatNameValue}
+		setChatNameValue={setChatNameValue}
+		setShowNextModal={setShowNextModal}
+		/>
+	 	:
+		<>
+			<SearchUserForChat getUsers={setUsers} />
+			<div style={{display: "flex",
+					padding: "10px 15px",
+					justifyContent: "right",
+					alignItems: "center"}}>
+				<Button variant="text" onClick={handleClose} >Cancel</Button>
+				<Button variant="text" onClick={() => {createChat(null, chatNameValue, users, avatar); handleClose()}}>Create</Button>
+			</div>
+		</>
+		
+}
+
+export default connect(null, {createChat: actionUpsertChat})(CreateNewChat);
+

+ 63 - 0
src/components/Drawer/UserMenu.jsx

@@ -0,0 +1,63 @@
+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 { connect } from 'react-redux';
+import { Box } from '@mui/system';
+import { backendURL, gql } from '../../helpers/gql';
+import { actionPromise } from '../../actions/actionsPromise';
+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 { 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';
+
+// const actionUploadFiles = (files) => 
+// actionPromise('filesUpload', Promise.all(files.map((file) => uploadFile(file))))
+
+const UserMenu = ({open, setOpen, closeUserMenu, userInfo: {nick, avatar} }) => {
+	
+	return (
+		<Drawer 
+			anchor='left'
+			open={open}
+			onClose={closeUserMenu}
+		>
+			<div style={{width: "300px"}}> 
+				<UserMenuHeader>
+					<Avatar
+						src={`${backendURL}/${avatar?.url || ''}`}
+						alt={nick}
+						sx={{width: 60, height: 60}}
+					/>
+					<UserMenuTitle>
+						{nick}
+					</UserMenuTitle>
+				</UserMenuHeader>
+				<Divider/>
+				
+				<List>
+					<ListItemWrap onClick={() => setOpen('createChatModal')}>
+						<img src={ChatIcon} alt="chatIcon" style={{height: "22px"}}/> <StyledSpan>New chat</StyledSpan>
+					</ListItemWrap>
+					<ListItemWrap onClick={() => setOpen('userEditorModal')}>
+						<EditIcon/> <StyledSpan>Edit profile</StyledSpan>
+					</ListItemWrap>
+					<ListItemWrap onClick={() => store.dispatch(actionAuthLogout())}>
+						<img src={Exit} alt="chatIcon" style={{height: "22px"}}/> <StyledSpan>Log out</StyledSpan>
+					</ListItemWrap>
+				</List>
+			</div> 
+		</Drawer>
+	)
+}
+
+export default connect(state => ({userInfo: state.promise?.aboutMe?.payload || {}}), {setOpen : actionOpenModal})(UserMenu)

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

@@ -0,0 +1,26 @@
+import { ListItem } from "@mui/material";
+import styled from "styled-components";
+
+export const UserMenuHeader = styled.div`
+    padding: 25px;
+`;
+
+export const UserMenuTitle = styled.h3`
+    margin: 0;
+    margin-top: 15px;
+    font-weight: 500;
+`;
+
+export const StyledSpan = styled.span`
+    margin-left: 16px;
+    font-weight: 500;
+`;
+
+export const ListItemWrap = styled(ListItem)`
+    padding: 0;
+    height: 40px;
+    padding-left: 25px;
+    &:hover{
+        background-color: #f3f3f3;
+    }
+`;

BIN
src/components/Drawer/icons8-chat.png


+ 37 - 0
src/components/DropMediaFilesOptions/DropMediaFilesOptions.jsx

@@ -0,0 +1,37 @@
+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 { DropMediaFilesOptionsWrap } from "./DropMediaFilesOptions.style"
+import ArrowsIcon from './icons8-arrows.png';
+import Basket from './icons8_basket.svg';
+
+const DropMediaFilesOptions = ({deleteFile, mediaKey, changeFile, chatId}) => {
+
+    const {acceptedFiles, getRootProps, getInputProps} = useDropzone({
+        noDrag: true, 
+        multiple: false
+    });
+
+    useEffect(()=>{
+        let files = addUploadDate(acceptedFiles)
+        acceptedFiles[0] && changeFile(chatId, files, mediaKey)
+    }, [acceptedFiles])
+
+    return (
+        <DropMediaFilesOptionsWrap>
+            
+                <div style={{height: '19px'}}
+                {...getRootProps({className: 'dropzone'})}>
+                <input {...getInputProps()} />
+                    <img src={ArrowsIcon}/>
+                </div>
+            
+            
+            <img src={Basket} onClick={() => deleteFile(chatId, mediaKey)}/>
+        </DropMediaFilesOptionsWrap>
+    )
+}
+
+export default connect(null, {deleteFile: actionDeleteOneMediaFile, changeFile: actionChangeFile})(DropMediaFilesOptions)

+ 18 - 0
src/components/DropMediaFilesOptions/DropMediaFilesOptions.style.js

@@ -0,0 +1,18 @@
+import styled from "styled-components";
+
+export const DropMediaFilesOptionsWrap = styled.div`
+    width: 50px; 
+    height: 30px;
+    background-color: rgba(0, 0, 0, .2);
+    border-radius: 5px;
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    display: flex;
+    justify-content: space-evenly;
+    align-items: center;
+    img{
+        height: 16px;
+        cursor: pointer;
+    }
+`;

BIN
src/components/DropMediaFilesOptions/icons8-arrows.png


File diff suppressed because it is too large
+ 1 - 0
src/components/DropMediaFilesOptions/icons8_basket.svg


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

@@ -0,0 +1,63 @@
+import { useRef, useState } from "react";
+import { DropAudioPlayer, DropAudioTitle, DropAudioWrap, DropImgWrap, DropVideoWrap, 
+    DropFileWrap, DropFileIcon, DropFileTitle, DropFileIconWrap, DropFileHeader, DropFileInfo, DropFileSize
+ } from "./DropMediaFiles.style"
+import FileIcon from './icons8-File.svg';
+import MusicIcon from './icons8-music.png';
+
+
+
+export const DropImg = ({url}) => {
+    const image = useRef(null);
+
+    return (
+        <DropImgWrap src={url} ref={image}/>
+    )
+}
+
+export const DropVideo = ({url}) => {
+    return (
+        <DropVideoWrap src={url} controls/>
+    )
+}
+
+export const DropAudio = ({url, name, size}) => {
+
+    const [isOpen, setIsOpen] = useState(false);
+
+    const handleOpen = () => setIsOpen(!isOpen);
+    
+
+    return (
+        <DropAudioWrap>
+            <DropFileWrap>
+                <DropFileIconWrap isAudio={true} onClick={handleOpen}>
+                    <DropFileIcon src={MusicIcon}/>
+                </DropFileIconWrap>
+                <DropFileInfo>
+                    <DropFileTitle>{name}</DropFileTitle>
+                    <DropFileSize>{size}</DropFileSize>
+                </DropFileInfo>
+            </DropFileWrap>
+
+            {isOpen && <DropAudioPlayer controls src={url}/>}
+        </DropAudioWrap>
+    )
+}
+
+export const DropFile = ({size, name}) => {
+    return(
+        <DropFileWrap>
+            <DropFileIconWrap>
+                <DropFileIcon src={FileIcon}/>
+            </DropFileIconWrap>
+            
+            <DropFileInfo>
+                <DropFileTitle>{name}</DropFileTitle>
+                <DropFileSize>{size}</DropFileSize>
+            </DropFileInfo>
+            
+        </DropFileWrap>
+    )
+}
+

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

@@ -0,0 +1,74 @@
+import styled from 'styled-components'
+/////////////image
+export const DropImgWrap = styled.img`
+    max-width: 100%; 
+    height: 80px;
+`;
+
+////////////////////video
+export const DropVideoWrap = styled.video`
+    // height: 200px;
+    width: 100%;
+`;
+
+//////////////////////////////////////audio
+export const DropAudioWrap = styled.div`
+    
+`;
+
+export const DropAudioPlayer = styled.audio`
+    height: 30px;
+    margin-top: 10px;
+    width: 100%;
+`;
+
+export const DropAudioTitle = styled.div`
+
+`;
+
+export const DropFileHeader = styled.div`
+    display: flex;
+    justify-content: left;
+    align-items: center;
+`
+////////////////////////////////////////another files
+export const DropFileWrap = styled.div`
+    display: flex;
+    justify-content: left;
+    align-items: center;
+`;
+
+export const DropFileIconWrap = styled.div`
+    height: 40px;
+    width: 40px;
+    border-radius: 50%;
+    background-color: #3498DB;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 10px;
+    cursor: ${(props) => props.isAudio ? 'pointer' : 'auto'}
+`;
+
+export const DropFileIcon = styled.img`
+    height: 24px;
+`;
+
+export const DropFileInfo = styled.div`
+    
+`;
+
+export const DropFileTitle = styled.div`
+    font-size: 14px;
+    font-weight: 600;
+`;
+
+export const DropFileSize = styled.div`
+    color: #a1a1a1;
+    font-size: 14px;
+`;
+
+
+
+
+

+ 9 - 0
src/components/DropMediaItem/DropMediaFiles/icons8-File.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 30" width="20px" height="20px">
+<g id="surface5700508">
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 4.875 28.125 L 4.875 1.875 L 18.59375 1.875 L 25.125 8.40625 L 25.125 28.125 Z M 4.875 28.125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 18.441406 2.25 L 24.75 8.558594 L 24.75 27.75 L 5.25 27.75 L 5.25 2.25 L 18.441406 2.25 M 18.75 1.5 L 4.5 1.5 L 4.5 28.5 L 25.5 28.5 L 25.5 8.25 Z M 18.75 1.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.392157%,59.607846%,85.882354%);fill-opacity:1;" d="M 18.375 8.625 L 18.375 1.875 L 18.59375 1.875 L 25.125 8.40625 L 25.125 8.625 Z M 18.375 8.625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 18.75 2.558594 L 24.441406 8.25 L 18.75 8.25 L 18.75 2.558594 M 18.75 1.5 L 18 1.5 L 18 9 L 25.5 9 L 25.5 8.25 Z M 18.75 1.5 "/>
+</g>
+</svg>

BIN
src/components/DropMediaItem/DropMediaFiles/icons8-music.png


+ 47 - 0
src/components/DropMediaItem/DropMediaItem.jsx

@@ -0,0 +1,47 @@
+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 { 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('');
+    const [size, setSize] = useState('')
+
+    useEffect(() => {
+        setUrl(mediaItem.url ? `${backendURL}/${mediaItem.url || ''}` : URL.createObjectURL(mediaItem))
+        setName((mediaItem?.name?.length || mediaItem?.originalFileName?.length) > 22 ? 
+                (mediaItem?.name || mediaItem?.originalFileName).slice(0,10)  + '...' + (mediaItem?.name || mediaItem?.originalFileName).slice(-10) : 
+                (mediaItem?.name || mediaItem?.originalFileName))
+        setSize(calculateFileSize(mediaItem?.size))
+    },[])
+    
+
+    const checkType = (type) => {
+        if(type.includes("image")){
+            return <DropImg url={url} />
+        } else if(type.includes("video")){
+            return <DropVideo url={url}/> 
+        } else if (type.includes("audio")){
+            return <DropAudio url={url} name={name} size={size}/>
+        } else {
+            return <DropFile name={name} url={url} size={size}/>
+        }
+    }
+    
+    
+    return(
+        <DropMediaItemWrap>
+            {checkType(mediaItem?.type)}
+            <DropMediaFilesOptions chatId={chatId} mediaKey={mediaItem.uploadDate}/>
+            
+        </DropMediaItemWrap>
+    )
+}
+
+export const MemoizedDropMediaItem = memo(DropMediaItem);

+ 7 - 0
src/components/DropMediaItem/DropMediaItem.style.js

@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+export const DropMediaItemWrap = styled.li`
+    width: 100%;
+    position: relative;
+    margin-top: 10px;
+`;

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

@@ -0,0 +1,76 @@
+import {useDropzone} from 'react-dropzone';
+import { useEffect} from 'react';
+import { connect } from 'react-redux';
+import { Avatar } from '@mui/material';
+import { backendURL } from '../../helpers/gql';
+import { actionSetUserAvatar } from '../../actions/actionsMedia';
+import Camera from './icons8_camera.png';
+import { Drop } from '../ChatInfoModal/ChatInfoModal.style';
+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>
+      <Avatar
+        src={url}
+        sx={{width: "75px", height: "75px"}}
+      /> 
+      <AvatarOverlay>
+        <img src={Camera}/>
+      </AvatarOverlay>
+    </AvatarWrap>
+  )
+}
+
+const DropAvatarPopupComponent = () => {
+  return(
+    <div style={{
+      width: "100%",
+      height: "30px",
+      backgroundColor: "grey",
+      opacity: "0.5",
+      display: "flex",
+      justifyContent: "center",
+      alignItem: "center", 
+      cursor: "pointer"
+    }}>
+      <img style={{height: "26px"}} src={Camera}/>
+    </div>
+  )
+}
+
+export default function DropZoneAvatar({onLoad, component, url }) {
+    const dropMap = {
+      dropAvatarComponent: <DropAvatarComponent url={url}/>,
+      dropAvatarPopupComponent: <DropAvatarPopupComponent/>,
+      dropAvatarBadge: <BadgeComponent><img src={Photo}/></BadgeComponent>
+    }
+
+    const {acceptedFiles, getRootProps, getInputProps} = useDropzone({
+      noDrag: true, 
+      multiple: false,
+      accept: 'image/*'
+    });
+    
+    useEffect(()=>{
+        console.log(acceptedFiles)
+        acceptedFiles[0] && onLoad(acceptedFiles[0])
+    }, [acceptedFiles])
+
+    return (
+      <section className="container" >
+          <div 
+           {...getRootProps({className: 'dropzone'})}>
+            <input {...getInputProps()} />
+            {dropMap[component]}
+          </div>
+      </section>
+    );
+  }
+  
+
+  

+ 30 - 0
src/components/DropZone/DropZoneAvatar.style.js

@@ -0,0 +1,30 @@
+import styled from "styled-components";
+
+export const AvatarWrap = styled.div`
+    overflow: hidden;
+    border-radius: 50%;
+    width: 75px;
+    height: 75px;
+    position: relative;
+    margin-right: 30px;
+    &:hover div:last-child {
+        z-index: 100;
+    }
+`;
+
+export const AvatarOverlay = styled.div`
+    background-color: grey;
+    width: 75px;
+    height: 75px;
+    opacity: 0.5;
+    display: flex;
+    justify-content: center;
+    align-items: center; 
+    cursor: pointer;
+    position: absolute;
+    top: 0px;
+    z-index: -1;
+    img{
+        height: 30px;
+    }
+`;

+ 37 - 0
src/components/DropZone/MessageDropZone/MessageDropZone.jsx

@@ -0,0 +1,37 @@
+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';
+
+function MessageDropZone({onLoad, openModal, url, chatId }) {
+    
+
+    const {acceptedFiles, getRootProps, getInputProps} = useDropzone({
+      noDrag: true, 
+      multiple: true,
+    });
+    useEffect(()=>{
+        acceptedFiles[0] && openModal('messageMediaModal');
+        let files = addUploadDate(acceptedFiles)
+        acceptedFiles[0] && onLoad(chatId, files)
+    }, [acceptedFiles])
+
+    return (
+      <section className="container" >
+          <div style={{height: '24px'}}
+           {...getRootProps({className: 'dropzone'})}>
+            <input {...getInputProps()} />
+            <img style={{
+                cursor: 'pointer',
+                height: '24px'
+            }} src={DropIcon}/>
+          </div>
+      </section>
+    );
+  }
+
+export default connect(null , {openModal: actionOpenModal, onLoad: actionSetDropMedia})(MessageDropZone)

BIN
src/components/DropZone/MessageDropZone/icons8-dropIcon.png


BIN
src/components/DropZone/icons8_camera.png


+ 13 - 0
src/components/ForwardedMessage/ForwardedMessage.jsx

@@ -0,0 +1,13 @@
+import { ForwardedMessageHeader, ForwardedMessageWrap } from "./ForwardedMessage.style"
+
+export const ForwardedMessage = ({message}) => {
+
+    return (
+        <ForwardedMessageWrap>
+            <ForwardedMessageHeader>
+                Forwareded FROm {message?.owner?.nick}
+            </ForwardedMessageHeader>
+
+        </ForwardedMessageWrap>
+    )
+}

+ 9 - 0
src/components/ForwardedMessage/ForwardedMessage.style.js

@@ -0,0 +1,9 @@
+import styled from "styled-components";
+
+export const ForwardedMessageWrap = styled.div`
+
+`;
+
+export const ForwardedMessageHeader = styled.div`
+
+`;

+ 60 - 0
src/components/InputArea/InputArea.jsx

@@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from "react"
+import { connect } from "react-redux"
+import { actionSendMessage, actionUpsertMSG } 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 ReplyMessage from "../MessageDraft/ReplyMessageDraft";
+import { MessageDraftBox } from "../MessageDraft/MessageDraftBox";
+import InputAreaMessageEditor from "./InputAreaMessageEditor";
+
+
+const InputArea = ({sendMessage, deleteDraftMessage, chatId, chats, setInputValue, modal: {content, isOpen}}) => {
+    const message = chats[chatId]?.draft?.mainInputValue?.message;
+    const inputValue = chats[chatId]?.draft?.mainInputValue?.value || '';
+    let replyMessageId = (message && typeof message !== 'object') ? message : null;
+    let forwardedMessageId = (message && typeof message === 'object') ? message._id : null;
+    console.log(forwardedMessageId)
+
+	return (chats[chatId]?.draft?.messageEditor && !chats[chatId]?.draft?.messageEditor?.message?.media) ? 
+        <InputAreaMessageEditor chatId={chatId}/> 
+        :
+		<InputAreaWrapper>
+            <MessageDraftBox chat={chats[chatId]}>
+                
+            </MessageDraftBox>
+            
+            <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}
+                    placeholder="Write a message..." 
+                />
+                {inputValue ? 
+                <SendRoundedIcon
+                    style={{margin: '0 16px', cursor: "pointer"}}
+                    color="primary"
+                    // src={Plane} 
+                    onClick={
+                        async() => {
+                            let val = await sendMessage(null, chatId, inputValue.replace(/^\s+|\s+$/g, ''), null, replyMessageId, forwardedMessageId);
+                            (val?.replyTo?._id || val?.forwarded?._id) && deleteDraftMessage(chatId, null)
+                            val && setInputValue(chatId, "", 'mainInputValue')
+                        }
+                    }
+                /> : <div></div>}
+            </div>
+        </InputAreaWrapper>
+	
+}
+
+export default connect(state => ({chats: state.chats, modal: state?.modal}), {
+    sendMessage: actionSendMessage,
+    setInputValue: actionSetInputMessageValue,
+    deleteDraftMessage: actionAddDraftMessage
+})(InputArea);

+ 33 - 0
src/components/InputArea/InputArea.style.js

@@ -0,0 +1,33 @@
+import styled from "styled-components";
+import TextareaAutosize from 'react-textarea-autosize';
+
+export const InputAreaWrapper = styled.div`
+    width: 100%;
+    background-color: #fff;
+    border-top: 1px solid #e9e9e9;
+    
+    // display: flex;
+    // align-items: center;
+    img{
+        margin: 0 20px;
+        cursor: pointer;
+
+    }
+`;
+
+export const TextArea = styled(TextareaAutosize)`
+    display: block;
+    border: 0.1px solid #fff;
+    padding: 0;
+    width: 100%;
+    resize: none;
+    font-style: normal;
+    font-weight: 400;
+    font-size: 16px;
+    line-height: 22px;
+    height: 24px;
+    overflow: hidden;
+    outline: none;
+    
+`;
+

+ 55 - 0
src/components/InputArea/InputAreaMessageEditor.jsx

@@ -0,0 +1,55 @@
+import { useEffect, useRef, useState } from "react"
+import { connect } from "react-redux"
+import { actionSendMessage, actionUpsertMSG } 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 { MessageDraftBox } from "../MessageDraft/MessageDraftBox";
+import {MessageEditor} from "../MessageDraft/MessageEditor";
+
+const InputAreaMessageEditor = ({chats, chatId, deleteMessageEditor, sendMessage, setInputValue}) => {
+    
+    const inputValue = chats[chatId]?.draft?.messageEditor?.value || '';
+    const messageId = chats[chatId]?.draft?.messageEditor?.message._id;
+    const message = chats[chatId]?.draft?.messageEditor?.message;
+
+    return (
+        <InputAreaWrapper>
+            <MessageEditor deleteMessageEditor={deleteMessageEditor} chatId={chatId} message={message}/>
+            
+            <div style={{display: 'flex', alignItems: 'center', padding: '10px 0'}}>
+                <MessageDropZone chatId={chatId}/>
+                <TextArea
+                    value={inputValue}         
+                    onChange={e=>{setInputValue(chatId, e.target.value, 'messageEditor')}}
+                    maxRows={8}
+                    placeholder="Write a message..." 
+                />
+                {inputValue ? 
+                <SendRoundedIcon
+                    style={{margin: '0 16px', cursor: "pointer"}}
+                    color="primary"
+                    // src={Plane} 
+                    onClick={
+                        async() => {
+                            let val = await sendMessage(messageId, null, inputValue.replace(/^\s+|\s+$/g, ''), null, null);
+                            val && deleteMessageEditor(chatId, null);
+                            
+                        }
+                    }
+                /> : <div></div>}
+            </div>
+        </InputAreaWrapper>
+       )
+}
+
+export default connect(state => ({chats: state.chats}), {
+    sendMessage: actionSendMessage,
+    setInputValue: actionSetInputMessageValue,
+    deleteMessageEditor: actionSetMessageEditor
+    
+})(InputAreaMessageEditor);

BIN
src/components/InputArea/icons8-plane.png


+ 0 - 110
src/components/LoginForm.jsx

@@ -1,110 +0,0 @@
-import { actionFullLogin } from '../actions/actionLogin';
-
-
-import TextField from '@mui/material/TextField';
-import { 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 { Visibility, VisibilityOff } from '@mui/icons-material';
-import Button from '@mui/material/Button';
-import { connect } from 'react-redux';
-import { history } from '../App';
-
-
-
-
-
-function LoginForm({onLogin, }){
-
-    const [values, setValues] = useState({
-        login: '', 
-        password: '',
-        showPassword: false, 
-        error: false
-    })
-
-    const [errorLogin, setErrorLogin] = useState(false);
-    const [errorPass, setErrorPass] = useState(false);
-
-
-    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})
-        
-    }
-
-
-    const handleChange = (prop) => (event) => {
-        setValues({...values, [prop] : event.target.value})
-    }
-
-    const handleClickShowPassword = () => {
-        setValues({
-            ...values,
-            showPassword: !values.showPassword
-        })
-    }
-
-
-    return (
-        <form className='LoginForm' noValidate autoComplete='off'>
-            <Typography sx={{mb: '30px'}} variant='h4'>Вход</Typography>
-			<TextField 
-                error={errorLogin}
-                sx={{width: '100%', mb: '30px'}} 
-                required  
-                label="Логин" 
-                variant="outlined" 
-                helperText="минимум 3" 
-                value={values.login}
-                autoFocus
-                onChange={handleChange('login')}/>
-
-            <TextField
-                error={errorPass}
-                sx={{width: '100%'}}
-                helperText="минимум 3" 
-                required  
-                variant="outlined" 
-                label="Пароль"
-                type={values.showPassword ? 'text' : 'password'}
-                value={values.password}
-                onChange={handleChange('password')}
-                InputProps={{
-                    endAdornment: (
-                        <InputAdornment position="end">
-                            <IconButton
-                                aria-label="toggle password visibility"
-                                onClick={handleClickShowPassword}
-                                onMouseDown={(event) => event.preventDefault()}
-                                edge="end"
-                            >
-                                {values.showPassword ? <VisibilityOff/> : <Visibility/>}
-                            </IconButton>
-                        </InputAdornment>
-                    )
-                }}
-                
-            />
-
-            {/* {values.error && <div>неверный {errorLogin && <span>логин </span>}{errorPass && <span>пароль</span>}</div>} */}
-            <Button 
-            onClick={
-                (e) => {
-                    if (values.password.length >= 3 && values.login.length >= 3){
-                         onLogin(values.login, values.password);
-                    }
-                }
-            } 
-            sx={{width: '200px', mt: '30px'}} 
-            variant="contained">
-                Войти
-            </Button><br/>
-            <Link className='RegisterBtn' to='/register'>Зарегистрироваться</Link>
-		</form>
-    )
-}
-
-export default connect(null, {onLogin: actionFullLogin})(LoginForm)

+ 19 - 0
src/components/MemberList/MemberList.jsx

@@ -0,0 +1,19 @@
+import { Avatar } from "@mui/material"
+import { backendURL } from "../../helpers/gql"
+import { MemberItem, MemberListWrapper, MemberName } from "./MemberList.style"
+
+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>)}
+        </MemberListWrapper>
+    )
+}
+

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

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

+ 56 - 0
src/components/Message/Message.jsx

@@ -0,0 +1,56 @@
+import { useEffect, useRef, useState } from "react";
+import { Link } from "react-router-dom";
+import { backendURL } from "../../helpers/gql";
+import MessageOptions from "../MessageOptions/MessageOptions";
+import { ReplyMessage } from "../ReplyMessage/ReplyMessage";
+import { convert } from "../Time/Time";
+import { MessageContainer, MessageFooter, MessageOwner, MessageText, MessageWrapper, TimeMessage } from "./Message.style";
+import { saveAs } from "file-saver";
+import { MemoMessageMediaContainer, MessageMediaContainer } from "../MessageMediaContainer/MessageMediaContainer";
+import { ForwardedMessage } from "../ForwardedMessage/ForwardedMessage";
+
+
+
+
+export const Message = ({mes, chatId}) => {
+
+	const owner = true;
+  
+    const [anchorEl, setAnchorEl] = useState(null);
+    const open = Boolean(anchorEl);
+    const handleClick = (event) => {
+      setAnchorEl(event.currentTarget);
+    };
+    const handleClose = () => {
+      setAnchorEl(null)
+    };
+
+
+	return (mes.forwarded ? 
+		<div>
+			<MessageWrapper onClick={(e) => {handleClick(e)}}>
+				<MessageContainer>
+					<ForwardedMessage messsage={mes.forwarded}/> 
+				</MessageContainer>
+			</MessageWrapper>
+			<MessageOptions message={mes} chatId={chatId} handleClose={handleClose} open={open} anchorEl={anchorEl}/>
+		</div>
+	
+		: 
+        <div>
+        <MessageWrapper onClick={(e) => {handleClick(e)}}>
+				<MessageContainer>
+					{mes?.replyTo && <ReplyMessage owner={owner} message={mes.replyTo}/>}
+					{mes?.media && mes?.media?.length !==0 && <MemoMessageMediaContainer media={mes.media}/>}
+					<MessageText>{mes.text.replace(/ /g, "\u00a0")}</MessageText>
+					<MessageFooter>
+						<MessageOwner>{mes.owner.nick || 'nick'}</MessageOwner>
+						<TimeMessage owner={owner}>{convert(mes.createdAt).getTime()}</TimeMessage>
+					</MessageFooter>
+				</MessageContainer>
+        </MessageWrapper>
+            <MessageOptions message={mes} chatId={chatId} handleClose={handleClose} open={open} anchorEl={anchorEl}/>
+         </div>)
+			
+	
+}

+ 52 - 0
src/components/Message/Message.style.js

@@ -0,0 +1,52 @@
+import styled from "styled-components";
+
+export const MessageWrapper = styled.div`
+    display: flex;
+    margin:30px;
+`;
+
+export const MessageContainer = styled.div`
+color: #000;
+max-width: 400px;
+background-color: #E2DCC2;
+padding: 12px 16px 8px;
+position: relative;
+display: block;
+border-radius: 5px 5px 5px 0px;
+&::before{
+    content: '';
+    width: 15px;
+    height: 15px;
+    position: absolute;
+    background-color: #E2DCC2;
+    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%);
+};
+`;
+
+export const MessageText = styled.div`
+    width: 100%;
+    text-align: left;
+    word-wrap: break-word;
+    font-size: 14px;
+    line-height: 1.4;
+`;
+
+export const TimeMessage = styled.span`
+    color: #a1a1a1;
+    font-size: 13px;
+`; 
+
+export const MessageFooter = styled.div`
+    display: flex;
+    justify-content: space-between;
+    margin-top: 5px;
+`;
+
+export const MessageOwner = styled.div`
+    color: #a1a1a1;
+    font-size: 13px;
+    margin-right: 30px;
+`;
+

+ 43 - 0
src/components/MessageDraft/MessageDraft.style.js

@@ -0,0 +1,43 @@
+import styled from 'styled-components';
+
+export const MessageDraftWrapper = styled.div`
+    height: 50px;
+    display: flex;
+    justify-content: left;
+    align-items: center;
+    box-sizing: border-box;
+    width: 100%;
+    padding: 0 16px;
+`;
+
+export const MessageDraftContainer = styled.div`
+    display: flex;
+    justify-content: left;
+    padding: 0 20px;
+    width: 100%;
+    overflow: hidden;
+`;
+
+export const MessageDraftInfo = styled.div`
+    overflow: hidden;
+    margin-left: 10px;
+`;
+export const MessageOwnerDraftName = styled.div`
+    color: #00a6dd;
+    font-weight: 600;
+    font-size: 14px;
+    text-align: left;
+
+`;
+
+export const MessageDraftText = styled.div`
+    color: #a1a1a1;
+    font-size: 14px;
+    overflow: hidden !important; 
+    text-overflow: ellipsis;
+    text-align: left;
+`;
+
+export const MessageDraftBoxWrapper = styled.div`
+   
+`;

+ 16 - 0
src/components/MessageDraft/MessageDraftBox.jsx

@@ -0,0 +1,16 @@
+import { useEffect, useState } from "react";
+import { MessageDraftBoxWrapper } from "./MessageDraft.style";
+import ReplyMessage from "./ReplyMessageDraft";
+
+export const MessageDraftBox = ({chat}) => {
+    const message = chat?.draft?.mainInputValue?.message || null;
+    
+    return (message)? 
+        <MessageDraftBoxWrapper>
+             <ReplyMessage chat={chat} mes={message} /> 
+        </MessageDraftBoxWrapper>
+    :
+    <div></div>
+   
+    
+}

+ 27 - 0
src/components/MessageDraft/MessageEditor.jsx

@@ -0,0 +1,27 @@
+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>
+            <EditRoundedIcon color="primary"/>
+            <MessageDraftContainer >
+                <MessageDraftInfo>
+                    <MessageOwnerDraftName>
+                        {message?.owner?.nick || ''}
+                    </MessageOwnerDraftName>
+                    <MessageDraftText>
+                        {message?.text.replace(/ /g, "\u00a0") || ''}
+                    </MessageDraftText>
+                </MessageDraftInfo>
+            </MessageDraftContainer>
+            <CloseRoundedIcon style={{cursor: 'pointer'}} color="primary" onClick={() => {deleteMessageEditor(chatId, null)}}/>
+        </MessageDraftWrapper>
+    )
+}
+
+// export connect(null, {})(MessageEditor);

+ 31 - 0
src/components/MessageDraft/ReplyMessageDraft.jsx

@@ -0,0 +1,31 @@
+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';
+
+const ReplyMessage = ({chat, deleteDraftMessage, mes}) => {
+    const message = typeof mes !== 'object' ?  chat?.messages?.[mes] : mes;
+    return (
+        <MessageDraftWrapper>
+            {typeof mes !== 'object' ? <ReplyIcon color="primary"/> : <ForwardRoundedIcon color="primary"/>}
+            <MessageDraftContainer>
+                {message?.media && checkTypeFileForReply(message.media?.[0]?.type, false, message.media?.[0]?.url)}
+                <MessageDraftInfo>
+                    <MessageOwnerDraftName>
+                        {message?.owner?.nick || ''}
+                    </MessageOwnerDraftName>
+                    <MessageDraftText>
+                        {message?.text.replace(/ /g, "\u00a0") || ''}
+                    </MessageDraftText>
+                </MessageDraftInfo>
+            </MessageDraftContainer>
+            <CloseRoundedIcon style={{cursor: 'pointer'}} color="primary" onClick={() => {deleteDraftMessage(chat._id, null)}}/>
+        </MessageDraftWrapper>
+    ) 
+}
+
+export default connect(null, {deleteDraftMessage: actionAddDraftMessage})(ReplyMessage);

+ 66 - 0
src/components/MessageEditorMediaModal/MessageEditorMediaModal.jsx

@@ -0,0 +1,66 @@
+import { DropMediaList, MediaBox, MediaModalHeader } from "../MessageMediaModal/MessageMediaModal.style"
+import { MessageEditorMediaFooter, MessageEditorMediaModalWrapper } from "./MessageEditorMediaModal.style"
+import SimpleBar from 'simplebar-react';
+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";
+
+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);
+    }, [open])
+
+    return (
+        <MessageEditorMediaModalWrapper>
+            <MediaModalHeader>Edit message</MediaModalHeader>
+            <MediaBox>
+                <SimpleBar style={{ maxHeight: '400px'}}>
+                    <DropMediaList>
+                        {media.map((item) => <MemoizedDropMediaItem key={item.uploadDate} chatId={chatId} mediaItem={item}/>)}
+                    </DropMediaList>
+                </SimpleBar>
+            </MediaBox>
+            <TextField
+                maxRows={7}
+                multiline
+                sx={{width: "100%", mt: "30px"}}
+                variant="standard" 
+                label="Caption"
+                onChange = {(e) => setInputValue(chatId, e.target.value, 'messageEditor')}
+                value={inputValue}
+            />
+            <MessageEditorMediaFooter>
+                    <Button variant="text" onClick={handleClose}>Cancel</Button>
+                    <Button 
+                        variant="text" 
+                        onClick={
+                            async () => {
+                                let val = await sendMessage(messageId, chatId, inputValue.replace(/^\s+|\s+$/g, ''), media, null, null);
+                                val && setInputValue(chatId, "", 'messageEditor'); handleClose();
+                            }
+                        }
+                    >Send</Button>
+                </MessageEditorMediaFooter>
+        </MessageEditorMediaModalWrapper>
+    )
+} 
+
+export default connect(state => ({
+    chats: state.chats
+}),
+{
+    deleteMedia: actionDeleteDropMedia, 
+    deleteMessageEditor: actionSetMessageEditor,
+    sendMessage: actionSendMessage,
+    setInputValue: actionSetInputMessageValue,
+})(MessageEditorMediaModal);

+ 19 - 0
src/components/MessageEditorMediaModal/MessageEditorMediaModal.style.js

@@ -0,0 +1,19 @@
+import styled from 'styled-components'
+
+export const MessageEditorMediaModalWrapper = styled.div`
+	width: 100%;
+	height: 100%;
+	padding: 30px 30px 20px;
+`;
+
+export const MessageMediaFooter = styled.div`
+	margin-top: 20px;
+	display: flex;
+	justify-content: space-between;
+`;
+
+export const MessageEditorMediaFooter = styled.div`
+	margin-top: 20px;
+	display: flex;
+	justify-content: right;
+`;

+ 16 - 0
src/components/MessageMediaContainer/MessageMediaContainer.jsx

@@ -0,0 +1,16 @@
+import { memo } from "react"
+import { MessageMediaItem } from "../MessageMediaItem/MessageMediaItem"
+import { MessageMediaContainerWrap, MessageMediaList } from "./MessageMediaContainer.style"
+
+const MessageMediaContainer = ({media}) => {
+    console.log(media)
+    return (
+        <MessageMediaContainerWrap>
+            <MessageMediaList>
+                {media.map(file => <MessageMediaItem key={file._id} file={file}/>)}
+            </MessageMediaList>
+        </MessageMediaContainerWrap>
+    )
+}
+
+export const MemoMessageMediaContainer = memo(MessageMediaContainer);

+ 11 - 0
src/components/MessageMediaContainer/MessageMediaContainer.style.js

@@ -0,0 +1,11 @@
+import styled from "styled-components";
+
+export const MessageMediaContainerWrap = styled.div`
+
+`;
+
+export const MessageMediaList = styled.ul`
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+`;

+ 31 - 0
src/components/MessageMediaItem/MessageMediaItem.jsx

@@ -0,0 +1,31 @@
+import { AudioItem } from "../MessageMediaItems/MessageAudioItem/AudioItem"
+import { FileItem } from "../MessageMediaItems/MessageFileItem/MessageFileItem";
+import { MessageImgItem } from "../MessageMediaItems/MessageImgItem/MessageImgItem";
+import { MessageVideoItem } from "../MessageMediaItems/MessageVideoItem/MessageVideoItem";
+import { MessageMediaItemWrap } from "./MessageMediaItem.style"
+
+export const MessageMediaItem = ({file}) => {
+    const url = file.url;
+    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}/>
+        }else if (type.includes("audio")){
+            return <AudioItem url={url} name={name}/>
+        }else if(type.includes("video")){
+            return <MessageVideoItem url={url}/>
+        }else {
+            return <FileItem url={url} name={name}/>
+        }
+    }
+
+    return (
+        <MessageMediaItemWrap>
+            {checkType(file.type)}
+        </MessageMediaItemWrap>
+    )
+}

+ 5 - 0
src/components/MessageMediaItem/MessageMediaItem.style.js

@@ -0,0 +1,5 @@
+import styled from "styled-components";
+
+export const MessageMediaItemWrap = styled.li`
+
+`;

+ 32 - 0
src/components/MessageMediaItems/MessageAudioItem/AudioItem.jsx

@@ -0,0 +1,32 @@
+import { FileWrap, FileIconWrap, FileIcon, AudioPlayer, FileTitle, DownloaderFile } from "./AudioItem.style";
+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";
+
+export const AudioItem = ({url, name}) => {
+
+    const [isOpen, setIsOpen] = useState(false);
+
+    const handleOpen = () => setIsOpen(!isOpen);
+
+    return (
+        <div>
+            <FileWrap>
+                <FileIconWrap isAudio={true} onClick={handleOpen}>
+                    <FileIcon src={MusicIcon}/>
+                </FileIconWrap>
+                <div>
+                    <FileTitle>{name}</FileTitle>
+                    <div style={{display: 'flex', justifyContent: 'right'}}>
+                        <DownloaderFile onClick={() => saveFile(`${backendURL}/${url}`, name)} src={DownloadIcon}></DownloaderFile>
+                    </div>
+                    
+                </div>
+            </FileWrap>
+
+            {isOpen && <AudioPlayer controls src={`${backendURL}/${url || ''}`}/>}
+        </div>
+    )
+}

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

@@ -0,0 +1,41 @@
+import styled from "styled-components";
+
+export const FileWrap = styled.div`
+    display: flex;
+    justify-content: left;
+    align-items: center;
+    margin: 5px 0;
+`;
+
+export const FileIconWrap = styled.div`
+    height: 44px;
+    width: 44px;
+    border-radius: 50%;
+    background-color: #3498DB;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 10px;
+    
+    cursor: pointer;
+`;
+
+export const FileIcon = styled.img`
+    height: 26px;
+`;
+
+export const FileTitle = styled.div`
+    font-size: 14px;
+    font-weight: 600;
+`;
+
+export const DownloaderFile = styled.img`
+    height: 16px;
+    cursor: pointer;
+`;
+
+export const AudioPlayer = styled.audio`
+    height: 30px;
+    margin-top: 10px;
+    width: 100%;
+`;

BIN
src/components/MessageMediaItems/MessageAudioItem/icons8-download.png


+ 25 - 0
src/components/MessageMediaItems/MessageFileItem/MessageFileItem.jsx

@@ -0,0 +1,25 @@
+import { backendURL } from "../../../helpers/gql"
+import { saveFile } from "../../utils/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';
+
+export const FileItem = ({url, name}) => {
+
+    return (
+        <div>
+            <FileWrap>
+                <FileIconWrap>
+                    <FileIcon src={File}/>
+                </FileIconWrap>
+                <div>
+                    <FileTitle>{name}</FileTitle>
+                    <div style={{display: 'flex', justifyContent: 'right'}}>
+                        <DownloaderFile onClick={() => saveFile(`${backendURL}/${url}`, name)} src={DownloadIcon}></DownloaderFile>
+                    </div>
+                    
+                </div>
+            </FileWrap>
+        </div>
+    )
+}

+ 0 - 0
src/components/MessageMediaItems/MessageFileItem/MessageFileItem.style.js


+ 12 - 0
src/components/MessageMediaItems/MessageImgItem/MessageImgItem.jsx

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

+ 16 - 0
src/components/MessageMediaItems/MessageImgItem/MessageImgItem.style.js

@@ -0,0 +1,16 @@
+import styled from "styled-components";
+
+export const MessageImgItemWrap = styled.div`
+    position: relative;
+`;
+
+export const ImageItem = styled.img`
+    width: 100%; 
+    max-height: 140px;
+`;
+
+export const DownloadImg = styled.img`
+    position: absolute;
+    top: 0;
+    right: 0
+`;

+ 12 - 0
src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.jsx

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

+ 15 - 0
src/components/MessageMediaItems/MessageVideoItem/MessageVideoItem.style.js

@@ -0,0 +1,15 @@
+import styled from "styled-components";
+
+export const MessageVideoItemWrap = styled.div`
+    position: relative;
+`;
+
+export const VideoItem = styled.video`
+    width: 100%; 
+`;
+
+export const DownloadVideo = styled.img`
+    position: absolute;
+    top: 0;
+    right: 0
+`;

+ 97 - 0
src/components/MessageMediaModal/MessageMediaModal.jsx

@@ -0,0 +1,97 @@
+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 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";
+ 
+
+const MessageMediaModal = ({chatId, handleClose, deleteMedia, deleteDraftMessage, chats, open, onload, sendMessage, setInputValue}) => {
+    const media = chats[chatId].draft?.media || [];
+    const inputValue = chats[chatId].draft?.mainInputValue?.value || '';
+    const message = chats[chatId]?.draft?.mainInputValue?.message;
+    let replyMessageId = (message && typeof message !== 'object') ? message : null;
+    let forwardedMessageId = (message && typeof message === 'object') ? message._id : null;
+    //remove media array
+    useEffect(() => {
+        open || deleteMedia(chatId);
+        
+    }, [open])
+
+    const {acceptedFiles, getRootProps, getInputProps} = useDropzone({
+        noDrag: true, 
+        multiple: true
+    });
+
+    useEffect(()=>{
+        let files = addUploadDate(acceptedFiles)
+        console.log(files)
+        acceptedFiles[0] && onload(chatId, files)
+    }, [acceptedFiles]);
+
+    useEffect(() => {
+        media.length == 0 && handleClose();
+    }, [media])
+
+    return(
+        <MessageMediaModalWrapper>
+            <MediaModalHeader>{media.length} {media.length === 1 ? "file" : "files"} selected</MediaModalHeader>
+            <MediaBox>
+                <SimpleBar style={{ maxHeight: '400px'}}>
+                    <DropMediaList>
+                        {media.map((item) => <MemoizedDropMediaItem key={item.uploadDate} chatId={chatId} mediaItem={item}/>)}
+                    </DropMediaList>
+                </SimpleBar>
+            </MediaBox>
+            <TextField 
+                maxRows={7}
+                multiline
+                sx={{width: "100%", mt: "30px"}}
+                variant="standard" 
+                label="Caption"
+                onChange = {(e) => setInputValue(chatId, e.target.value, 'mainInputValue')}
+                value={inputValue}
+            />
+            
+            <MessageMediaFooter>
+                <div 
+                    {...getRootProps({className: 'dropzone'})}>
+                    <input {...getInputProps()} />
+                    <Button variant="text">Add</Button>
+                </div>
+                
+                <div>
+                    <Button variant="text" onClick={handleClose}>Cancel</Button>
+                    <Button 
+                        variant="text" 
+                        onClick={
+                            async () => {
+                                let val = await sendMessage(null, chatId, inputValue.replace(/^\s+|\s+$/g, ''), media, replyMessageId, forwardedMessageId);
+                                (val?.replyTo?._id || val?.forwarded?._id) && deleteDraftMessage(chatId, null)
+                                val && setInputValue(chatId, "", 'mainInputValue'); handleClose();
+                            }
+                        }
+                    >Send</Button>
+                </div>
+            </MessageMediaFooter>
+        </MessageMediaModalWrapper>
+    )
+}
+
+export default connect(state => ({
+        chats: state.chats
+    }),
+    {
+        deleteMedia: actionDeleteDropMedia, 
+        onload: actionSetDropMedia, 
+        sendMessage: actionSendMessage,
+        setInputValue: actionSetInputMessageValue,
+        deleteDraftMessage: actionAddDraftMessage,
+        
+    })(MessageMediaModal);

+ 31 - 0
src/components/MessageMediaModal/MessageMediaModal.style.js

@@ -0,0 +1,31 @@
+import styled from 'styled-components'
+
+export const MessageMediaModalWrapper = styled.div`
+	width: 100%;
+	height: 100%;
+	padding: 30px 30px 20px;
+`;
+
+export const MediaBox = styled.div`
+`;
+
+export const MediaModalHeader = styled.h3`
+	margin: 0 0 20px;
+	font-weight: 600;
+`;
+
+export const MessageMediaFooter = styled.div`
+	margin-top: 20px;
+	display: flex;
+	justify-content: space-between;
+`;
+
+export const DropMediaList = styled.ul`
+	list-style-type: none;
+	padding: 0; 
+	margin: 0;
+	display: flex;
+	flex-wrap: wrap;
+	
+`;
+

+ 59 - 0
src/components/MessageOptions/MessageOptions.jsx

@@ -0,0 +1,59 @@
+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';
+
+const MessageOptions = ({userId, addDraftMessageEditor, handleClose, addMessageId, open, anchorEl, chatId, message, addDraftMessage, openModal}) => {
+    const isOwner = userId === message.owner?._id;
+    return(
+        
+        <Menu
+                id="demo-positioned-menu"
+                aria-labelledby="demo-positioned-button"
+                anchorEl={anchorEl}
+                open={open}
+                onClose={handleClose}
+                anchorOrigin={{
+                    vertical: 'bottom',
+                    horizontal: 'center',
+                    }} 
+                transformOrigin={{
+                    vertical: 'top',
+                    horizontal: 'center',
+                }}
+            >
+                <MenuItem onClick={() => {handleClose(); addDraftMessage(chatId, message._id)}} disableRipple>
+                    <ImgIcon src={ReplyIcon} />
+                    Reply
+                </MenuItem>
+                {isOwner && <MenuItem onClick={() => {handleClose(); addDraftMessageEditor(chatId, message)}} disableRipple>
+                    <EditIcon style={{height: '18px', width: '18px', marginRight: '18px'}}/>
+                    Edit
+                </MenuItem>}
+                
+
+                <MenuItem onClick={() => {addMessageId(message, 'forwardedMessage'); openModal('searchChatModal'); handleClose()}} disableRipple> 
+                    <ImgIcon src={ForwardIcon} />
+                    Forward
+                </MenuItem>
+
+                <MenuItem onClick={() => {handleClose()}} disableRipple>
+                    <DeleteOutlineIcon style={{height: '18px', width: '18px', marginRight: '18px'}}/>
+                    Delete
+                </MenuItem>
+            </Menu>
+    )
+}
+
+export default connect(state => ({userId: state?.auth?.payload?.sub?.id}), {addDraftMessage: actionAddDraftMessage, addDraftMessageEditor: actionEditMessage, openModal: actionOpenModal, addMessageId: actionModalDraft})(MessageOptions)

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

@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const MessageOptionsWrapper = styled.ul`
+    
+`;
+
+export const MessageOptionsItem = styled.li`
+
+
+
+`;
+
+export const ImgIcon = styled.img`
+    height: 16px;
+    margin-right: 20px;
+`;

BIN
src/components/MessageOptions/icons8-forward.png


BIN
src/components/MessageOptions/icons8-reply.png


+ 69 - 0
src/components/MessagesArea/MessagesArea.jsx

@@ -0,0 +1,69 @@
+import { connect } from "react-redux"
+import { Message } from "../Message/Message"
+import { MessagesAreaWrapper, SimpleBarWrapper } from "./MessagesArea.style"
+import 'simplebar/dist/simplebar.min.css';
+import InfiniteScroll from 'react-infinite-scroll-component';
+import { useEffect, useRef, useState } from "react";
+import { actionGetMessageForChat } from "../../actions/actionGetMessageForChat";
+// import { ClassNames } from "@emotion/react";
+
+// 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}) => {
+	
+	// console.log(chats[chatId].messages)
+	const messagesByChat = Object.values(chats[chatId]?.messages || []);
+	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){
+				let data = await getMessages(chatId);
+				if (data.length < 20) ref.current?.contentWrapperEl.removeEventListener("scroll", infinityScroll);
+				// console.log('вызов')
+			}
+		},30)
+	}
+	
+	useEffect(() => {
+		ref.current?.contentWrapperEl.addEventListener("scroll", infinityScroll);
+		// Remove listener (like componentWillUnmount)
+		// console.log('i am working from useEffect');
+		return () => {
+			ref.current.contentWrapperEl.scrollTop = 0;
+			ref.current?.contentWrapperEl.removeEventListener("scroll", infinityScroll);
+			console.log("unMount from parent")
+		};
+	  }, [chatId]);
+	
+	
+	
+	return (
+		
+			<MessagesAreaWrapper >
+					 <SimpleBarWrapper  ref={ref}> 
+						{messagesByChat.map((item) => <Message key={item._id} chatId={chatId} mes={item}/>)}
+					 </SimpleBarWrapper> 
+			</MessagesAreaWrapper>
+	)
+}
+
+export default connect(state=>({chats: state.chats || {}}), {getMessages : actionGetMessageForChat})(MessagesArea)

+ 20 - 0
src/components/MessagesArea/MessagesArea.style.js

@@ -0,0 +1,20 @@
+import styled from "styled-components";
+import 'simplebar/dist/simplebar.min.css';
+import SimpleBar from 'simplebar-react';
+
+export const MessagesAreaWrapper = styled.div`
+    width: 100%;
+    overflow: hidden;
+    
+`;
+
+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;
+        
+    }
+    
+`;

+ 57 - 0
src/components/Modal.jsx

@@ -0,0 +1,57 @@
+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";
+
+const ModalComponent = ({modal, setOpen}) => {
+    const [,route, histId] = history.location.pathname.split('/');
+    const open = modal?.isOpen || false;
+    const handleClose = () => setOpen(false);
+    
+    const modalMap = {
+        createChatModal: <CreateNewChat 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}/>
+    }
+
+	return (
+        <Modal 
+            aria-labelledby="transition-modal-title"
+            aria-describedby="transition-modal-description"
+            open={open}
+            onClose={handleClose}
+            closeAfterTransition
+            disableAutoFocus
+            BackdropComponent={Backdrop}
+            BackdropProps={{
+            timeout: 500,
+            }}
+            >
+                <Fade in={open}>
+                    <Box className='ModalBox'>
+                        {modalMap[modal.content]}
+                    </Box>
+                </Fade>
+
+        </Modal>
+	)
+}
+
+export const CModalComponent = connect(state => ({modal: state?.modal || {} }), {setOpen: actionIsOpen})(ModalComponent)

+ 19 - 0
src/components/NewChatButton.jsx

@@ -0,0 +1,19 @@
+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/ReplyMessage/ReplyMessage.jsx


Some files were not shown because too many files changed in this diff