Vitalii Polishchuk před 3 roky
rodič
revize
bb846b5a82

+ 15 - 0
.vscode/launch.json

@@ -0,0 +1,15 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "pwa-chrome",
+            "request": "launch",
+            "name": "Launch Chrome against localhost",
+            "url": "http://localhost:8080",
+            "webRoot": "${workspaceFolder}"
+        }
+    ]
+}

+ 474 - 0
package-lock.json

@@ -2326,6 +2326,15 @@
         "@types/node": "*"
       }
     },
+    "@types/hoist-non-react-statics": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+      "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+      "requires": {
+        "@types/react": "*",
+        "hoist-non-react-statics": "^3.3.0"
+      }
+    },
     "@types/html-minifier-terser": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz",
@@ -2505,11 +2514,37 @@
       "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.1.tgz",
       "integrity": "sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw=="
     },
+    "@types/prop-types": {
+      "version": "15.7.4",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
+      "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
+    },
     "@types/q": {
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
       "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ=="
     },
+    "@types/react": {
+      "version": "17.0.27",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.27.tgz",
+      "integrity": "sha512-zgiJwtsggVGtr53MndV7jfiUESTqrbxOcBvwfe6KS/9bzaVPCTDieTWnFNecVNx6EAaapg5xsLLWFfHHR437AA==",
+      "requires": {
+        "@types/prop-types": "*",
+        "@types/scheduler": "*",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@types/react-redux": {
+      "version": "7.1.18",
+      "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz",
+      "integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==",
+      "requires": {
+        "@types/hoist-non-react-statics": "^3.3.0",
+        "@types/react": "*",
+        "hoist-non-react-statics": "^3.3.0",
+        "redux": "^4.0.0"
+      }
+    },
     "@types/resolve": {
       "version": "0.0.8",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@@ -2518,6 +2553,11 @@
         "@types/node": "*"
       }
     },
+    "@types/scheduler": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+    },
     "@types/source-list-map": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -2921,6 +2961,11 @@
         "regex-parser": "^2.2.11"
       }
     },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+    },
     "agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -3105,6 +3150,11 @@
         "es-abstract": "^1.19.0"
       }
     },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+    },
     "arrify": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
@@ -3205,6 +3255,11 @@
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
       "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
     },
+    "attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+    },
     "autoprefixer": {
       "version": "9.8.7",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.7.tgz",
@@ -3685,6 +3740,11 @@
       "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
       "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
     },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3740,6 +3800,16 @@
         }
       }
     },
+    "base-64": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+      "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3750,6 +3820,14 @@
       "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
       "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY="
     },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
     "bfj": {
       "version": "7.0.2",
       "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz",
@@ -3772,6 +3850,11 @@
       "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
       "optional": true
     },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -4088,6 +4171,11 @@
         "caller-callsite": "^2.0.0"
       }
     },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+    },
     "callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4342,11 +4430,21 @@
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
     },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+    },
     "component-emitter": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
       "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
     },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+    },
     "compose-function": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
@@ -4914,6 +5012,11 @@
         }
       }
     },
+    "csstype": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz",
+      "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw=="
+    },
     "cyclist": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@@ -5414,6 +5517,76 @@
         "once": "^1.4.0"
       }
     },
+    "engine.io-client": {
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz",
+      "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==",
+      "requires": {
+        "component-emitter": "~1.3.0",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.2.0",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.6",
+        "parseuri": "0.0.6",
+        "ws": "~6.1.0",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        },
+        "parseqs": {
+          "version": "0.0.6",
+          "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
+          "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
+        },
+        "parseuri": {
+          "version": "0.0.6",
+          "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
+          "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
+        },
+        "ws": {
+          "version": "6.1.4",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+          "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+          "requires": {
+            "async-limiter": "~1.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
+      "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.4",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      },
+      "dependencies": {
+        "base64-arraybuffer": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
+          "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
+        }
+      }
+    },
     "enhanced-resolve": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
@@ -6631,6 +6804,21 @@
         }
       }
     },
+    "file-selector": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
+      "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
+      "requires": {
+        "tslib": "^2.0.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+          "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+        }
+      }
+    },
     "filesize": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@@ -7086,6 +7274,26 @@
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
       "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
     },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -7197,6 +7405,19 @@
       "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
       "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
     },
+    "history": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "loose-envify": "^1.2.0",
+        "resolve-pathname": "^3.0.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0",
+        "value-equal": "^1.0.1"
+      }
+    },
     "hmac-drbg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -7207,6 +7428,14 @@
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
+    "hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "requires": {
+        "react-is": "^16.7.0"
+      }
+    },
     "hoopy": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -7639,6 +7868,11 @@
       "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
       "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
     },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
     "infer-owner": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -8125,6 +8359,14 @@
         "istanbul-lib-report": "^3.0.0"
       }
     },
+    "javascript-time-ago": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.3.9.tgz",
+      "integrity": "sha512-AhVoLXsN+CRNjVaTM837zIN/8uRzGy2G/8MTNw24bjBFpWyqMeGQCAoI5HOED7UKCqK2fuXDuAugBbmbODpzkA==",
+      "requires": {
+        "relative-time-format": "^1.0.5"
+      }
+    },
     "jest": {
       "version": "26.6.0",
       "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz",
@@ -10076,6 +10318,15 @@
       "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
       "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
     },
+    "mini-create-react-context": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+      "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "tiny-warning": "^1.0.3"
+      }
+    },
     "mini-css-extract-plugin": {
       "version": "0.11.3",
       "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz",
@@ -10496,6 +10747,11 @@
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
     },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+    },
     "object-copy": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@@ -10826,6 +11082,22 @@
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
       "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
     },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -12507,6 +12779,16 @@
         "scheduler": "^0.20.2"
       }
     },
+    "react-dropzone": {
+      "version": "11.4.2",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.4.2.tgz",
+      "integrity": "sha512-ocYzYn7Qgp0tFc1gQtUTOaHHSzVTwhWHxxY+r7cj2jJTPfMTZB5GWSJHdIVoxsl+EQENpjJ/6Zvcw0BqKZQ+Eg==",
+      "requires": {
+        "attr-accept": "^2.2.1",
+        "file-selector": "^0.2.2",
+        "prop-types": "^15.7.2"
+      }
+    },
     "react-error-overlay": {
       "version": "6.0.9",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
@@ -12517,11 +12799,70 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "react-redux": {
+      "version": "7.2.5",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz",
+      "integrity": "sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "@types/react-redux": "^7.1.16",
+        "hoist-non-react-statics": "^3.3.2",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.7.2",
+        "react-is": "^16.13.1"
+      }
+    },
     "react-refresh": {
       "version": "0.8.3",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
       "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
     },
+    "react-router": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
+      "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "hoist-non-react-statics": "^3.1.0",
+        "loose-envify": "^1.3.1",
+        "mini-create-react-context": "^0.4.0",
+        "path-to-regexp": "^1.7.0",
+        "prop-types": "^15.6.2",
+        "react-is": "^16.6.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "path-to-regexp": {
+          "version": "1.8.0",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+          "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+          "requires": {
+            "isarray": "0.0.1"
+          }
+        }
+      }
+    },
+    "react-router-dom": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
+      "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.2",
+        "react-router": "5.2.1",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      }
+    },
     "react-scripts": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz",
@@ -12588,6 +12929,20 @@
         "workbox-webpack-plugin": "5.1.4"
       }
     },
+    "react-scrollable-feed": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/react-scrollable-feed/-/react-scrollable-feed-1.3.1.tgz",
+      "integrity": "sha512-C+ED45W+wY5EKhBmS2h6D/Nt0fv1Qx0KrvhHwpKpqpooeb/PjyLZqyVSCdFDSTJ2d9baP1Qa7ZB7/SXFnYghMA=="
+    },
+    "react-time-ago": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.1.3.tgz",
+      "integrity": "sha512-H+mhWft++gNt2x8Y9eAZ9vYwL6giPDnVIo68Ty7xFTJ2L+Tt7cyZ1sbMkTMKzHeeaQ+J8F2vt3PatdW6mFJmWQ==",
+      "requires": {
+        "prop-types": "^15.7.2",
+        "raf": "^3.4.1"
+      }
+    },
     "read-pkg": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -12707,6 +13062,19 @@
         "strip-indent": "^3.0.0"
       }
     },
+    "redux": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz",
+      "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==",
+      "requires": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
+    "redux-thunk": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+      "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+    },
     "regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -12799,6 +13167,11 @@
       "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
       "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
     },
+    "relative-time-format": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.0.6.tgz",
+      "integrity": "sha512-voemOJLxlKun4P1fAo4PEg2WXNGjhqfE/G8Xen4gcy24Hyu/djn5bT5axmhx4MnjynoZ8f0HCOjk3RZpsY6X/g=="
+    },
     "remove-trailing-separator": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -12940,6 +13313,11 @@
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
     },
+    "resolve-pathname": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+    },
     "resolve-url": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -13770,6 +14148,72 @@
         }
       }
     },
+    "socket.io-client": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
+      "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~4.1.0",
+        "engine.io-client": "~3.4.0",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.3.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
+      "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
+      "requires": {
+        "component-emitter": "~1.3.0",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
     "sockjs": {
       "version": "0.3.21",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
@@ -14516,11 +14960,26 @@
       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
     },
+    "tiny-invariant": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
+      "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+    },
+    "tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "tmpl": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
       "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="
     },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+    },
     "to-arraybuffer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@@ -14976,6 +15435,11 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "value-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -16393,6 +16857,11 @@
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
     },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -16447,6 +16916,11 @@
         }
       }
     },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

+ 12 - 1
package.json

@@ -6,9 +6,19 @@
     "@testing-library/jest-dom": "^5.14.1",
     "@testing-library/react": "^11.2.7",
     "@testing-library/user-event": "^12.8.3",
+    "base-64": "^1.0.0",
+    "javascript-time-ago": "^2.3.9",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
+    "react-dropzone": "^11.4.2",
+    "react-redux": "^7.2.5",
+    "react-router-dom": "^5.3.0",
     "react-scripts": "4.0.3",
+    "react-scrollable-feed": "^1.3.1",
+    "react-time-ago": "^7.1.3",
+    "redux": "^4.1.1",
+    "redux-thunk": "^2.3.0",
+    "socket.io-client": "^2.3.0",
     "web-vitals": "^1.1.2"
   },
   "scripts": {
@@ -34,5 +44,6 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  }
+  },
+  "proxy": "http://chat.fs.a-level.com.ua/"
 }

+ 138 - 23
src/App.css

@@ -1,38 +1,153 @@
 .App {
-  text-align: center;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
 }
 
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
+.reg-form {
+  display: flex;
+  flex-direction: column;
+  width: 30vw;
 }
 
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
+.chat-window {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.aside-chat {
+  background: rgb(158, 158, 226);
+  width: 20vw;
+  margin-bottom: 5px;
 }
 
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
+.aside-chat a {
+  display: flex;
+  text-decoration: none;
+  color: black;
+  border: 1px solid black;
+}
+
+.aside-chat-info {
   display: flex;
   flex-direction: column;
+}
+
+.aside-chat img,
+.chat-edit-form img {
+  width: 40%;
+  object-fit: contain;
+}
+
+.aside-chat h5 {
+  margin: 0;
+}
+
+main {
+  display: flex;
   align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
+  width: 100%;
+}
+
+.new-chat {
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-list,
+.message-list {
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+  height: 100vh;
+}
+
+.chat-list li {
+  list-style: none;
 }
 
-.App-link {
-  color: #61dafb;
+.aside-chat-btn button {
+  width: 100%;
+  height: 40px;
 }
 
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
+.aside-chat-btn {
+  margin-bottom: 5px;
+}
+
+.msg-user,
+.msg-someone {
+  display: flex;
+  flex-direction: column;
+  border: 1px solid black;
+  margin: 10px;
+  list-style: none;
+  border-radius: 10px;
+  padding: 5px;
+  max-width: 200px;
+  min-width: 200px;
+  background: honeydew;
+}
+
+.msg-user {
+  align-self: flex-end;
+}
+
+.msg-nick {
+  font-weight: 600;
+}
+
+.msg-date {
+  font-size: 12px;
+}
+
+.chat-window {
+  flex-grow: 2;
+  background: rgb(209, 207, 207);
+}
+
+.pick {
+  background: red;
+}
+
+.msg-text {
+  white-space: pre-line;
+}
+
+.message-input {
+  display: flex;
+}
+
+textarea {
+  flex-grow: 1;
+  resize: none;
+}
+
+.user-search-list {
+  width: 80%;
+  height: 300px;
+  overflow-y: scroll;
+  list-style: none;
+}
+
+.user-search-panel {
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-nav {
+  background: grey;
+  height: 5vw;
+}
+
+.chat-edit-form {
+  display: flex;
+  flex-direction: column;
+  width: 25vw;
+  padding: 0 10px;
+}
+
+.edit-btn-container {
+  display: flex;
 }

+ 88 - 18
src/App.js

@@ -1,25 +1,95 @@
-import logo from './logo.svg';
 import './App.css';
+import { BrowserRouter as Router, Route, Link, Switch, Redirect } from 'react-router-dom';
+import createHistory from "history/createBrowserHistory";
+import React, { useState } from 'react';
+import { Provider, connect } from 'react-redux';
+import store from './reducers';
+import { actionFullLogin, actionFullRegister, actionUploadFile, actionGetFile, actionFullAddChat, actionFullEditMSG, actionFullGetChats, actionAddMSG } from './actions';
+import { MyDropzone } from './components/dropzone';
+import { LoginForm } from './components/loginForm';
+import { RegistrationForm } from './components/registrationForm';
+import { ConnectChatOnline, ConnectMain } from './pages/main';
+
+//for tests
+// let msgARR = [{ "_id": "615e0b5fdd6e923e41f3348e", "text": "hi111", "createdAt": "1633553247000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e0c88dd6e923e41f3348f", "text": "hi111", "createdAt": "1633553544000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e0e02dd6e923e41f33490", "text": "hi111", "createdAt": "1633553922000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e102cdd6e923e41f33492", "text": "hi", "createdAt": "1633554476000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e113bdd6e923e41f33493", "text": "hi111", "createdAt": "1633554747000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e12cddd6e923e41f33495", "text": "hi111", "createdAt": "1633555149000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } },
+// { "_id": "615e146bdd6e923e41f33497", "text": "hi111", "createdAt": "1633555563000", "owner": { "login": "test1111", "_id": "61460c90ae7d905e54d32d11", "nick": null } }]
+
+// let chatsASS = {
+//   1: {
+//     title: "первый",
+//     lastModified: "токошо",
+//     avatar: {
+//       url: "/опа"
+//     },
+//     messages: [{}, {}, { text: "привет" }]
+//   },
+//   2: {
+//     title: "второй"
+//   },
+//   3: {
+//     title: "третий"
+//   }
+// }
+
+const Preloading = ({ promiseName, promiseState, children }) => {
+  return (
+    <>
+      {(promiseState[promiseName] && promiseState[promiseName].status === "RESOLVED") ? children : null}
+    </>
+  )
+}
+
+const CPreloading = connect(state => ({ promiseState: state.promise }))
+// const PrivateRoute = ({ component, roles, auth, fallback = "/login", ...originalProps }) => {
+//   const PageWrapper = (pageProps) => {
+//     const OriginalPage = component
+//     //сопоставить роли с аус
+//     if (пересечение роли с аус(ацл из него) в наличии) {
+//       return <OriginalPage {...pageProps} />
+//     } else {
+//       return <Redirect to={fallback} />
+//     }
+//   }
+//   return (
+//     <Route component={PageWrapper} {...originalProps} />
+//   )
+// }
+// const MyUltraRoute = connect(сделать аус)(PrivateRoute)
+
+const ConnectLoginForm = connect(null, { onLogin: actionFullLogin })(LoginForm)
+
+const ConnectRegistrationForm = connect(null, { onRegistration: actionFullRegister })(RegistrationForm)
+
+const ConnectDropZone = connect(null, { onUpload: actionUploadFile })(MyDropzone)
+
+store.subscribe(() => console.log(store.getState()))
+
+// console.log(store.dispatch(actionGetFile("61534453dd6e923e41f3344c")))
+// store.dispatch(actionFullGetChats("61460c90ae7d905e54d32d11"))
 
 function App() {
   return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
+    <Provider store={store}>
+      <Router history={createHistory()}>
+        <div className="App">
+          <Switch>
+
+            <Route path="/registration" component={ConnectRegistrationForm} exact />
+            <Route path="/login" component={ConnectLoginForm} exact />
+            <Route path="/:_id" component={ConnectMain} />
+            <Redirect from="/login" to="/chat" />
+            <Redirect from="/" to="/chat" />
+
+          </Switch>
+        </div >
+      </Router>
+    </Provider>
   );
 }
 
-export default App;
+export default App;

+ 476 - 0
src/actions/index.js

@@ -0,0 +1,476 @@
+//потом почищу лишнее
+
+const getGQL = url => {
+  return (query, variables) => {
+    return fetch(url, {
+      method: 'POST',
+      headers: {
+        "content-type": "application/json",
+        ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {})
+      },
+      body: JSON.stringify({ query, variables }),
+    }).then(res => res.json())
+  }
+}
+
+let gql = getGQL("/graphql")
+
+let reg = async (login, password) => {
+  let query = `mutation reg($l: String, $p: String) {
+      UserUpsert(user:  {
+          login: $l,
+          password: $p
+        } ) {
+          _id
+        }
+    }`
+
+  let qVariables = {
+    "l": login,
+    "p": password
+  }
+
+  let res = await gql(query, qVariables)
+  return res
+}
+
+let log = async (login, password) => {
+  let query = `query log($l: String, $p: String) {
+          login(login: $l, password: $p)
+        }`
+
+  let qVariables = {
+    "l": login,
+    "p": password
+  }
+
+  let token = await gql(query, qVariables)
+  return token.data.login
+}
+
+let userChats = async (id) => {
+  let query = `query getUserChats($uid: String){
+        UserFindOne(query: $uid){
+          chats{
+             _id title createdAt lastModified avatar{
+            _id url
+            } owner{
+              _id login nick
+            } members{
+                _id login nick
+            }
+          }
+        }
+      }`
+
+  let qVariables = {
+    "uid": JSON.stringify([{ "_id": id }])
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let chatMSG = async (id) => {
+  let query = `query getMSG($chatID: String){
+        MessageFind(query: $chatID){
+         _id text createdAt owner{
+          login _id nick
+          } replies {
+            _id text createdAt owner{
+              login _id nick
+              }
+          }
+        }
+      }`
+
+  let qVariables = {
+    "chatID": JSON.stringify([{ "chat._id": id }])
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let msgCount = async (chat_id) => {
+  let query = `query msgCount($query: String){
+        MessageCount(query: $query)
+      }`
+
+  let qVariables = {
+    "query": JSON.stringify([{ "chat._id": chat_id }])
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let chatSortMSG = async (id) => {
+  let query = `query getMSGWithSort($query: String){
+        MessageFind(query: $query){
+         _id text createdAt owner{
+          login _id nick
+        } forwarded{
+          _id text
+        }
+        }
+      }`
+
+  let qVariables = {
+    "query": JSON.stringify([{ "chat._id": id }, { sort: [{ title: -1 }], skip: [100], limit: [100] }])
+  }
+
+  //меняем skip по событию скролла вверх
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let newChat = async (title, members) => {
+  let membersQuery = []
+
+  if (Array.isArray(members)) {
+    members.map(member => membersQuery.push({ "_id": member._id }))
+  }
+
+  let query = `mutation newChat($chat: ChatInput){
+        ChatUpsert(chat: $chat){
+          _id title createdAt lastModified owner{
+            _id login nick
+          } avatar{
+            _id url
+          } messages {
+            _id text createdAt owner{
+              login nick
+            }
+          } members{
+            _id login nick
+          }
+        }
+      }`
+
+  let qVariables = {
+    "chat": {
+      "title": title,
+      "members": membersQuery
+    }
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let editChat = async (chat_id, title, members) => {
+  let input = {}
+  input._id = chat_id
+  input.title = title
+
+  if (members && Array.isArray(members)) {
+    let membersQuery = []
+
+    members.map(member => membersQuery.push({ "_id": member._id }))
+
+    input.members = membersQuery
+  }
+
+  let query = `mutation editChat($chat: ChatInput){
+        ChatUpsert(chat: $chat){
+          _id title members{
+            _id login nick
+          }
+        }
+      }`
+
+  let qVariables = {
+    "chat": input
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let avatarSet = async (avatar_id, chat_id) => {
+  let query = `mutation setAvatar($avatar: MediaInput){
+    MediaUpsert(media: $avatar){
+      _id text url
+    }
+  }`
+
+  let qVariables = {
+    "avatar": {
+      "_id": avatar_id,
+      "chatAvatars": {
+        "_id": chat_id
+      }
+    }
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let newMSG = async (chat_id, text, media) => {
+  let mediaQuery = []
+
+  if (media && Array.isArray(media)) {
+    media.map(item => mediaQuery.push({ "_id": item }))
+  } else {
+    mediaQuery.push({})
+  }
+
+  let query = `mutation newMSG($msg: MessageInput){
+        MessageUpsert(message: $msg){
+          _id text createdAt owner{
+            _id nick login
+          } media{
+            _id url
+          }
+        }
+      }`
+
+  let qVariables = {
+    "msg": {
+      "text": text,
+      "media": mediaQuery,
+      "chat": {
+        "_id": chat_id
+      }
+    }
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let editMSG = async (id, text, media) => {
+  let input = {}
+  input._id = id
+  input.text = text
+
+  if (media && Array.isArray(media)) {
+    let mediaQuery = []
+    media.map(item => mediaQuery.push({ "_id": item }))
+    input.media = mediaQuery
+  }
+
+  let query = `mutation editMSG($msg: MessageInput){
+        MessageUpsert(message: $msg){
+          _id text media{
+            _id url
+          } chat{
+            _id
+          }
+        }
+      }`
+
+  let qVariables = {
+    "msg": input
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let fileFound = async (id) => {
+  let query = `query findMedia($id: String){
+        MediaFind(query: $id){
+          url originalFileName owner {
+            login _id
+          }
+        }
+      }`
+
+  let qVariables = {
+    "id": JSON.stringify([{ "_id": id }])
+  }
+
+  let result = await gql(query, qVariables)
+  return result
+}
+
+let userSearch = async (searchInput) => {
+  let query = `query usrsFND($search: String){
+    UserFind(query: $search){
+      _id login nick avatar{
+        _id url
+      }
+    }
+  }`
+
+  let qVariables = {
+    "search": JSON.stringify([
+      {
+        $or: [{ login: `/^${searchInput}/` }, { nick: `/^${searchInput}/` }]
+      },
+      {
+        sort: [{ login: 1 }]
+      }
+    ])
+  }
+
+  let result = await gql(query, qVariables)
+  console.log(result)
+  return result
+}
+
+const actionPending = name => ({ type: 'PROMISE', status: 'PENDING', name })
+const actionResolved = (name, payload) => ({ type: 'PROMISE', status: 'RESOLVED', name, payload })
+const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
+
+const actionPromise = (name, promise) =>
+  async dispatch => {
+    dispatch(actionPending(name))
+    try {
+      let payload = await promise
+      dispatch(actionResolved(name, payload))
+      return payload
+    }
+    catch (error) {
+      dispatch(actionRejected(name, error))
+    }
+  }
+
+export const actionAuthLogin = token => ({ type: "LOGIN", token })
+
+export const actionLogin = (login, password) => actionPromise("login", log(login, password))
+
+export const actionFullLogin = (login, password) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionLogin(login, password))
+    console.log(result)
+    dispatch(actionAuthLogin(result))
+  }
+}
+
+const actionRegister = (login, password) => actionPromise("register", reg(login, password))
+
+export const actionFullRegister = (login, password) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionRegister(login, password))
+    console.log(result)
+    if (result.data.UserUpsert !== null) {
+      dispatch(actionFullLogin(login, password))
+    }
+  }
+}
+
+let upload = async (files) => {
+  let form = new FormData()
+  form.append("media", files)
+
+  return fetch("/upload", {
+    method: "POST",
+    headers: localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {},
+    body: form
+  }).then(res => res.json())
+}
+
+export const actionUploadFile = (files) => actionPromise("upload", upload(files))
+
+export const actionAuthLogout = () => ({ type: "LOGOUT" })
+
+export const actionGetFile = (id) => actionPromise("fileFound", fileFound(id))
+
+const actionGetChats = (id) => actionPromise("chats", userChats(id))
+
+const actionGetMessages = (chatID) => actionPromise("messages", chatMSG(chatID))
+
+const actionChats = (chat_id, title, createdAt, lastModified, avatar, messages, members) => ({ type: "ADD_CHAT", chat_id, title, createdAt, lastModified, avatar, messages, members })
+
+export const actionEditChat = (chat_id, title, avatar, members) => ({ type: "EDIT_CHAT", chat_id, title, avatar, members })
+
+const actionEditChatBack = (title, avatar, members) => actionPromise("edit_chat", editChat(title, avatar, members))
+
+export const actionEditMSG = (chat_id, message_id, text, media) => ({ type: "EDIT_MESSAGE", chat_id, messages: [{ _id: message_id, text, media }] })
+
+const actionEditMSGback = (message_id, text, media) => actionPromise("edit_message", editMSG(message_id, text, media))
+
+export const actionAddChatBack = (title, members) => actionPromise("chat", newChat(title, members))
+
+export const actionAddChat = (chat_id, title, createdAt, lastModified, owner, avatar, messages, members) => ({ type: "ADD_CHAT", chat_id, title, createdAt, lastModified, owner, avatar, messages, members })
+
+export const actionAddMSG = (chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media) => ({ type: "ADD_MESSAGE", chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media })
+
+export const actionAddMSGBack = (chat_id, text, media) => actionPromise("new_message", newMSG(chat_id, text, media))
+
+export const actionSetAvatar = (avatar_id, chat_id) => actionPromise("set_avatar", avatarSet(avatar_id, chat_id))
+
+// export const actionFullGetChats = (id) => {
+//   return async (dispatch) => {
+//     let chats = await dispatch(actionGetChats(id))
+
+//     chats.data.UserFindOne.chats.map(async (chat) => {
+//       let result = await dispatch(actionGetMessages(chat._id))
+//       let messages = result.data.MessageFind
+//       dispatch(actionChats(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.avatar, messages, chat.members))
+//     })
+//   }
+// }
+
+export const actionUserSearch = (searchInput) => actionPromise("userSearch", userSearch(searchInput))
+
+export const actionFullGetChats = (id) => {
+  return async (dispatch) => {
+    let chats = await dispatch(actionGetChats(id))
+
+    chats.data.UserFindOne.chats.map(async (chat) => {
+      // let result = await dispatch(actionGetMessages(chat._id))
+      // let messages = result.data.MessageFind
+      dispatch(actionChats(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.avatar, [], chat.members))
+    })
+  }
+}
+
+export const actionFullGetMessages = (id) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionGetMessages(id))
+    let messages = result.data.MessageFind
+    messages.map(message => dispatch(actionAddMSG(id, message._id, message.text, message.createdAt, message.owner, message.media)))
+  }
+}
+
+export const actionFullAddChat = (title, members) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionAddChatBack(title, members))
+
+    dispatch(actionAddChat(result.data.ChatUpsert._id,
+      result.data.ChatUpsert.title,
+      result.data.ChatUpsert.createdAt,
+      result.data.ChatUpsert.lastModified,
+      result.data.ChatUpsert.owner,
+      result.data.ChatUpsert.avatar,
+      result.data.ChatUpsert.messages,
+      result.data.ChatUpsert.members))
+  }
+}
+
+export const actionFullEditChat = (chat_id, title, avatar, members) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionEditChatBack(chat_id, title, members))
+    let avatarResult = await dispatch(actionSetAvatar(avatar, chat_id))
+
+    if (result.data?.ChatUpsert && avatarResult) {
+      dispatch(actionEditChat(chat_id, title, avatar, members))
+    }
+  }
+}
+
+export const actionFullAddMessage = (chat_id, text, media) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionAddMSGBack(chat_id, text, media))
+
+    // dispatch(actionAddMSG(result.))
+  }
+}
+
+export const actionFullEditMSG = (message_id, text, media) => {
+  return async (dispatch) => {
+    let result = await dispatch(actionEditMSGback(message_id, text, media))
+
+    dispatch(actionEditMSG(result.data.MessageUpsert.chat._id,
+      result.data.MessageUpsert._id,
+      result.data.MessageUpsert.text,
+      result.data.MessageUpsert.media))
+  }
+}
+

+ 59 - 0
src/components/chatEditForm.js

@@ -0,0 +1,59 @@
+import { useEffect, useState } from "react"
+import { connect } from "react-redux"
+import { actionEditChat, actionFullEditChat, actionUploadFile, actionUserSearch } from "../actions"
+import { MemberList, UserSearch } from "./chatForm"
+import { MyDropzone } from "./dropzone"
+
+export const ChatEditForm = ({ chat_id, chat, searchState, onUpload, onUserSearch, onChangeChat }) => {
+    let [edit, setEdit] = useState(false)
+    let [newTitle, setNewTitle] = useState(chat.title || "")
+    let [newAvatar, setNewAvatar] = useState(chat.avatar)
+    let [newMembers, setNewMembers] = useState(chat.members)
+
+    useEffect(() => {
+        setEdit(false)
+        setNewTitle(chat.title)
+        setNewAvatar(chat.avatar)
+        setNewMembers(chat.members)
+    }, [chat])
+
+    console.log(chat_id)
+
+    let editChatHandler = () => {
+        setEdit(!edit)
+        setNewMembers(chat.members)
+    }
+
+    let userAddHandler = (user) => {
+        setNewMembers([...newMembers, user])
+    }
+
+    let memberDeleteHandler = (user) => {
+        setNewMembers(newMembers.filter(member => member._id !== user._id))
+    }
+
+    return (
+        <div className="chat-edit-form">
+            {edit ? <input onChange={(e) => setNewTitle(e.target.value)} value={newTitle} /> : <span>Название чата: {chat.title}</span>}
+            {edit ? <div>
+                <span>Загрузите новый аватар</span>
+                <MyDropzone maxFiles={1} onUpload={onUpload} onSet={setNewAvatar} />
+            </div> :
+                chat.avatar && <img src={"/" + chat.avatar.url} />}
+
+            {<MemberList members={newMembers} onDeleteMember={edit && memberDeleteHandler} />}
+            {edit && <UserSearch searchState={searchState} onUserSearch={onUserSearch} onAddMember={userAddHandler} />}
+            <div className="edit-btn-container">
+                <button onClick={() => editChatHandler()} >{edit ? "Отменить редактирование" : "Редактировать чат"}</button>
+                {edit && <button onClick={() => onChangeChat(chat_id, newTitle, newAvatar._id, newMembers)}>Применить изменения</button>}
+            </div>
+        </div>
+    )
+}
+
+export const ConnectChatEditForm = connect(state => ({ searchState: state.promise?.userSearch?.payload?.data?.UserFind }),
+    {
+        onUpload: actionUploadFile,
+        onUserSearch: actionUserSearch,
+        onChangeChat: actionFullEditChat
+    })(ChatEditForm)

+ 89 - 0
src/components/chatForm.js

@@ -0,0 +1,89 @@
+import { useEffect, useState } from "react";
+import { connect } from "react-redux";
+import { actionAddChatBack, actionUserSearch } from "../actions"
+
+const UserList = ({ users, onAddMember }) => {
+    return (
+        <ul className="user-search-list">
+            {users.map(user =>
+                <li key={user._id}>
+                    <button onClick={() => onAddMember(user)}>{user.nick || user.login}</button>
+                </li>)}
+        </ul>
+    )
+}
+
+export const UserSearch = ({ searchState, onUserSearch, onAddMember }) => {
+    let [searchInput, setSearchInput] = useState("")
+    let [users, setUsers] = useState([])
+
+    useEffect(() => {
+        searchInput && setUsers(searchState)
+    }, [searchState])
+
+    let searchHandler = async (e) => {
+        setSearchInput(e.target.value)
+
+        if (e.target.value) {
+            onUserSearch(e.target.value)
+        } else {
+            setUsers([])
+        }
+    }
+
+    return (
+        <div className="user-search-panel">
+            <span>Добавьте пользователей</span>
+            <input onInput={e => searchHandler(e)} value={searchInput} placeholder="Введите имя пользователя..." />
+            {searchInput && users && users.length > 0 && <UserList users={users} onAddMember={onAddMember} />}
+            {searchState && searchInput && users && !users.length && <span>Пользователей с таким именем не существует</span>}
+        </div>
+    )
+}
+
+export const MemberList = ({ members, onDeleteMember }) => {
+    return (
+        <div className="chat-member-list">
+            <h4>Пользователи чата</h4>
+            <ul>
+                {members.map(m =>
+                    <li key={m._id}>
+                        <span>{m.nick || m.login}</span>
+                        {onDeleteMember && <button onClick={() => onDeleteMember(m)}>x</button>}
+                    </li>
+                )}
+            </ul>
+        </div>
+    )
+}
+
+const ChatForm = ({ searchState, onUserSearch, onNewChat }) => {
+    let [title, setTitle] = useState("")
+    let [members, setMembers] = useState([])
+
+    let userAddHandler = (user) => {
+        members.length > 0 ? setMembers([...members, user]) : setMembers([user])
+    }
+
+    let memberDeleteHandler = (user) => {
+        setMembers(members.filter(member => member._id !== user._id))
+    }
+
+    return (
+        <div className="new-chat">
+            <span>Выберите чат или создайте новый</span>
+            <input onInput={e => setTitle(e.target.value)} value={title} placeholder="Введите название чата" />
+            <UserSearch searchState={searchState} onUserSearch={onUserSearch} onAddMember={userAddHandler} />
+            {members && members.length > 0 && <MemberList members={members} onDeleteMember={memberDeleteHandler} />}
+            <button onClick={() => onNewChat(title, members)} disabled={!title || !members.length}>Создать чат</button>
+        </div>
+    )
+}
+
+const ConnectChatForm = connect(state => ({ searchState: state.promise?.userSearch?.payload?.data?.UserFind }),
+    {
+        onUserSearch: actionUserSearch,
+        onNewChat: actionAddChatBack
+    })(ChatForm)
+
+export default ConnectChatForm

+ 46 - 0
src/components/chatWindow.js

@@ -0,0 +1,46 @@
+import { useState } from "react"
+import { Message } from "./message"
+import { MyDropzone } from "./dropzone"
+import ScrollableFeed from 'react-scrollable-feed'
+import { Link } from "react-router-dom"
+
+const Chat = ({ chat_id, chat_title, chat_avatar, user_id, messages, onUpload, onSend }) => {
+    let [msg, setMSG] = useState("")
+    let [isUpload, setIsUpload] = useState(false)
+    let [files, setFiles] = useState([])
+    console.log(files)
+    let filesHandler = (file) => {
+        files.length ? setFiles([...files, file]) : setFiles([file])
+    }
+
+    let handler = (e) => {
+        if ((e.key === "Enter" && !e.shiftKey) || e.type === "click") {
+            onSend(chat_id, msg, files)
+            setMSG("")
+            e.preventDefault()
+        }
+    }
+
+    return (
+        <div className="chat-window">
+            <div className="chat-nav">
+                <span>{chat_title}</span>
+            </div>
+
+            <ScrollableFeed className="message-list" forceScroll>
+                {messages.length > 0 && messages.map(message => <Message key={message._id} id={message._id} nick={message.owner.nick || message.owner.login} msg={message.text} date={message.createdAt} modified={message.modified} own={message.owner._id === user_id} />)}
+            </ScrollableFeed>
+
+            <div className="message-input">
+                <textarea placeholder="Введите сообщение..." value={msg} onInput={(e) => setMSG(e.target.value)} onKeyDown={(e) => handler(e)} />
+                <button onClick={() => setIsUpload(!isUpload)}>Прикрепить файл</button>
+                {isUpload && <MyDropzone onUpload={onUpload} onSet={filesHandler} />}
+                <button onClick={(e) => handler(e)}>Отправить</button>
+            </div>
+        </div>
+    )
+}
+
+
+
+export default Chat

+ 40 - 0
src/components/chatsList.js

@@ -0,0 +1,40 @@
+import { Link } from "react-router-dom"
+import ReactTimeAgo from 'react-time-ago'
+import TimeAgo from 'javascript-time-ago'
+import ru from 'javascript-time-ago/locale/ru.json'
+import React from 'react';
+
+TimeAgo.addLocale(ru)
+
+const ChatItem = ({ id, chat }) => {
+
+    return (
+        <li className="aside-chat" >
+            <Link to={`/chat_id.${id}`}>
+                {chat.avatar && <img src={chat.avatar.url} alt="chat-avatar" />}
+                <div className="aside-chat-info">
+
+                    <h5>{chat.title}</h5>
+                    {/* {chat.messages && chat.messages.length ? <span>{chat.messages[chat.messages.length - 1].owner.login + ": " + chat.messages[chat.messages.length - 1].text}</span> : <span>Пусто</span>} */}
+                    <span>Последнее обновление: </span>
+                    {chat.lastModified && <ReactTimeAgo date={+(chat.lastModified)} locale="ru" timeStyle="round" />}
+                </div>
+            </Link>
+        </li>
+    )
+}
+
+const ChatsList = ({ chats, onChat }) => {
+    return (
+        <>
+            <ul className="chat-list">
+                <li className="aside-chat-btn">
+                    <Link to="/chat">Новый чат</Link>
+                </li>
+                {Object.entries(chats).map(([id, chat]) => <ChatItem key={id} id={id} chat={chat} />)}
+            </ul>
+        </>
+    )
+}
+
+export default ChatsList

+ 37 - 0
src/components/dropzone.js

@@ -0,0 +1,37 @@
+import { useCallback } from 'react';
+import { useDropzone } from 'react-dropzone';
+import { connect } from 'react-redux';
+import { actionUploadFile } from '../actions';
+
+export const MyDropzone = ({ maxFiles, onUpload, onSet }) => {
+    const onDrop = useCallback(acceptedFiles => {
+        acceptedFiles.forEach(async (file) => {
+            let result = await onUpload(file)
+            onSet(result)
+        })
+    }, [onUpload])
+
+    const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ onDrop, maxFiles: maxFiles })
+
+    const files = acceptedFiles.map(file => (
+        <li key={file.path}>
+            {file.path} - {file.size} bytes
+        </li>
+    ))
+
+    return (
+        <div>
+            <div {...getRootProps()}>
+                <input {...getInputProps()} />
+                <p>Drag 'n' drop some files here, or click to select files</p>
+                {maxFiles && <span>Max files: {maxFiles}</span>}
+            </div>
+            <aside>
+                <h4>Files</h4>
+                <ul>{files}</ul>
+            </aside>
+        </div>
+    )
+}
+
+export const ConnectDropzone = connect(null, { onUpload: actionUploadFile })(MyDropzone)

+ 14 - 0
src/components/loginForm.js

@@ -0,0 +1,14 @@
+import { useState } from 'react';
+
+export const LoginForm = ({ onLogin }) => {
+    const [login, setLogin] = useState("")
+    const [password, setPassword] = useState("")
+
+    return (
+        <div className="login-form">
+            <input value={login} onChange={e => setLogin(e.target.value)} />
+            <input type='password' value={password} onChange={e => setPassword(e.target.value)} />
+            <button disabled={login === "" || password === ""} onClick={() => onLogin(login, password)}>Login</button>
+        </div>
+    )
+}

+ 27 - 0
src/components/message.js

@@ -0,0 +1,27 @@
+import ReactTimeAgo from 'react-time-ago'
+import TimeAgo from 'javascript-time-ago'
+import ru from 'javascript-time-ago/locale/ru.json'
+
+TimeAgo.addLocale(ru)
+let handler = (e) => {
+
+    console.log(e)
+    e.preventDefault()
+}
+
+export const Message = ({ nick, msg, date, media, modified, own = false, replies }) => {
+
+    return (
+        <li className={own ? "msg-user" : "msg-someone"} onContextMenu={(e) => handler(e)}>
+            {media && media.length && <div>
+                <span>Прикрепленные файлы</span>
+                {media.map(file => <a href={"/" + file.url}>{file}</a>)}
+            </div>}
+            <span className="msg-nick">{nick}</span>
+            <span className="msg-text">{msg}</span>
+            <ReactTimeAgo className="msg-date" date={+date} locale="ru" timeStyle="round" />
+            {modified && <span className="mutated-msg">Сообщение изменено</span>}
+            {replies && replies.map(repMSG => <Message nick={repMSG.login} msg={repMSG.text} date={repMSG.createdAt} />)}
+        </li>
+    )
+}

+ 18 - 0
src/components/registrationForm.js

@@ -0,0 +1,18 @@
+import { useState } from 'react';
+
+export const RegistrationForm = ({ onRegistration }) => {
+    const [login, setLogin] = useState("")
+    const [password, setPassword] = useState("")
+    const [passwordValidation, setPasswordValidation] = useState("")
+    const validation = new RegExp(`^(?=.*[0-9]).{${4},}$`)
+
+    return (
+        <div className="reg-form">
+            <span>password must be at least 4 characters long and contain a number </span>
+            <input value={login} onChange={e => setLogin(e.target.value)} />
+            <input type='password' value={password} onChange={e => setPassword(e.target.value)} />
+            <input type='password' value={passwordValidation} onChange={e => setPasswordValidation(e.target.value)} />
+            <button disabled={!(password === passwordValidation && validation.test(password))} onClick={() => onRegistration(login, password)}>Registration</button>
+        </div>
+    )
+}

+ 0 - 0
src/pages/404.js


+ 0 - 0
src/pages/login.js


+ 111 - 0
src/pages/main.js

@@ -0,0 +1,111 @@
+import Chat from "../components/chatWindow";
+import ChatsList from "../components/chatsList";
+import { useEffect } from "react";
+import io from 'socket.io-client';
+import { connect } from "react-redux";
+import { actionAddChat, actionAddChatBack, actionAddMSG, actionAddMSGBack, actionEditChat, actionEditMSG, actionFullEditChat, actionFullGetChats, actionFullGetMessages, actionUploadFile, actionUserSearch } from "../actions";
+import ConnectChatForm from "../components/chatForm";
+import { ChatEditForm } from "../components/chatEditForm";
+
+//все в куче
+const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages, addChat, editChat, addMSG, editMSG, sendMSG, search, userSearch, changeChat, addFile }) => {
+    let chat_id = _id.split(".")[1]
+    useEffect(() => {
+        getChat(userID)
+        console.log("монтируем")
+
+        const socket = io("ws://chat.fs.a-level.com.ua")
+
+        socket.emit("jwt", localStorage.authToken)
+
+        socket.on("jwt_ok", data => console.log("С JWT все норм", data))
+
+        socket.on("jwt_fail", error => console.log("С JWT траблы", error))
+
+        socket.on("connect", () => {
+            // getChat(userID)
+            console.log("Законектились, сокет: ", socket.id)
+        });
+
+        socket.on("connect_error", error => console.log("ошибка конекта", error));
+
+        socket.on("reconnect_attempt", () => console.log("пробуем реконектнуть"));
+
+        socket.on("disconnect", () => console.log("дисконект", socket.id));
+
+        socket.on('msg', msg => {
+            console.log("это пришло по сокету (сообщение)", msg)
+
+            if (chats) {
+                let check = chats[msg.chat._id]["messages"].filter(({ _id }) => _id === msg._id).length
+                console.log(check)
+                if (check) {
+                    editMSG(msg.chat._id, msg._id, msg.text, msg.media)
+                } else {
+                    addMSG(msg.chat._id, msg._id, msg.text, msg.createdAt, msg.owner, msg.media)
+                }
+            }
+        })
+
+        socket.on('chat', chat => {
+            console.log("это пришло по сокету (чат)", chat)
+            let check = Object.keys(chats).filter(id => id === chat._id).length
+            console.log(check)
+            if (check) {
+                editChat(chat._id, chat.title, chat.avatar, chat.members)
+            } else {
+                addChat(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.owner, chat.avatar, chat.messages, chat.members)
+            }
+        })
+
+        return () => {
+            console.log("размонтировали")
+        }
+    }, [])
+
+    useEffect(() => {
+        chat_id && getMessages(chat_id)
+    }, [_id])
+
+    return (
+        <main>
+            <ChatsList chats={chats} />
+            {_id === "chat" && <ConnectChatForm />}
+
+            {chats[chat_id] &&
+                <Chat chat_id={chat_id}
+                    chat_title={chats[chat_id].title}
+                    chat_avatar={chats[chat_id].avatar?.url}
+                    user_id={userID}
+                    messages={chats[chat_id].messages || []}
+                    onUpload={addFile}
+                    onSend={sendMSG} />}
+
+
+            {chats[chat_id] && <ChatEditForm chat_id={chat_id}
+                chat={chats[chat_id]}
+                searchState={search}
+                onUserSearch={userSearch}
+                onChangeChat={changeChat}
+                onUpload={addFile} />}
+        </main>
+    )
+}
+
+export const ConnectMain = connect(state => ({
+    userID: state.auth.payload.sub.id, chats: state.chat,
+    search: state.promise?.userSearch?.payload?.data?.UserFind
+}),
+    {
+        getChat: actionFullGetChats,
+        getMessages: actionFullGetMessages,
+        addChat: actionAddChat,
+        editChat: actionEditChat,
+        addMSG: actionAddMSG,
+        editMSG: actionEditMSG,
+        sendMSG: actionAddMSGBack,
+        userSearch: actionUserSearch,
+        newChat: actionAddChatBack,
+        changeChat: actionFullEditChat,
+        addFile: actionUploadFile
+    })(Main)

+ 0 - 0
src/pages/registration.js


+ 24 - 0
src/reducers/auth.js

@@ -0,0 +1,24 @@
+import { decode as atob } from 'base-64';
+
+function authReducer(state, { type, token }) {
+    if (state === undefined) {
+        if (localStorage.authToken) {
+            type = "LOGIN"
+            token = localStorage.authToken
+        } else {
+            return {}
+        }
+    }
+
+    if (type === "LOGIN") {
+        localStorage.authToken = token
+        return { token, payload: JSON.parse(atob(token.split(".")[1])) }
+    }
+    if (type === "LOGOUT") {
+        localStorage.removeItem("authToken")
+        return {}
+    }
+    return state
+}
+
+export default authReducer

+ 79 - 0
src/reducers/chat.js

@@ -0,0 +1,79 @@
+function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified, avatar, members, messages, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replies }) {
+    if (type === 'ADD_CHAT') {
+        return {
+            ...state,
+
+            [chat_id]: {
+                title,
+                createdAt,
+                lastModified,
+                avatar,
+                messages,
+                members
+            }
+        }
+    }
+
+    if (type === "EDIT_CHAT") {
+        return {
+            ...state,
+
+            [chat_id]: {
+                title: title || state[chat_id].title,
+                createdAt: state[chat_id].createdAt,
+                lastModified: state[chat_id].lastModified,
+                avatar: avatar || state[chat_id].avatar,
+                messages: state[chat_id].messages,
+                members: members || state[chat_id].members
+            }
+        }
+    }
+
+    if (type === "ADD_MESSAGE") {
+        if ((state[chat_id]?.messages?.filter(msg => msg._id === msg_id).length === 0)) {
+            return {
+                ...state,
+
+                [chat_id]: {
+                    title: state[chat_id].title,
+                    createdAt: state[chat_id].createdAt,
+                    lastModified: msg_createdAt,
+                    avatar: state[chat_id].avatar,
+                    messages: [...state[chat_id].messages, { _id: msg_id, text: msg_text, createdAt: msg_createdAt, owner: msg_owner, media: msg_media, replies: msg_replies }],
+                    members: state[chat_id].members
+                }
+            }
+        }
+    }
+
+    if (type === "EDIT_MESSAGE") {
+        let order;
+
+        for (let key in state[chat_id].messages) {
+            if (state[chat_id].messages[key]._id === messages[0]._id) {
+                order = key
+            }
+        }
+
+        return {
+            ...state,
+
+            [chat_id]: {
+                title: state[chat_id].title,
+                createdAt: state[chat_id].createdAt,
+                lastModified,
+                avatar: state[chat_id].avatar,
+                messages: [
+                    ...state[chat_id].messages,
+
+                    state[chat_id].messages[order].text = msg_text,
+                    state[chat_id].messages[order].modified = true
+                ],
+                members: state[chat_id].members
+            }
+        }
+    }
+    return state
+}
+
+export default chatReducer

+ 15 - 0
src/reducers/index.js

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

+ 11 - 0
src/reducers/promise.js

@@ -0,0 +1,11 @@
+function promiseReducer(state = {}, { type, status, payload, error, name }) {
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: { status, payload, error }
+        }
+    }
+    return state
+}
+
+export default promiseReducer