├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.json ├── client ├── .eslintcache ├── .gitignore ├── README.md ├── build │ ├── asset-manifest.json │ ├── avatars │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ ├── favicon.ico │ ├── index.html │ ├── robots.txt │ ├── static │ │ ├── css │ │ │ ├── 2.4c97ca4f.chunk.css │ │ │ ├── 2.4c97ca4f.chunk.css.map │ │ │ ├── main.aa073b9c.chunk.css │ │ │ └── main.aa073b9c.chunk.css.map │ │ └── js │ │ │ ├── 2.cddb4d36.chunk.js │ │ │ ├── 2.cddb4d36.chunk.js.LICENSE.txt │ │ │ ├── 2.cddb4d36.chunk.js.map │ │ │ ├── main.0f170182.chunk.js │ │ │ ├── main.0f170182.chunk.js.map │ │ │ ├── runtime-main.03887cff.js │ │ │ └── runtime-main.03887cff.js.map │ └── welcome-back.png ├── package.json ├── public │ ├── avatars │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ ├── favicon.ico │ ├── index.html │ ├── robots.txt │ └── welcome-back.png ├── src │ ├── App.jsx │ ├── api.js │ ├── components │ │ ├── Chat │ │ │ ├── components │ │ │ │ ├── ChatList │ │ │ │ │ ├── components │ │ │ │ │ │ ├── AvatarImage.jsx │ │ │ │ │ │ ├── ChatIcon.jsx │ │ │ │ │ │ ├── ChatListItem │ │ │ │ │ │ │ ├── index.jsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── Footer.jsx │ │ │ │ │ └── index.jsx │ │ │ │ ├── MessageList │ │ │ │ │ ├── components │ │ │ │ │ │ ├── ClockIcon.jsx │ │ │ │ │ │ ├── InfoMessage.jsx │ │ │ │ │ │ ├── MessagesLoading.jsx │ │ │ │ │ │ ├── NoMessages.jsx │ │ │ │ │ │ ├── ReceiverMessage.jsx │ │ │ │ │ │ └── SenderMessage.jsx │ │ │ │ │ └── index.jsx │ │ │ │ ├── OnlineIndicator.jsx │ │ │ │ └── TypingArea.jsx │ │ │ └── index.jsx │ │ ├── LoadingScreen.jsx │ │ ├── Login │ │ │ ├── index.jsx │ │ │ └── style.css │ │ ├── Logo.jsx │ │ └── Navbar.jsx │ ├── hooks.js │ ├── index.jsx │ ├── state.js │ ├── styles │ │ ├── font-face.css │ │ ├── style-overrides.css │ │ └── style.css │ ├── utils.js │ └── websockets │ │ ├── config.js │ │ ├── data.js │ │ ├── messages.js │ │ ├── process.js │ │ ├── storage.js │ │ └── view.js └── yarn.lock ├── config └── config.go ├── docker-compose.yaml ├── docs ├── YTThumbnail.png ├── screenshot000.png └── screenshot001.png ├── go.mod ├── go.sum ├── golang-chat-redis.service ├── heroku.yml ├── images └── app_preview_image.png ├── main.go ├── marketplace.json ├── message ├── controller.go ├── error.go ├── message.go ├── message_channel_join.go ├── message_channel_leave.go ├── message_channel_message.go ├── message_error.go ├── message_ready.go ├── message_signin.go ├── message_signout.go ├── message_signup.go ├── message_sys.go └── message_users.go ├── rediscli ├── channel.go ├── channel_test.go ├── connection.go ├── redis.go ├── user.go └── user_test.go └── websocket ├── README.md ├── connection.go ├── error.go └── websocket.go /.env.example: -------------------------------------------------------------------------------- 1 | SERVER_ADDRESS=:5555 2 | CLIENT_LOCATION=/api/public 3 | REDIS_HOST=chat-redis 4 | REDIS_ADDRESS=:6379 5 | REDIS_PASSWORD= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang as builder 2 | 3 | RUN mkdir /build 4 | 5 | COPY . /build/ 6 | 7 | WORKDIR /build 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux go build -o bin . 10 | 11 | FROM golang 12 | 13 | ENV PORT=$PORT 14 | ENV SERVER_ADDRESS=:5000 15 | ENV CLIENT_LOCATION=/api/public 16 | ENV REDIS_ADDRESS=:6379 17 | ENV REDIS_PASSWORD="" 18 | 19 | RUN mkdir /api 20 | 21 | WORKDIR /build 22 | 23 | COPY --from=builder /build/bin /api/ 24 | COPY client/build /api/public 25 | 26 | WORKDIR /api 27 | 28 | LABEL Name="Chat Api" 29 | 30 | #Run service 31 | ENTRYPOINT ["./bin"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Redis Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dc-build: 2 | docker-compose build 3 | dc-start: 4 | docker-compose up -d 5 | dc-stop: 6 | docker-compose down 7 | dc-logs: 8 | docker-compose logs -f 9 | test: 10 | docker run --name chat-redis-test -d --rm -p 60001:6379 redis:5 11 | go test -v ./... 12 | docker stop chat-redis-test -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic Redis Leaderboard Demo Golang", 3 | "description": "List of top 100 companies", 4 | "stack": "container", 5 | "repository": "https://github.com/redis-developer/basic-redis-chat-demo-go", 6 | "logo": "https://redis.io/images/redis-white.png", 7 | "keywords": ["golang", "gin", "redis", "chat"], 8 | "addons": ["rediscloud:30"], 9 | "env": { 10 | "REDIS_ADDRESS": { 11 | "description": "Redis server address host:port", 12 | "required": true 13 | }, 14 | "REDIS_PASSWORD": { 15 | "description": "Redis server password", 16 | "required": true 17 | }, 18 | "CLIENT_LOCATION": { 19 | "description": "Public path to frontend, as example: `/api/public`" 20 | }, 21 | "SERVER_ADDRESS": { 22 | "description": "Api public host:port, as example: `:5000`" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | 5 | ``` 6 | yarn install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | yarn start 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | yarn build 19 | ``` 20 | -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.aa073b9c.chunk.css", 4 | "main.js": "/static/js/main.0f170182.chunk.js", 5 | "main.js.map": "/static/js/main.0f170182.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.03887cff.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.03887cff.js.map", 8 | "static/css/2.4c97ca4f.chunk.css": "/static/css/2.4c97ca4f.chunk.css", 9 | "static/js/2.cddb4d36.chunk.js": "/static/js/2.cddb4d36.chunk.js", 10 | "static/js/2.cddb4d36.chunk.js.map": "/static/js/2.cddb4d36.chunk.js.map", 11 | "index.html": "/index.html", 12 | "static/css/2.4c97ca4f.chunk.css.map": "/static/css/2.4c97ca4f.chunk.css.map", 13 | "static/css/main.aa073b9c.chunk.css.map": "/static/css/main.aa073b9c.chunk.css.map", 14 | "static/js/2.cddb4d36.chunk.js.LICENSE.txt": "/static/js/2.cddb4d36.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.03887cff.js", 18 | "static/css/2.4c97ca4f.chunk.css", 19 | "static/js/2.cddb4d36.chunk.js", 20 | "static/css/main.aa073b9c.chunk.css", 21 | "static/js/main.0f170182.chunk.js" 22 | ] 23 | } -------------------------------------------------------------------------------- /client/build/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/0.jpg -------------------------------------------------------------------------------- /client/build/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/1.jpg -------------------------------------------------------------------------------- /client/build/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/10.jpg -------------------------------------------------------------------------------- /client/build/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/11.jpg -------------------------------------------------------------------------------- /client/build/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/12.jpg -------------------------------------------------------------------------------- /client/build/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/2.jpg -------------------------------------------------------------------------------- /client/build/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/3.jpg -------------------------------------------------------------------------------- /client/build/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/4.jpg -------------------------------------------------------------------------------- /client/build/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/5.jpg -------------------------------------------------------------------------------- /client/build/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/6.jpg -------------------------------------------------------------------------------- /client/build/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/7.jpg -------------------------------------------------------------------------------- /client/build/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/8.jpg -------------------------------------------------------------------------------- /client/build/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/avatars/9.jpg -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | Node.JS Redis chat
-------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/build/static/css/main.aa073b9c.chunk.css: -------------------------------------------------------------------------------- 1 | :root{--primary:#556ee6!important;--light:#f5f5f8!important;--success:#34c38f!important}.bg-success{background-color:#34c38f!important;background-color:var(--success)!important}.bg-light{background-color:#f5f5f8!important;background-color:var(--light)!important}.bg-gray{background-color:var(--gray)!important}.bg-primary{background-color:#556ee6!important;background-color:var(--primary)!important}.text-primary{color:#556ee6!important;color:var(--primary)!important}.list-group-item.active{background-color:#556ee6!important;background-color:var(--primary)!important;border-color:#556ee6!important;border-color:var(--primary)!important}.btn-rounded{border-radius:30px!important}.btn{display:inline-block;font-weight:400;color:#495057;text-align:center;vertical-align:middle;-webkit-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;border-radius:30px!important;padding:.47rem .75rem;font-size:.8125rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn-primary{color:#fff;background-color:#556ee6;background-color:var(--primary);border-color:#556ee6;border-color:var(--primary)}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#3452e1;border-color:#2948df}.font-size-14{font-size:14px!important}.font-size-11{font-size:11px!important}.font-size-12{font-size:12px!important}.font-size-15{font-size:15px!important}.w-md{min-width:110px}body{font-family:"Poppins",Arial,Helvetica,sans-serif;font-size:13px;color:#495057}.navbar{box-shadow:0 12px 24px 0 rgba(18,38,63,.03)}.navbar-brand{font-size:16px}.chats-title{padding-left:14px}.login-page{display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-flex-direction:column;flex-direction:column;-webkit-justify-content:center;justify-content:center;padding-bottom:190px;height:100vh}.form-signin{width:100%;max-width:330px;padding:15px;margin:0 auto}.text-small{font-size:.9rem}.chat-box,.messages-box{width:100%}.chat-box-wrapper{-webkit-flex:1 1;flex:1 1;overflow-y:scroll}.rounded-lg{border-radius:.5rem}input::-webkit-input-placeholder{font-size:.9rem;color:#999}input:-ms-input-placeholder{font-size:.9rem;color:#999}input::placeholder{font-size:.9rem;color:#999}.centered-box{width:100%;height:100vh;display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}.login-error-anchor{position:relative}.toast-box{text-align:left;margin-top:30px;position:absolute;width:100%;top:0;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:center;justify-content:center}.full-height,.toast-box{display:-webkit-flex;display:flex}.full-height{height:100vh;-webkit-flex-direction:column;flex-direction:column}.full-height .container{-webkit-flex:1 1;flex:1 1}.container .row{height:100%}.flex-column{display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column}.bg-white.flex-column{height:100%}.flex{-webkit-flex:1 1;flex:1 1}.logout-button{cursor:pointer;display:-webkit-flex;display:flex;-webkit-flex-direction:row;flex-direction:row;-webkit-align-items:center;align-items:center;padding:15px 20px}.logout-button svg{margin-right:15px}.no-messages{opacity:.5;height:100%;width:100%}.avatar-box{width:50px;height:50px;object-fit:cover;object-position:50%;overflow:hidden;border-radius:4px}.avatar-box,.user-link{cursor:pointer}.user-link:hover{text-decoration:underline}.online-indicator{width:14px;height:14px;border:2px solid #fff;bottom:-7px;right:-7px;background-color:#4df573}.online-indicator.selected{border:none;width:12px;height:12px;bottom:-5px;right:-5px}.online-indicator.offline{background-color:#bbb}span.pseudo-link{font-size:14px;text-decoration:underline;color:var(--blue);cursor:pointer}span.pseudo-link:hover{text-decoration:none}.list-group-item{cursor:pointer;height:70px;box-sizing:border-box;transition:background-color .1s ease-out}.chat-icon{width:45px;height:45px;border-radius:4px;background-color:#eee}.chat-icon.active{background-color:var(--blue)}.chats-title{font-size:15px}.chat-body{border-radius:10px!important}.chat-list-container{height:100%}.chat-input{border-radius:30px!important;background-color:#eff2f7!important;border-color:#eff2f7!important;padding-right:120px}.form-control::-webkit-input-placeholder{font-size:13px}.form-control:-ms-input-placeholder{font-size:13px}.form-control::placeholder{font-size:13px}.form-control{display:block;width:100%;height:calc(1.5em + .94rem + 2px);padding:7.5px 12px;font-size:13px;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.rounded-button{border-radius:30px;background-color:var(--light)}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}.login-form .username-select button{background-color:transparent!important;color:inherit;padding:7.5px 12px!important;display:block!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important}.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:inherit}.login-form .username-select .dropdown-menu.show.dropdown-menu-right{-webkit-transform:translateY(38px)!important;transform:translateY(38px)!important}.username-select-dropdown{position:relative;display:-webkit-flex!important;display:flex!important;-webkit-align-items:center;align-items:center;background-color:transparent!important;color:inherit;padding:0 12px!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important;height:calc(1.5em + .94rem + 2px)!important;cursor:pointer}.username-select-dropdown .username-select-block{background-color:var(--white);position:absolute;top:-1138px;left:0;opacity:0;-webkit-transform:scale(.5);transform:scale(.5);-webkit-transform-origin:top left;transform-origin:top left;transition:opacity .2s ease,-webkit-transform .2s ease;transition:opacity .2s ease,transform .2s ease;transition:opacity .2s ease,transform .2s ease,-webkit-transform .2s ease;border:1px solid #ced4da!important;border-radius:4px!important;padding:8px 0}.username-select-dropdown:focus{outline:none;border-color:#80bdff!important;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.username-select-dropdown .username-select-block.open{top:42px;-webkit-transform:scale(1);transform:scale(1);opacity:1}.username-select-row{display:-webkit-flex;display:flex;width:100%;-webkit-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;align-items:center}.username-select-dropdown .username-select-block .username-select-block-item{padding:4px 24px}.username-select-dropdown .username-select-block .username-select-block-item:hover{background-color:var(--light)}.chat-list-item{cursor:pointer;padding:14px 16px}.mdi-circle:before{content:"󰝥"}.mdi-set,.mdi:before{display:inline-block;font:normal normal normal 24px/1 Material Design Icons;font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} 2 | /*# sourceMappingURL=main.aa073b9c.chunk.css.map */ -------------------------------------------------------------------------------- /client/build/static/css/main.aa073b9c.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/styles/style-overrides.css","webpack://src/styles/style.css","webpack://src/styles/font-face.css","webpack://src/components/Login/style.css","webpack://src/components/Chat/components/ChatList/components/ChatListItem/style.css"],"names":[],"mappings":"AAAA,MACE,2BAAuC,CACvC,yBAA2B,CAC3B,2BACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,UACE,kCAAyC,CAAzC,uCACF,CAEA,SACE,sCACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,cACE,uBAAgC,CAAhC,8BACF,CAEA,wBACE,kCAA2C,CAA3C,yCAA2C,CAC3C,8BAAuC,CAAvC,qCACF,CAEA,aACE,4BACF,CAEA,KACE,oBAAqB,CACrB,eAAgB,CAChB,aAAc,CACd,iBAAkB,CAClB,qBAAsB,CACtB,wBAAiB,CAAjB,oBAAiB,CAAjB,gBAAiB,CACjB,4BAA6B,CAC7B,4BAA6B,CAC7B,4BAA8B,CAC9B,qBAAwB,CACxB,kBAAoB,CACpB,eAAgB,CAChB,oBAAsB,CAGtB,6HAKF,CAEA,aACE,UAAW,CACX,wBAAgC,CAAhC,+BAAgC,CAChC,oBAA4B,CAA5B,2BACF,CAEA,yDAGE,UAAW,CACX,wBAAyB,CACzB,oBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,MACE,eACF,CC1FA,KACE,gDAAoD,CACpD,cAAe,CACf,aACF,CAEA,QACE,2CACF,CAEA,cACE,cACF,CAEA,aACE,iBACF,CAEA,YACE,oBAAa,CAAb,YAAa,CACb,0BAAmB,CAAnB,kBAAmB,CACnB,6BAAsB,CAAtB,qBAAsB,CACtB,8BAAuB,CAAvB,sBAAuB,CACvB,oBAAqB,CACrB,YACF,CAEA,aACE,UAAW,CACX,eAAgB,CAChB,YAAa,CACb,aACF,CAEA,YACE,eACF,CAEA,wBAGE,UACF,CAEA,kBACE,gBAAO,CAAP,QAAO,CACP,iBACF,CAEA,YACE,mBACF,CAEA,iCACE,eAAiB,CACjB,UACF,CAHA,4BACE,eAAiB,CACjB,UACF,CAHA,mBACE,eAAiB,CACjB,UACF,CAEA,cACE,UAAW,CACX,YAAa,CACb,oBAAa,CAAb,YAAa,CACb,0BAAmB,CAAnB,kBAAmB,CACnB,8BAAuB,CAAvB,sBACF,CAEA,oBACE,iBACF,CAEA,WACE,eAAgB,CAChB,eAAgB,CAChB,iBAAkB,CAClB,UAAW,CACX,KAAM,CAEN,0BAAmB,CAAnB,kBAAmB,CACnB,8BAAuB,CAAvB,sBACF,CAEA,wBALE,oBAAa,CAAb,YASF,CAJA,aACE,YAAa,CACb,6BAAsB,CAAtB,qBAEF,CAEA,wBACE,gBAAO,CAAP,QACF,CAEA,gBACE,WACF,CAEA,aACE,oBAAa,CAAb,YAAa,CACb,6BAAsB,CAAtB,qBACF,CAEA,sBACE,WACF,CAEA,MACE,gBAAO,CAAP,QACF,CAEA,eACE,cAAe,CACf,oBAAa,CAAb,YAAa,CACb,0BAAmB,CAAnB,kBAAmB,CACnB,0BAAmB,CAAnB,kBAAmB,CACnB,iBACF,CAEA,mBACE,iBACF,CAEA,aACE,UAAY,CACZ,WAAY,CACZ,UACF,CAEA,YACE,UAAW,CACX,WAAY,CACZ,gBAAiB,CACjB,mBAAoB,CACpB,eAAgB,CAChB,iBAEF,CAEA,uBAHE,cAKF,CAEA,iBACE,yBACF,CAEA,kBACE,UAAW,CACX,WAAY,CACZ,qBAAuB,CACvB,WAAY,CACZ,UAAW,CACX,wBACF,CAEA,2BACE,WAAY,CACZ,UAAW,CACX,WAAY,CACZ,WAAY,CACZ,UACF,CAEA,0BACE,qBACF,CAEA,iBACE,cAAe,CACf,yBAA0B,CAC1B,iBAAkB,CAClB,cACF,CAEA,uBACE,oBACF,CAEA,iBACE,cAAe,CACf,WAAY,CACZ,qBAAsB,CACtB,wCACF,CAEA,WACE,UAAW,CACX,WAAY,CACZ,iBAAkB,CAClB,qBACF,CAEA,kBACE,4BACF,CAEA,aACE,cACF,CAEA,WACE,4BACF,CAEA,qBACE,WACF,CAEA,YACE,4BAA8B,CAC9B,kCAAoC,CACpC,8BAAgC,CAChC,mBACF,CAEA,yCACE,cACF,CAFA,oCACE,cACF,CAFA,2BACE,cACF,CAEA,cACE,aAAc,CACd,UAAW,CACX,iCAAmC,CACnC,kBAAmB,CACnB,cAAe,CACf,eAAgB,CAChB,eAAgB,CAChB,aAAc,CACd,qBAAsB,CACtB,2BAA4B,CAC5B,wBAAyB,CAKzB,oEAGF,CAEA,gBACE,kBAAmB,CACnB,6BACF,CChPA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,kGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CCzKA,oCACE,sCAAwC,CACxC,aAAc,CACd,4BAA8B,CAC9B,uBAAyB,CACzB,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBACF,CAEA,iMAGE,aACF,CAEA,qEACE,4CAA0C,CAA1C,oCACF,CAEA,0BACE,iBAAkB,CAClB,8BAAwB,CAAxB,sBAAwB,CACxB,0BAAmB,CAAnB,kBAAmB,CACnB,sCAAwC,CACxC,aAAc,CACd,wBAA0B,CAC1B,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBAA2B,CAC3B,2CAA8C,CAE9C,cACF,CAEA,iDACE,6BAA8B,CAC9B,iBAAkB,CAClB,WAAY,CACZ,MAAO,CACP,SAAU,CACV,2BAA0B,CAA1B,mBAA0B,CAC1B,iCAA0B,CAA1B,yBAA0B,CAC1B,sDAAkD,CAAlD,8CAAkD,CAAlD,yEAAkD,CAElD,kCAA+C,CAC/C,2BAA6B,CAE7B,aACF,CAEA,gCACE,YAAa,CACb,8BAAgC,CAChC,0CACF,CAEA,sDACE,QAAS,CACT,0BAAsB,CAAtB,kBAAsB,CACtB,SACF,CAEA,qBACE,oBAAa,CAAb,YAAa,CACb,UAAW,CACX,qCAA8B,CAA9B,6BAA8B,CAC9B,0BAAmB,CAAnB,kBACF,CAEA,6EACE,gBACF,CAEA,mFAGE,6BACF,CChFA,gBACE,cAAe,CACf,iBACF,CACA,mBACE,YACF,CAEA,qBAEE,oBAAqB,CACrB,sDAAuD,CACvD,iBAAkB,CAClB,mBAAoB,CACpB,mBAAoB,CACpB,kCAAmC,CACnC,iCACF","file":"main.aa073b9c.chunk.css","sourcesContent":[":root {\n --primary: rgb(85, 110, 230) !important;\n --light: #f5f5f8 !important;\n --success: rgb(52, 195, 143) !important;\n}\n\n.bg-success {\n background-color: var(--success) !important;\n}\n\n.bg-light {\n background-color: var(--light) !important;\n}\n\n.bg-gray {\n background-color: var(--gray) !important;\n}\n\n.bg-primary {\n background-color: var(--primary) !important;\n}\n\n.text-primary {\n color: var(--primary) !important;\n}\n\n.list-group-item.active {\n background-color: var(--primary) !important;\n border-color: var(--primary) !important;\n}\n\n.btn-rounded {\n border-radius: 30px !important;\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #495057;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 30px !important;\n padding: 0.47rem 0.75rem;\n font-size: 0.8125rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\n -webkit-box-shadow 0.15s ease-in-out;\n}\n\n.btn-primary {\n color: #fff;\n background-color: var(--primary);\n border-color: var(--primary);\n}\n\n.btn-primary.focus,\n.btn-primary:focus,\n.btn-primary:hover {\n color: #fff;\n background-color: #3452e1;\n border-color: #2948df;\n}\n\n.font-size-14 {\n font-size: 14px !important;\n}\n\n.font-size-11 {\n font-size: 11px !important;\n}\n\n.font-size-12 {\n font-size: 12px !important;\n}\n\n.font-size-15 {\n font-size: 15px !important;\n}\n\n.w-md {\n min-width: 110px;\n}\n","body {\r\n font-family: \"Poppins\", Arial, Helvetica, sans-serif;\r\n font-size: 13px;\r\n color: #495057;\r\n}\r\n\r\n.navbar {\r\n box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px;\r\n}\r\n\r\n.navbar-brand {\r\n font-size: 16px;\r\n}\r\n\r\n.chats-title {\r\n padding-left: 14px;\r\n}\r\n\r\n.login-page {\r\n display: flex;\r\n align-items: center;\r\n flex-direction: column;\r\n justify-content: center;\r\n padding-bottom: 190px;\r\n height: 100vh;\r\n}\r\n\r\n.form-signin {\r\n width: 100%;\r\n max-width: 330px;\r\n padding: 15px;\r\n margin: 0 auto;\r\n}\r\n\r\n.text-small {\r\n font-size: 0.9rem;\r\n}\r\n\r\n.messages-box,\r\n.chat-box {\r\n /* height: 510px; */\r\n width: 100%;\r\n}\r\n\r\n.chat-box-wrapper {\r\n flex: 1;\r\n overflow-y: scroll;\r\n}\r\n\r\n.rounded-lg {\r\n border-radius: 0.5rem;\r\n}\r\n\r\ninput::placeholder {\r\n font-size: 0.9rem;\r\n color: #999;\r\n}\r\n\r\n.centered-box {\r\n width: 100%;\r\n height: 100vh;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.login-error-anchor {\r\n position: relative;\r\n}\r\n\r\n.toast-box {\r\n text-align: left;\r\n margin-top: 30px;\r\n position: absolute;\r\n width: 100%;\r\n top: 0;\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: center;\r\n}\r\n\r\n.full-height {\r\n height: 100vh;\r\n flex-direction: column;\r\n display: flex;\r\n}\r\n\r\n.full-height .container {\r\n flex: 1;\r\n}\r\n\r\n.container .row {\r\n height: 100%;\r\n}\r\n\r\n.flex-column {\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.bg-white.flex-column {\r\n height: 100%;\r\n}\r\n\r\n.flex {\r\n flex: 1;\r\n}\r\n\r\n.logout-button {\r\n cursor: pointer;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n padding: 15px 20px;\r\n}\r\n\r\n.logout-button svg {\r\n margin-right: 15px;\r\n}\r\n\r\n.no-messages {\r\n opacity: 0.5;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\n.avatar-box {\r\n width: 50px;\r\n height: 50px;\r\n object-fit: cover;\r\n object-position: 50%;\r\n overflow: hidden;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n}\r\n\r\n.user-link {\r\n cursor: pointer;\r\n}\r\n\r\n.user-link:hover {\r\n text-decoration: underline;\r\n}\r\n\r\n.online-indicator {\r\n width: 14px;\r\n height: 14px;\r\n border: 2px solid white;\r\n bottom: -7px;\r\n right: -7px;\r\n background-color: #4df573;\r\n}\r\n\r\n.online-indicator.selected {\r\n border: none;\r\n width: 12px;\r\n height: 12px;\r\n bottom: -5px;\r\n right: -5px;\r\n}\r\n\r\n.online-indicator.offline {\r\n background-color: #bbb;\r\n}\r\n\r\nspan.pseudo-link {\r\n font-size: 14px;\r\n text-decoration: underline;\r\n color: var(--blue);\r\n cursor: pointer;\r\n}\r\n\r\nspan.pseudo-link:hover {\r\n text-decoration: none;\r\n}\r\n\r\n.list-group-item {\r\n cursor: pointer;\r\n height: 70px;\r\n box-sizing: border-box;\r\n transition: background-color 0.1s ease-out;\r\n}\r\n\r\n.chat-icon {\r\n width: 45px;\r\n height: 45px;\r\n border-radius: 4px;\r\n background-color: #eee;\r\n}\r\n\r\n.chat-icon.active {\r\n background-color: var(--blue);\r\n}\r\n\r\n.chats-title {\r\n font-size: 15px;\r\n}\r\n\r\n.chat-body {\r\n border-radius: 10px !important;\r\n}\r\n\r\n.chat-list-container {\r\n height: 100%;\r\n}\r\n\r\n.chat-input {\r\n border-radius: 30px !important;\r\n background-color: #eff2f7 !important;\r\n border-color: #eff2f7 !important;\r\n padding-right: 120px;\r\n}\r\n\r\n.form-control::placeholder {\r\n font-size: 13px;\r\n}\r\n\r\n.form-control {\r\n display: block;\r\n width: 100%;\r\n height: calc(1.5em + 0.94rem + 2px);\r\n padding: 7.5px 12px;\r\n font-size: 13px;\r\n font-weight: 400;\r\n line-height: 1.5;\r\n color: #495057;\r\n background-color: #fff;\r\n background-clip: padding-box;\r\n border: 1px solid #ced4da;\r\n -webkit-transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n}\r\n\r\n.rounded-button {\r\n border-radius: 30px;\r\n background-color: var(--light);\r\n}\r\n","/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n",".login-form .username-select button {\n background-color: transparent !important;\n color: inherit;\n padding: 7.5px 12px !important;\n display: block !important;\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n width: 100% !important;\n text-align: left !important;\n}\n\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,\n.show > .btn-primary.dropdown-toggle {\n color: inherit;\n}\n\n.login-form .username-select .dropdown-menu.show.dropdown-menu-right {\n transform: translate(0px, 38px) !important;\n}\n\n.username-select-dropdown {\n position: relative;\n display: flex !important;\n align-items: center;\n background-color: transparent !important;\n color: inherit;\n padding: 0 12px !important;\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n width: 100% !important;\n text-align: left !important;\n height: calc(1.5em + 0.94rem + 2px) !important;\n\n cursor: pointer;\n}\n\n.username-select-dropdown .username-select-block {\n background-color: var(--white);\n position: absolute;\n top: -1138px;\n left: 0;\n opacity: 0;\n transform: scale(0.5, 0.5);\n transform-origin: top left;\n transition: opacity 0.2s ease, transform 0.2s ease;\n\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n\n padding: 8px 0px;\n}\n\n.username-select-dropdown:focus {\n outline: none;\n border-color: #80bdff !important;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.username-select-dropdown .username-select-block.open {\n top: 42px;\n transform: scale(1, 1);\n opacity: 1;\n}\n\n.username-select-row {\n display: flex;\n width: 100%;\n justify-content: space-between;\n align-items: center;\n}\n\n.username-select-dropdown .username-select-block .username-select-block-item {\n padding: 4px 24px;\n}\n\n.username-select-dropdown\n .username-select-block\n .username-select-block-item:hover {\n background-color: var(--light);\n}\n",".chat-list-item {\n cursor: pointer;\n padding: 14px 16px;\n}\n.mdi-circle:before {\n content: \"󰝥\";\n}\n\n.mdi-set,\n.mdi:before {\n display: inline-block;\n font: normal normal normal 24px/1 Material Design Icons;\n font-size: inherit;\n text-rendering: auto;\n line-height: inherit;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n"]} -------------------------------------------------------------------------------- /client/build/static/js/2.cddb4d36.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** @license React v0.20.1 14 | * scheduler.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v17.0.1 23 | * react-dom.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.1 32 | * react-jsx-runtime.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.1 41 | * react.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | //! moment.js 50 | -------------------------------------------------------------------------------- /client/build/static/js/runtime-main.03887cff.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@types/socket.io-client": "^1.4.34" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/public/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/0.jpg -------------------------------------------------------------------------------- /client/public/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/1.jpg -------------------------------------------------------------------------------- /client/public/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/10.jpg -------------------------------------------------------------------------------- /client/public/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/11.jpg -------------------------------------------------------------------------------- /client/public/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/12.jpg -------------------------------------------------------------------------------- /client/public/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/2.jpg -------------------------------------------------------------------------------- /client/public/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/3.jpg -------------------------------------------------------------------------------- /client/public/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/4.jpg -------------------------------------------------------------------------------- /client/public/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/5.jpg -------------------------------------------------------------------------------- /client/public/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/6.jpg -------------------------------------------------------------------------------- /client/public/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/7.jpg -------------------------------------------------------------------------------- /client/public/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/8.jpg -------------------------------------------------------------------------------- /client/public/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/avatars/9.jpg -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Node.JS Redis chat 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/welcome-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/client/public/welcome-back.png -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, {useEffect, useState} from "react"; 3 | import Login from "./components/Login"; 4 | import Chat from "./components/Chat"; 5 | import {AppContext} from "./state"; 6 | import {LoadingScreen} from "./components/LoadingScreen"; 7 | import Navbar from "./components/Navbar"; 8 | import {processChannelMessage, processSignIn} from "./websockets/process"; 9 | import useAppStateContext from './state'; 10 | import {initWebSocket} from './hooks'; 11 | 12 | const App = () => { 13 | const [showLogin, setShowLogin] = useState(true) 14 | const [state, dispatch] = useAppStateContext(); 15 | 16 | function onLogout() { 17 | localStorage.clear(); 18 | setShowLogin(true); 19 | } 20 | 21 | useEffect(() => { 22 | initWebSocket({dispatch, state}); 23 | }, []); 24 | 25 | return ( 26 | 27 |
33 | 34 | {showLogin ? ( 35 | 36 | ) : ( 37 | 43 | )} 44 |
45 |
46 | ); 47 | 48 | 49 | }; 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /client/src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {processSignIn} from "./websockets/process"; 3 | axios.defaults.withCredentials = true; 4 | 5 | const BASE_URL = ''; 6 | 7 | export const MESSAGES_TO_LOAD = 15; 8 | 9 | const url = x => `${BASE_URL}${x}`; 10 | 11 | /** Checks if there's an existing session. */ 12 | export const getMe = () => { 13 | return axios.get(url('/me')) 14 | .then(x => x.data) 15 | .catch(_ => null); 16 | }; 17 | 18 | /** Handle user log in */ 19 | export const login = (username, password) => { 20 | processSignIn(username,password); 21 | }; 22 | 23 | export const logOut = () => { 24 | return axios.post(url('/logout')); 25 | }; 26 | 27 | /** 28 | * Function for checking which deployment urls exist. 29 | * 30 | * @returns {Promise<{ 31 | * heroku?: string; 32 | * google_cloud?: string; 33 | * vercel?: string; 34 | * github?: string; 35 | * }>} 36 | */ 37 | export const getButtonLinks = () => { 38 | return axios.get(url('/links')) 39 | .then(x => x.data) 40 | .catch(_ => null); 41 | }; 42 | 43 | /** This was used to get a random login name (for demo purposes). */ 44 | export const getRandomName = () => { 45 | return axios.get(url('/randomname')).then(x => x.data); 46 | }; 47 | 48 | /** 49 | * Load messages 50 | * 51 | * @param {string} id room id 52 | * @param {number} offset 53 | * @param {number} size 54 | */ 55 | export const getMessages = (id, 56 | offset = 0, 57 | size = MESSAGES_TO_LOAD 58 | ) => { 59 | return axios.get(url(`/room/${id}/messages`), { 60 | params: { 61 | offset, 62 | size 63 | } 64 | }) 65 | .then(x => x.data.reverse()); 66 | }; 67 | 68 | /** 69 | * @returns {Promise<{ name: string, id: string, messages: Array }>} 70 | */ 71 | export const getPreloadedRoom = async () => { 72 | return axios.get(url(`/room/0/preload`)).then(x => x.data); 73 | }; 74 | 75 | /** 76 | * Fetch users by requested ids 77 | * @param {Array} ids 78 | */ 79 | export const getUsers = (ids) => { 80 | return axios.get(url(`/users`), { params: { ids } }).then(x => x.data); 81 | }; 82 | 83 | /** Fetch users which are online */ 84 | export const getOnlineUsers = () => { 85 | return axios.get(url(`/users/online`)).then(x => x.data); 86 | }; 87 | 88 | /** This one is called on a private messages room created. */ 89 | export const addRoom = async (user1, user2) => { 90 | return axios.post(url(`/room`), { user1, user2 }).then(x => x.data); 91 | }; 92 | 93 | /** 94 | * @returns {Promise>} 95 | */ 96 | export const getRooms = async (userId) => { 97 | return axios.get(url(`/rooms/${userId}`)).then(x => x.data); 98 | }; 99 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/AvatarImage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useMemo } from "react"; 3 | import { getAvatarByUserAndRoomId } from "../../../../../utils"; 4 | import ChatIcon from "./ChatIcon"; 5 | 6 | const AvatarImage = ({ name, id }) => { 7 | const url = useMemo(() => { 8 | const av = getAvatarByUserAndRoomId("" + id); 9 | if (name === "Mary") { 10 | return `${process.env.PUBLIC_URL}/avatars/0.jpg`; 11 | } else if (name === "Pablo") { 12 | return `${process.env.PUBLIC_URL}/avatars/2.jpg`; 13 | } else if (name === "Joe") { 14 | return `${process.env.PUBLIC_URL}/avatars/9.jpg`; 15 | } else if (name === "Alex") { 16 | return `${process.env.PUBLIC_URL}/avatars/8.jpg`; 17 | } 18 | return av; 19 | }, [id, name]); 20 | 21 | return ( 22 | <> 23 | {name !== "General" ? ( 24 | {name} 30 | ) : ( 31 |
32 | 33 |
34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default AvatarImage; 40 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatIcon.jsx: -------------------------------------------------------------------------------- 1 | const ChatIcon = () => ( 2 | 9 | 10 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export default ChatIcon; 33 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatListItem/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import "./style.css"; 3 | import React, { useMemo } from "react"; 4 | import moment from "moment"; 5 | import { useEffect } from "react"; 6 | import { getMessages } from "../../../../../../api"; 7 | import AvatarImage from "../AvatarImage"; 8 | import OnlineIndicator from "../../../OnlineIndicator"; 9 | import useAppStateContext, {useAppState} from '../../../../../../state'; 10 | 11 | /** 12 | * @param {{ active: boolean; room: import('../../../../../../state').Room; onClick: () => void; }} props 13 | */ 14 | const ChatListItem = ({ room, active = false, onClick }) => { 15 | const { online, name, userId } = useChatListItemHandlers(room); 16 | return ( 17 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
{name}
31 |
32 |
33 | ); 34 | }; 35 | 36 | const useChatListItemHandlers = ( 37 | /** @type {import("../../../../../../state").Room} */ room 38 | ) => { 39 | const { id, name } = room; 40 | const [state] = useAppState(); 41 | /** Here we want to associate the room with a user by its name (since it's unique). */ 42 | const [isUser, online, userId] = useMemo(() => { 43 | try { 44 | let pseudoUserId = Math.abs(parseInt(id.split(":").reverse().pop())); 45 | const isUser = pseudoUserId > 0; 46 | const usersFiltered = Object.entries(state.users) 47 | .filter(([, user]) => user.Username === name) 48 | .map(([, user]) => user); 49 | let online = false; 50 | if (usersFiltered.length > 0) { 51 | online = usersFiltered[0].OnLine; 52 | pseudoUserId = +usersFiltered[0].id; 53 | } 54 | return [isUser, online, pseudoUserId]; 55 | } catch (_) { 56 | return [false, false, "0"]; 57 | } 58 | }, [id, name, state.users]); 59 | 60 | return { 61 | isUser, 62 | online, 63 | userId, 64 | name: room.name, 65 | }; 66 | }; 67 | 68 | export default ChatListItem; 69 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatListItem/style.css: -------------------------------------------------------------------------------- 1 | .chat-list-item { 2 | cursor: pointer; 3 | padding: 14px 16px; 4 | } 5 | .mdi-circle:before { 6 | content: "󰝥"; 7 | } 8 | 9 | .mdi-set, 10 | .mdi:before { 11 | display: inline-block; 12 | font: normal normal normal 24px/1 Material Design Icons; 13 | font-size: inherit; 14 | text-rendering: auto; 15 | line-height: inherit; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | import { Power } from "react-bootstrap-icons"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | import AvatarImage from "./AvatarImage"; 7 | 8 | const Footer = ({ user, onLogOut }) => ( 9 |
13 | {( 14 | <> 15 | 16 | 17 | 18 | )} 19 |
20 | ); 21 | 22 | const LogoutButton = ({ onLogOut, col = 5, noinfo = false }) => ( 23 |
onLogOut()} 25 | style={{ cursor: "pointer" }} 26 | className={`col-${col} text-danger ${!noinfo ? "text-right" : ""}`} 27 | > 28 | Log out 29 |
30 | ); 31 | 32 | const UserInfo = ({ user = {}, col = 7, noinfo = false }) => ( 33 |
38 |
39 | 40 |
41 | {!noinfo && ( 42 |
43 |
{user.username}
44 |
45 | 46 |

Active

47 |
48 |
49 | )} 50 |
51 | ); 52 | 53 | export default Footer; 54 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useMemo } from "react"; 3 | import ChatListItem from "./components/ChatListItem"; 4 | import Footer from "./components/Footer"; 5 | import {useAppState} from '../../../../state'; 6 | import {processChannelJoin, processChannelLeave} from '../../../../websockets/process'; 7 | 8 | const ChatList = ({ rooms, user, currentRoom, onLogOut }) => { 9 | const [state, dispatch] = useAppState(); 10 | 11 | const processedRooms = useMemo(() => { 12 | const roomsList = Object.values(rooms); 13 | const main = roomsList.filter((x) => x.id === "0"); 14 | let other = roomsList.filter((x) => x.id !== "0"); 15 | other = other.sort( 16 | (a, b) => +a.id.split(":").pop() - +b.id.split(":").pop() 17 | ); 18 | return [...(main ? main : []), ...other]; 19 | }, [rooms]); 20 | 21 | return ( 22 | <> 23 |
24 |
25 |

Chats

26 |
27 |
28 |
29 | {processedRooms.map((room) => ( 30 | { 33 | dispatch({type: "set current room", payload: room.id}); 34 | // processChannelLeave(); 35 | processChannelJoin(room.id); 36 | }} 37 | active={currentRoom === room.id} 38 | room={room} 39 | /> 40 | ))} 41 |
42 |
43 |
44 |
45 | 46 | ); 47 | }; 48 | 49 | export default ChatList; 50 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/ClockIcon.jsx: -------------------------------------------------------------------------------- 1 | const ClockIcon = () => ( 2 | 12 | ); 13 | 14 | export default ClockIcon; 15 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/InfoMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const InfoMessage = ({ message }) => { 3 | return ( 4 |

8 | {message} 9 |

10 | ); 11 | }; 12 | 13 | export default InfoMessage; 14 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/MessagesLoading.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | const MessagesLoading = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default MessagesLoading; 15 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/NoMessages.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import { CardText } from "react-bootstrap-icons"; 4 | 5 | const NoMessages = () => { 6 | return ( 7 |
8 | 9 |

No messages

10 |
11 | ); 12 | }; 13 | 14 | export default NoMessages; 15 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/ReceiverMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | 6 | const ReceiverMessage = ({ 7 | username = "user", 8 | message = "Lorem ipsum dolor...", 9 | date, 10 | }) => ( 11 |
12 |
13 |
14 |
18 |
19 |
25 | {username} 26 |
27 |

{message}

28 |

29 | {moment(date).format("LT")}{" "} 30 |

31 |
32 |
33 |
34 |
35 | ); 36 | export default ReceiverMessage; 37 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/SenderMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | 7 | const SenderMessage = ({ 8 | user, 9 | message = "Lorem ipsum dolor...", 10 | date, 11 | onUserClicked, 12 | }) => ( 13 |
14 |
15 |
19 |
20 | {user && ( 21 |
22 |
30 | {user.Username} 31 |
32 | 33 |
34 | )} 35 |

{message}

36 |

37 | {moment(date).format("LT")}{" "} 38 |

39 |
40 |
41 |
42 |
43 |
44 | ); 45 | 46 | export default SenderMessage; 47 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import {login, MESSAGES_TO_LOAD} from "../../../../api"; 4 | import InfoMessage from "./components/InfoMessage"; 5 | import MessagesLoading from "./components/MessagesLoading"; 6 | import NoMessages from "./components/NoMessages"; 7 | import ReceiverMessage from "./components/ReceiverMessage"; 8 | import SenderMessage from "./components/SenderMessage"; 9 | 10 | const MessageList = ({ 11 | messageListElement, 12 | messages, 13 | room, 14 | onLoadMoreMessages, 15 | user = {}, 16 | onUserClicked, 17 | }) => { 18 | return ( 19 |
23 | {messages === undefined ? ( 24 | 25 | ) : messages.length === 0 ? ( 26 | 27 | ) : ( 28 | <> 29 | )} 30 |
31 | {messages && messages.length !== 0 && ( 32 | <> 33 | {room.offset && room.offset >= MESSAGES_TO_LOAD ? ( 34 |
35 |
38 |
39 | 49 |
50 |
53 |
54 | ) : ( 55 | <> 56 | )} 57 | {messages.map((message, x) => { 58 | const key = message.Message + message.CreatedAt + message.SenderUUID + x; 59 | if (message.SenderUUID === "info") { 60 | return ; 61 | } 62 | if (message.SenderUUID !== user.uuid) { 63 | return ( 64 | onUserClicked(message.SenderUUID)} 66 | key={key} 67 | message={message.Message} 68 | date={message.CreatedAt} 69 | user={message.Sender} 70 | /> 71 | ); 72 | } 73 | return ( 74 | 82 | ); 83 | })} 84 | 85 | )} 86 |
87 |
88 | )}; 89 | export default MessageList; 90 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/OnlineIndicator.jsx: -------------------------------------------------------------------------------- 1 | const OnlineIndicator = ({ online, hide = false, width = 8, height = 8 }) => { 2 | return ( 3 |
9 | ); 10 | }; 11 | 12 | export default OnlineIndicator; 13 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/TypingArea.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const TypingArea = ({ message, setMessage, onSubmit }) => ( 3 |
4 |
5 |
6 |
7 | setMessage(e.target.value)} 10 | type="text" 11 | placeholder="Enter Message..." 12 | className="form-control chat-input" 13 | /> 14 | {/**/} 15 |
16 |
17 |
18 | 27 |
28 |
29 |
30 | ); 31 | 32 | export default TypingArea; 33 | -------------------------------------------------------------------------------- /client/src/components/Chat/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, {useCallback, useEffect, useRef, useState} from "react"; 3 | import ChatList from "./components/ChatList"; 4 | import MessageList from "./components/MessageList"; 5 | import TypingArea from "./components/TypingArea"; 6 | import useAppStateContext, {useAppState} from '../../state'; 7 | 8 | /** 9 | * @param {{ 10 | * onLogOut: () => void, 11 | * onMessageSend: (message: string, roomId: string) => void, 12 | * user: import("../../state").UserEntry 13 | * }} props 14 | */ 15 | export default function Chat({ onLogOut, user, onMessageSend, users }) { 16 | const [{rooms, currentRoom}] = useAppState(); 17 | 18 | const [room, setRoom] = useState({}); 19 | const [message, setMessage] = useState(""); 20 | 21 | const messageListElement = useRef(null); 22 | 23 | const scrollToBottom = useCallback(() => { 24 | if (messageListElement.current) { 25 | messageListElement.current.scrollTo({ 26 | top: messageListElement.current.scrollHeight, 27 | }); 28 | } 29 | }, []); 30 | 31 | useEffect(() => { 32 | scrollToBottom(); 33 | }, [rooms[currentRoom].messages, scrollToBottom]); 34 | 35 | useEffect(() => { 36 | setRoom(rooms[currentRoom]); 37 | }, [currentRoom]) 38 | 39 | return ( 40 |
41 |
42 |
43 | 49 |
50 | {/* Chat Box*/} 51 |
52 |
53 |

54 | {room ? room.name : "Room"} 55 | {" Room"} 56 |

57 |
58 | 66 | 67 | {/* Typing area */} 68 | { 72 | e.preventDefault(); 73 | if (message) { 74 | onMessageSend(message.trim(), room.id); 75 | setMessage(""); 76 | 77 | messageListElement.current.scrollTop = 78 | messageListElement.current.scrollHeight; 79 | } 80 | }} 81 | /> 82 |
83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /client/src/components/LoadingScreen.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | export function LoadingScreen() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Login/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Toast } from "react-bootstrap"; 3 | import React, { useState, useRef } from "react"; 4 | import Logo from "../Logo"; 5 | import "./style.css"; 6 | import { useEffect } from "react"; 7 | 8 | const DEMO_USERS = ["Pablo", "Joe", "Mary", "Alex"]; 9 | 10 | export default function Login({ onLogIn,setShowLogin }) { 11 | const [username, setUsername] = useState( 12 | () => DEMO_USERS[Math.floor(Math.random() * DEMO_USERS.length)] 13 | ); 14 | const [password, setPassword] = useState("password123"); 15 | const [error, setError] = useState(null); 16 | 17 | const onSubmit = async (event) => { 18 | event.preventDefault(); 19 | onLogIn(username, password, setShowLogin); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |
31 |
32 |
43 |
44 |

Welcome Back !

45 |

Sign in to continue

46 |
47 |
48 | welcome back 53 |
54 |
55 |
59 |
67 | 68 |
69 |
70 |
71 | 72 |
81 | 82 | 83 |
84 | 89 |
90 | 91 | 94 | setPassword(event.target.value)} 97 | type="password" 98 | id="inputPassword" 99 | className="form-control" 100 | placeholder="Password" 101 | required 102 | /> 103 |
104 | 107 |
108 |
109 | setError(null)} 112 | show={error !== null} 113 | delay={3000} 114 | autohide 115 | > 116 | 117 | 122 | Error 123 | 124 | {error} 125 | 126 |
127 |
128 |
129 | 130 |
131 |
132 | 133 | ); 134 | } 135 | 136 | const UsernameSelect = ({ username, setUsername, names = [""] }) => { 137 | const [open, setOpen] = useState(false); 138 | const [width, setWidth] = useState(0); 139 | const ref = useRef(); 140 | /** @ts-ignore */ 141 | const clientRectWidth = ref.current?.getBoundingClientRect().width; 142 | useEffect(() => { 143 | /** @ts-ignore */ 144 | setWidth(clientRectWidth); 145 | }, [clientRectWidth]); 146 | 147 | /** Click away listener */ 148 | useEffect(() => { 149 | if (open) { 150 | const listener = () => setOpen(false); 151 | document.addEventListener("click", listener); 152 | return () => document.removeEventListener("click", listener); 153 | } 154 | }, [open]); 155 | 156 | /** Make the current div focused */ 157 | useEffect(() => { 158 | if (open) { 159 | /** @ts-ignore */ 160 | ref.current?.focus(); 161 | } 162 | }, [open]); 163 | 164 | return ( 165 |
setOpen((o) => !o)} 170 | > 171 |
172 |
{username}
173 |
174 | 175 | 176 | 177 |
178 |
179 |
183 | {names.map((name) => ( 184 |
setUsername(name)} 188 | > 189 | {name} 190 |
191 | ))} 192 |
193 |
194 | ); 195 | }; 196 | -------------------------------------------------------------------------------- /client/src/components/Login/style.css: -------------------------------------------------------------------------------- 1 | .login-form .username-select button { 2 | background-color: transparent !important; 3 | color: inherit; 4 | padding: 7.5px 12px !important; 5 | display: block !important; 6 | border: 1px solid rgb(206, 212, 218) !important; 7 | border-radius: 4px !important; 8 | width: 100% !important; 9 | text-align: left !important; 10 | } 11 | 12 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled).active, 13 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled):active, 14 | .show > .btn-primary.dropdown-toggle { 15 | color: inherit; 16 | } 17 | 18 | .login-form .username-select .dropdown-menu.show.dropdown-menu-right { 19 | transform: translate(0px, 38px) !important; 20 | } 21 | 22 | .username-select-dropdown { 23 | position: relative; 24 | display: flex !important; 25 | align-items: center; 26 | background-color: transparent !important; 27 | color: inherit; 28 | padding: 0 12px !important; 29 | border: 1px solid rgb(206, 212, 218) !important; 30 | border-radius: 4px !important; 31 | width: 100% !important; 32 | text-align: left !important; 33 | height: calc(1.5em + 0.94rem + 2px) !important; 34 | 35 | cursor: pointer; 36 | } 37 | 38 | .username-select-dropdown .username-select-block { 39 | background-color: var(--white); 40 | position: absolute; 41 | top: -1138px; 42 | left: 0; 43 | opacity: 0; 44 | transform: scale(0.5, 0.5); 45 | transform-origin: top left; 46 | transition: opacity 0.2s ease, transform 0.2s ease; 47 | 48 | border: 1px solid rgb(206, 212, 218) !important; 49 | border-radius: 4px !important; 50 | 51 | padding: 8px 0px; 52 | } 53 | 54 | .username-select-dropdown:focus { 55 | outline: none; 56 | border-color: #80bdff !important; 57 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 58 | } 59 | 60 | .username-select-dropdown .username-select-block.open { 61 | top: 42px; 62 | transform: scale(1, 1); 63 | opacity: 1; 64 | } 65 | 66 | .username-select-row { 67 | display: flex; 68 | width: 100%; 69 | justify-content: space-between; 70 | align-items: center; 71 | } 72 | 73 | .username-select-dropdown .username-select-block .username-select-block-item { 74 | padding: 4px 24px; 75 | } 76 | 77 | .username-select-dropdown 78 | .username-select-block 79 | .username-select-block-item:hover { 80 | background-color: var(--light); 81 | } 82 | -------------------------------------------------------------------------------- /client/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | const Logo = ({ width = 64, height = 64 }) => { 2 | return ( 3 | 10 | 11 | 12 | 16 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Logo; 49 | -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useEffect, useState } from "react"; 3 | import { getButtonLinks } from "../api"; 4 | 5 | const Navbar = () => { 6 | /** 7 | * @type {[{ 8 | * heroku?: string; 9 | * google_cloud?: string; 10 | * vercel?: string; 11 | * github?: string; 12 | * }, React.Dispatch]} 13 | */ 14 | const [links, setLinks] = useState(null); 15 | useEffect(() => { 16 | getButtonLinks().then(setLinks); 17 | }, []); 18 | return ( 19 | 29 | ); 30 | }; 31 | 32 | const GithubIcon = ({ link }) => ( 33 | 39 | 47 | 52 | 57 | 58 | 59 | ); 60 | 61 | export default Navbar; 62 | -------------------------------------------------------------------------------- /client/src/hooks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import {messages} from "./websockets/messages"; 3 | import {webSocketUrl} from './websockets/config'; 4 | 5 | const ws = new WebSocket(webSocketUrl); 6 | 7 | export function initWebSocket(appState) { 8 | ws.onmessage = (event) => { 9 | const data = JSON.parse(event.data); 10 | console.log('EVENT', data.type, data); 11 | if(typeof messages[data.type] == "function") { 12 | messages[data.type](data, appState); 13 | } else { 14 | console.log("Unknown message type: " + data.type); 15 | } 16 | } 17 | } 18 | 19 | /** 20 | * main method for send message to websocket server 21 | * @param {Object} message 22 | */ 23 | export function webSocketSend(message) { 24 | const wsData = JSON.stringify(message); 25 | ws.send(wsData); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "./styles/style-overrides.css"; 3 | import "./styles/style.css"; 4 | import "./styles/font-face.css"; 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | import App from "./App"; 10 | 11 | ReactDOM.render(, document.getElementById("root")); 12 | -------------------------------------------------------------------------------- /client/src/state.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createContext, useContext, useReducer } from "react"; 3 | 4 | /** 5 | * @typedef {{ 6 | * from: string 7 | * date: number 8 | * message: string 9 | * roomId?: string 10 | * }} Message 11 | * 12 | * @typedef {{ 13 | * name: string; 14 | * id: string; 15 | * messages?: Message[] 16 | * connected?: boolean; 17 | * offset?: number; 18 | * forUserId?: null | number | string 19 | * lastMessage?: Message | null 20 | * }} Room 21 | * 22 | * @typedef {{ 23 | * username: string; 24 | * id: string; 25 | * online?: boolean; 26 | * room?: string; 27 | * }} UserEntry 28 | * 29 | * @typedef {{ 30 | * currentRoom: string; 31 | * rooms: {[id: string]: Room}; 32 | * users: {[id: string]: UserEntry} 33 | * }} State 34 | * 35 | * @param {State} state 36 | * @param {{type: string; payload: any}} action 37 | * @returns {State} 38 | */ 39 | const reducer = (state, action) => { 40 | switch (action.type) { 41 | case "clear": 42 | return { currentRoom: "0", rooms: {}, users: {} }; 43 | case "set user": { 44 | console.log('set user', action.payload) 45 | return {...state, user: action.payload}; 46 | } 47 | case "set users": { 48 | return { 49 | ...state, 50 | users: action.payload, 51 | }; 52 | } 53 | case "make user online": { 54 | return { 55 | ...state, 56 | users: { 57 | ...state.users, 58 | [action.payload]: { ...state.users[action.payload], online: true }, 59 | }, 60 | }; 61 | } 62 | case "append users": { 63 | return { ...state, users: { ...state.users, ...action.payload } }; 64 | } 65 | case "set messages": { 66 | return { 67 | ...state, 68 | rooms: { 69 | ...state.rooms, 70 | [state.currentRoom]: { 71 | ...state.rooms[state.currentRoom], 72 | messages: action.payload, 73 | offset: action.payload.length, 74 | }, 75 | }, 76 | }; 77 | } 78 | case "prepend messages": { 79 | const messages = [ 80 | ...action.payload.messages, 81 | ...state.rooms[action.payload.id].messages, 82 | ]; 83 | return { 84 | ...state, 85 | rooms: { 86 | ...state.rooms, 87 | [action.payload.id]: { 88 | ...state.rooms[action.payload.id], 89 | messages, 90 | offset: messages.length, 91 | }, 92 | }, 93 | }; 94 | } 95 | case "append message": 96 | if (state.rooms[action.payload.id] === undefined) { 97 | return state; 98 | } 99 | return { 100 | ...state, 101 | rooms: { 102 | ...state.rooms, 103 | [action.payload.id]: { 104 | ...state.rooms[action.payload.id], 105 | lastMessage: action.payload.message, 106 | messages: state.rooms[action.payload.id].messages 107 | ? [ 108 | ...state.rooms[action.payload.id].messages, 109 | action.payload.message, 110 | ] 111 | : undefined, 112 | }, 113 | }, 114 | }; 115 | case 'set last message': 116 | return { ...state, rooms: { ...state.rooms, [action.payload.id]: { ...state.rooms[action.payload.id], lastMessage: action.payload.lastMessage } } }; 117 | case "set current room": 118 | return { ...state, currentRoom: action.payload }; 119 | case "add room": 120 | return { 121 | ...state, 122 | rooms: { ...state.rooms, [action.payload.id]: action.payload }, 123 | }; 124 | // case "set rooms": { 125 | // /** @type {Room[]} */ 126 | // const newRooms = action.payload; 127 | // const rooms = { ...state.rooms }; 128 | // newRooms.forEach((room) => { 129 | // rooms[room.id] = { 130 | // ...room, 131 | // messages: rooms[room.id] && rooms[room.id].messages, 132 | // }; 133 | // }); 134 | // return { ...state, rooms }; 135 | // } 136 | // temporary, while no rooms 137 | case "set rooms": { 138 | /** @type {Room[]} */ 139 | const newRooms = action.payload; 140 | const rooms = { ...state.rooms }; 141 | newRooms.forEach((room) => { 142 | rooms[room.UUID] = { 143 | ...room, 144 | id: room.UUID, 145 | name: room.Username, 146 | messages: rooms[room.id] && rooms[room.id].messages, 147 | }; 148 | }); 149 | return { ...state, rooms }; 150 | } 151 | default: 152 | return state; 153 | } 154 | }; 155 | 156 | /** @type {State} */ 157 | const initialState = { 158 | currentRoom: "", 159 | rooms: {"": {id: "", name: "General"}}, 160 | users: {}, 161 | user: {} 162 | }; 163 | 164 | const useAppStateContext = () => { 165 | return useReducer(reducer, initialState); 166 | }; 167 | 168 | // @ts-ignore 169 | export const AppContext = createContext(); 170 | 171 | /** 172 | * @returns {[ 173 | * State, 174 | * React.Dispatch<{ 175 | * type: string; 176 | * payload: any; 177 | * }> 178 | * ]} 179 | */ 180 | export const useAppState = () => { 181 | const [state, dispatch] = useContext(AppContext); 182 | return [state, dispatch]; 183 | }; 184 | 185 | export default useAppStateContext; 186 | -------------------------------------------------------------------------------- /client/src/styles/font-face.css: -------------------------------------------------------------------------------- 1 | /* devanagari */ 2 | @font-face { 3 | font-family: "Poppins"; 4 | font-style: normal; 5 | font-weight: 300; 6 | font-display: swap; 7 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) 8 | format("woff2"); 9 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 10 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 11 | } 12 | /* latin-ext */ 13 | @font-face { 14 | font-family: "Poppins"; 15 | font-style: normal; 16 | font-weight: 300; 17 | font-display: swap; 18 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) 19 | format("woff2"); 20 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 21 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 22 | } 23 | /* latin */ 24 | @font-face { 25 | font-family: "Poppins"; 26 | font-style: normal; 27 | font-weight: 300; 28 | font-display: swap; 29 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) 30 | format("woff2"); 31 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 32 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 33 | U+FEFF, U+FFFD; 34 | } 35 | /* devanagari */ 36 | @font-face { 37 | font-family: "Poppins"; 38 | font-style: normal; 39 | font-weight: 400; 40 | font-display: swap; 41 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) 42 | format("woff2"); 43 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 44 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 45 | } 46 | /* latin-ext */ 47 | @font-face { 48 | font-family: "Poppins"; 49 | font-style: normal; 50 | font-weight: 400; 51 | font-display: swap; 52 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) 53 | format("woff2"); 54 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 55 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 56 | } 57 | /* latin */ 58 | @font-face { 59 | font-family: "Poppins"; 60 | font-style: normal; 61 | font-weight: 400; 62 | font-display: swap; 63 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) 64 | format("woff2"); 65 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 66 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 67 | U+FEFF, U+FFFD; 68 | } 69 | /* devanagari */ 70 | @font-face { 71 | font-family: "Poppins"; 72 | font-style: normal; 73 | font-weight: 500; 74 | font-display: swap; 75 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) 76 | format("woff2"); 77 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 78 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 79 | } 80 | /* latin-ext */ 81 | @font-face { 82 | font-family: "Poppins"; 83 | font-style: normal; 84 | font-weight: 500; 85 | font-display: swap; 86 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) 87 | format("woff2"); 88 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 89 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 90 | } 91 | /* latin */ 92 | @font-face { 93 | font-family: "Poppins"; 94 | font-style: normal; 95 | font-weight: 500; 96 | font-display: swap; 97 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) 98 | format("woff2"); 99 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 100 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 101 | U+FEFF, U+FFFD; 102 | } 103 | /* devanagari */ 104 | @font-face { 105 | font-family: "Poppins"; 106 | font-style: normal; 107 | font-weight: 600; 108 | font-display: swap; 109 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) 110 | format("woff2"); 111 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 112 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 113 | } 114 | /* latin-ext */ 115 | @font-face { 116 | font-family: "Poppins"; 117 | font-style: normal; 118 | font-weight: 600; 119 | font-display: swap; 120 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) 121 | format("woff2"); 122 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 123 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 124 | } 125 | /* latin */ 126 | @font-face { 127 | font-family: "Poppins"; 128 | font-style: normal; 129 | font-weight: 600; 130 | font-display: swap; 131 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) 132 | format("woff2"); 133 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 134 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 135 | U+FEFF, U+FFFD; 136 | } 137 | /* devanagari */ 138 | @font-face { 139 | font-family: "Poppins"; 140 | font-style: normal; 141 | font-weight: 700; 142 | font-display: swap; 143 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) 144 | format("woff2"); 145 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 146 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 147 | } 148 | /* latin-ext */ 149 | @font-face { 150 | font-family: "Poppins"; 151 | font-style: normal; 152 | font-weight: 700; 153 | font-display: swap; 154 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) 155 | format("woff2"); 156 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 157 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 158 | } 159 | /* latin */ 160 | @font-face { 161 | font-family: "Poppins"; 162 | font-style: normal; 163 | font-weight: 700; 164 | font-display: swap; 165 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) 166 | format("woff2"); 167 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 168 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 169 | U+FEFF, U+FFFD; 170 | } 171 | -------------------------------------------------------------------------------- /client/src/styles/style-overrides.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: rgb(85, 110, 230) !important; 3 | --light: #f5f5f8 !important; 4 | --success: rgb(52, 195, 143) !important; 5 | } 6 | 7 | .bg-success { 8 | background-color: var(--success) !important; 9 | } 10 | 11 | .bg-light { 12 | background-color: var(--light) !important; 13 | } 14 | 15 | .bg-gray { 16 | background-color: var(--gray) !important; 17 | } 18 | 19 | .bg-primary { 20 | background-color: var(--primary) !important; 21 | } 22 | 23 | .text-primary { 24 | color: var(--primary) !important; 25 | } 26 | 27 | .list-group-item.active { 28 | background-color: var(--primary) !important; 29 | border-color: var(--primary) !important; 30 | } 31 | 32 | .btn-rounded { 33 | border-radius: 30px !important; 34 | } 35 | 36 | .btn { 37 | display: inline-block; 38 | font-weight: 400; 39 | color: #495057; 40 | text-align: center; 41 | vertical-align: middle; 42 | user-select: none; 43 | background-color: transparent; 44 | border: 1px solid transparent; 45 | border-radius: 30px !important; 46 | padding: 0.47rem 0.75rem; 47 | font-size: 0.8125rem; 48 | line-height: 1.5; 49 | border-radius: 0.25rem; 50 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 51 | border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 52 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 53 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 54 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 55 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 56 | -webkit-box-shadow 0.15s ease-in-out; 57 | } 58 | 59 | .btn-primary { 60 | color: #fff; 61 | background-color: var(--primary); 62 | border-color: var(--primary); 63 | } 64 | 65 | .btn-primary.focus, 66 | .btn-primary:focus, 67 | .btn-primary:hover { 68 | color: #fff; 69 | background-color: #3452e1; 70 | border-color: #2948df; 71 | } 72 | 73 | .font-size-14 { 74 | font-size: 14px !important; 75 | } 76 | 77 | .font-size-11 { 78 | font-size: 11px !important; 79 | } 80 | 81 | .font-size-12 { 82 | font-size: 12px !important; 83 | } 84 | 85 | .font-size-15 { 86 | font-size: 15px !important; 87 | } 88 | 89 | .w-md { 90 | min-width: 110px; 91 | } 92 | -------------------------------------------------------------------------------- /client/src/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", Arial, Helvetica, sans-serif; 3 | font-size: 13px; 4 | color: #495057; 5 | } 6 | 7 | .navbar { 8 | box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px; 9 | } 10 | 11 | .navbar-brand { 12 | font-size: 16px; 13 | } 14 | 15 | .chats-title { 16 | padding-left: 14px; 17 | } 18 | 19 | .login-page { 20 | display: flex; 21 | align-items: center; 22 | flex-direction: column; 23 | justify-content: center; 24 | padding-bottom: 190px; 25 | height: 100vh; 26 | } 27 | 28 | .form-signin { 29 | width: 100%; 30 | max-width: 330px; 31 | padding: 15px; 32 | margin: 0 auto; 33 | } 34 | 35 | .text-small { 36 | font-size: 0.9rem; 37 | } 38 | 39 | .messages-box, 40 | .chat-box { 41 | /* height: 510px; */ 42 | width: 100%; 43 | } 44 | 45 | .chat-box-wrapper { 46 | flex: 1; 47 | overflow-y: scroll; 48 | } 49 | 50 | .rounded-lg { 51 | border-radius: 0.5rem; 52 | } 53 | 54 | input::placeholder { 55 | font-size: 0.9rem; 56 | color: #999; 57 | } 58 | 59 | .centered-box { 60 | width: 100%; 61 | height: 100vh; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | } 66 | 67 | .login-error-anchor { 68 | position: relative; 69 | } 70 | 71 | .toast-box { 72 | text-align: left; 73 | margin-top: 30px; 74 | position: absolute; 75 | width: 100%; 76 | top: 0; 77 | display: flex; 78 | flex-direction: row; 79 | justify-content: center; 80 | } 81 | 82 | .full-height { 83 | height: 100vh; 84 | flex-direction: column; 85 | display: flex; 86 | } 87 | 88 | .full-height .container { 89 | flex: 1; 90 | } 91 | 92 | .container .row { 93 | height: 100%; 94 | } 95 | 96 | .flex-column { 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | 101 | .bg-white.flex-column { 102 | height: 100%; 103 | } 104 | 105 | .flex { 106 | flex: 1; 107 | } 108 | 109 | .logout-button { 110 | cursor: pointer; 111 | display: flex; 112 | flex-direction: row; 113 | align-items: center; 114 | padding: 15px 20px; 115 | } 116 | 117 | .logout-button svg { 118 | margin-right: 15px; 119 | } 120 | 121 | .no-messages { 122 | opacity: 0.5; 123 | height: 100%; 124 | width: 100%; 125 | } 126 | 127 | .avatar-box { 128 | width: 50px; 129 | height: 50px; 130 | object-fit: cover; 131 | object-position: 50%; 132 | overflow: hidden; 133 | border-radius: 4px; 134 | cursor: pointer; 135 | } 136 | 137 | .user-link { 138 | cursor: pointer; 139 | } 140 | 141 | .user-link:hover { 142 | text-decoration: underline; 143 | } 144 | 145 | .online-indicator { 146 | width: 14px; 147 | height: 14px; 148 | border: 2px solid white; 149 | bottom: -7px; 150 | right: -7px; 151 | background-color: #4df573; 152 | } 153 | 154 | .online-indicator.selected { 155 | border: none; 156 | width: 12px; 157 | height: 12px; 158 | bottom: -5px; 159 | right: -5px; 160 | } 161 | 162 | .online-indicator.offline { 163 | background-color: #bbb; 164 | } 165 | 166 | span.pseudo-link { 167 | font-size: 14px; 168 | text-decoration: underline; 169 | color: var(--blue); 170 | cursor: pointer; 171 | } 172 | 173 | span.pseudo-link:hover { 174 | text-decoration: none; 175 | } 176 | 177 | .list-group-item { 178 | cursor: pointer; 179 | height: 70px; 180 | box-sizing: border-box; 181 | transition: background-color 0.1s ease-out; 182 | } 183 | 184 | .chat-icon { 185 | width: 45px; 186 | height: 45px; 187 | border-radius: 4px; 188 | background-color: #eee; 189 | } 190 | 191 | .chat-icon.active { 192 | background-color: var(--blue); 193 | } 194 | 195 | .chats-title { 196 | font-size: 15px; 197 | } 198 | 199 | .chat-body { 200 | border-radius: 10px !important; 201 | } 202 | 203 | .chat-list-container { 204 | height: 100%; 205 | } 206 | 207 | .chat-input { 208 | border-radius: 30px !important; 209 | background-color: #eff2f7 !important; 210 | border-color: #eff2f7 !important; 211 | padding-right: 120px; 212 | } 213 | 214 | .form-control::placeholder { 215 | font-size: 13px; 216 | } 217 | 218 | .form-control { 219 | display: block; 220 | width: 100%; 221 | height: calc(1.5em + 0.94rem + 2px); 222 | padding: 7.5px 12px; 223 | font-size: 13px; 224 | font-weight: 400; 225 | line-height: 1.5; 226 | color: #495057; 227 | background-color: #fff; 228 | background-clip: padding-box; 229 | border: 1px solid #ced4da; 230 | -webkit-transition: border-color 0.15s ease-in-out, 231 | -webkit-box-shadow 0.15s ease-in-out; 232 | transition: border-color 0.15s ease-in-out, 233 | -webkit-box-shadow 0.15s ease-in-out; 234 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 235 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 236 | -webkit-box-shadow 0.15s ease-in-out; 237 | } 238 | 239 | .rounded-button { 240 | border-radius: 30px; 241 | background-color: var(--light); 242 | } 243 | -------------------------------------------------------------------------------- /client/src/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getUsers } from "./api"; 4 | 5 | /** 6 | * @param {string[]} names 7 | * @param {string} username 8 | */ 9 | export const parseRoomName = (names, username) => { 10 | for (let name of names) { 11 | if (typeof name !== 'string') { 12 | name = name[0]; 13 | } 14 | if (name !== username) { 15 | return name; 16 | } 17 | } 18 | return names[0]; 19 | }; 20 | 21 | /** Get an avatar for a room or a user */ 22 | export const getAvatarByUserAndRoomId = (roomId = "1") => { 23 | const TOTAL_IMAGES = 13; 24 | const seed1 = 654; 25 | const seed2 = 531; 26 | 27 | const uidParsed = +roomId.split(":").pop(); 28 | let roomIdParsed = +roomId.split(":").reverse().pop(); 29 | if (roomIdParsed < 0) { 30 | roomIdParsed += 3555; 31 | } 32 | 33 | const theId = (uidParsed * seed1 + roomIdParsed * seed2) % TOTAL_IMAGES; 34 | 35 | return `${process.env.PUBLIC_URL}/avatars/${theId}.jpg`; 36 | }; 37 | 38 | const jdenticon = require("jdenticon"); 39 | 40 | const avatars = {}; 41 | export const getAvatar = (username) => { 42 | let av = avatars[username]; 43 | if (av === undefined) { 44 | av = 45 | "data:image/svg+xml;base64," + window.btoa(jdenticon.toSvg(username, 50)); 46 | avatars[username] = av; 47 | } 48 | return av; 49 | }; 50 | 51 | export const populateUsersFromLoadedMessages = async (users, dispatch, messages) => { 52 | const userIds = {}; 53 | messages.forEach((message) => { 54 | userIds[message.from] = 1; 55 | }); 56 | 57 | const ids = Object.keys(userIds).filter( 58 | (id) => users[id] === undefined 59 | ); 60 | 61 | if (ids.length !== 0) { 62 | /** We need to fetch users first */ 63 | const newUsers = await getUsers(ids); 64 | dispatch({ 65 | type: "append users", 66 | payload: newUsers, 67 | }); 68 | } 69 | 70 | }; -------------------------------------------------------------------------------- /client/src/websockets/config.js: -------------------------------------------------------------------------------- 1 | export const webSocketUrl = "ws"+(window.location.protocol.substr(0,5)==="https"?"s":"")+"://"+window.location.host+"/ws"; 2 | -------------------------------------------------------------------------------- /client/src/websockets/data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Will return data object for websocket send to backend 3 | * 4 | * function Data([argument, ...]) { 5 | * return { 6 | * : , 7 | * ... 8 | * } 9 | * } 10 | * 11 | * */ 12 | 13 | import {StorageGet, storageKeySessionUUID, storageKeyUserAccessKey, storageKeyUserUUID} from "./storage"; 14 | 15 | const dataTypeSignIn = "signIn"; 16 | const dataTypeSignOut = "signOut"; 17 | const dataTypeUsers = "users"; 18 | const dataTypeChannelJoin = "channelJoin"; 19 | const dataTypeChannelMessage = "channelMessage"; 20 | const dataTypeChannelLeave = "channelLeave"; 21 | 22 | export function DataSignIn(username, password) { 23 | return { 24 | SUUID: StorageGet(storageKeySessionUUID), 25 | type: dataTypeSignIn, 26 | signIn: { 27 | username: username, 28 | password: password 29 | } 30 | } 31 | } 32 | 33 | export function DataUsers() { 34 | return { 35 | SSUID: StorageGet(storageKeySessionUUID), 36 | type: dataTypeUsers, 37 | userUUID: StorageGet(storageKeyUserUUID), 38 | accessKey: StorageGet(storageKeyUserAccessKey), 39 | } 40 | } 41 | 42 | export function DataChannelJoin(recipientUUID) { 43 | return { 44 | SUUID: StorageGet(storageKeySessionUUID), 45 | type: dataTypeChannelJoin, 46 | userUUID: StorageGet(storageKeyUserUUID), 47 | accessKey: StorageGet(storageKeyUserAccessKey), 48 | channelJoin: { 49 | recipientUUID: recipientUUID 50 | } 51 | } 52 | } 53 | 54 | export function DataChannelLeave(recipientUUID) { 55 | return { 56 | SUUID: StorageGet(storageKeySessionUUID), 57 | type: dataTypeChannelLeave, 58 | userUUID: StorageGet(storageKeyUserUUID), 59 | userAccessKey: StorageGet(storageKeyUserAccessKey), 60 | channelLeave: { 61 | recipientUUID: recipientUUID, 62 | senderUUID: StorageGet(storageKeyUserUUID) 63 | } 64 | } 65 | } 66 | 67 | export function DataChannelMessage(recipientUUID, message) { 68 | return { 69 | SUUID: StorageGet(storageKeySessionUUID), 70 | type: dataTypeChannelMessage, 71 | userUUID: StorageGet(storageKeyUserUUID), 72 | accessKey: StorageGet(storageKeyUserAccessKey), 73 | channelMessage: { 74 | recipientUUID: recipientUUID, 75 | message: message 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /client/src/websockets/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Websocket onmessage event handlers 3 | * 4 | * ws.onmessage event has one argument `event`, 5 | * we should read `event.data` value, it contained message from backend 6 | * 7 | * `event.data` is stringify JSON object, each of received JSON object contained `type:""` property 8 | * 9 | * `messages` - is a key:value map, where: 10 | * - key: received message type from websocket, message type contained in `event.data.type` 11 | * - value: handler function for processing received message 12 | * */ 13 | 14 | import {storageKeySessionUUID, storageKeyUserAccessKey, storageKeyUserUUID, StorageSet} from "./storage"; 15 | import {viewMessagesAdd, viewShowPageChat, viewUsersAdd, viewUsersClean} from "./view"; 16 | import useAppStateContext from '../state'; 17 | 18 | export const messages = { 19 | // system message from backend, usually it contained system info for debug 20 | "sys": sys, 21 | // error from backend when backend received message and can't processed it 22 | "error": error, 23 | // backend return session UUID when websocket connection successful 24 | "ready": ready, 25 | // backend return user data on signIn successful 26 | "authorized": authorized, 27 | // backend return all users 28 | "users": users, 29 | // backend said that somebody joined to channel 30 | "channelJoin": channelJoin, 31 | // backend said that channel accepted new message 32 | "channelMessage": channelMessage 33 | } 34 | 35 | const messagesSys = { 36 | "signIn": sysSignIn(), 37 | } 38 | 39 | function sys(data, {dispatch}) { 40 | if(typeof messagesSys[data.sys.type] == "function") { 41 | messagesSys[data.sys.type](data.sys); 42 | } else { 43 | console.log("Unknown message sys.type: " + data.sys.type) 44 | } 45 | if (data.sys.signIn) 46 | dispatch({type: 'set user', payload: data.sys.signIn}); 47 | } 48 | 49 | function error(data) { 50 | console.log("error: " + data.error.code + " - " + data.error.message, data) 51 | } 52 | 53 | function ready(data) { 54 | StorageSet(storageKeySessionUUID, data.ready.sessionUUID); 55 | } 56 | 57 | function authorized(data) { 58 | StorageSet(storageKeyUserUUID,data.authorized.userUUID); 59 | StorageSet(storageKeyUserAccessKey, data.authorized.accessKey); 60 | } 61 | 62 | 63 | function users(data, {dispatch}) { 64 | dispatch({type: 'set users', payload: data.users.users}) 65 | dispatch({type: 'set rooms', payload: data.users.users}) 66 | } 67 | 68 | function channelJoin(data, {dispatch}) { 69 | dispatch({type: 'set messages', payload: data.channelJoin.messages || []}); 70 | dispatch({type: 'set users', payload: data.channelJoin.users}); 71 | } 72 | 73 | function channelMessage(data, {dispatch}) { 74 | console.log('append message', data); 75 | dispatch({type: 'append message', payload: {id: data.channelMessage.RecipientUUID, message: data.channelMessage}}); 76 | } 77 | 78 | function sysSignIn(data){ 79 | console.log('sysSignIn') 80 | } 81 | -------------------------------------------------------------------------------- /client/src/websockets/process.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Basic processing examples 3 | * 4 | * You can compare websocket messages sending or receiving together in 5 | * specific functions for make custom processing flow 6 | * 7 | * */ 8 | 9 | import {webSocketSend} from "../hooks"; 10 | import {DataChannelJoin, DataChannelLeave, DataChannelMessage, DataSignIn, DataUsers} from "./data"; 11 | import {node} from "prop-types"; 12 | 13 | let inputSignInUsername = null; 14 | let inputSignInPassword = null; 15 | let inputMessage = null; 16 | 17 | const nodeIdInputSignInUsername = "input-username"; 18 | const nodeIdInputSignInPassword = "input-password"; 19 | const nodeIdInputMessage = "input-message"; 20 | 21 | let selectedRecipientUUID = ""; 22 | 23 | // signIn flow 24 | export function processSignIn(username,password,setShowLogin) { 25 | let process = new Promise((resolve, reject) => { 26 | webSocketSend(DataSignIn(username, password)); 27 | resolve(); 28 | }) 29 | 30 | process.then(() => {webSocketSend(DataUsers())}); 31 | process.then(() => {webSocketSend(DataChannelJoin(""));}); 32 | process.catch((err) => {console.log(err)}); 33 | 34 | setShowLogin(false) 35 | return ; 36 | } 37 | 38 | export function processChannelJoin(recipientUUID) { 39 | let process = new Promise((resolve, reject) => { 40 | webSocketSend(DataChannelJoin(recipientUUID)); 41 | resolve(); 42 | }); 43 | process.then(() => {selectedRecipientUUID = recipientUUID;}) 44 | process.catch((err) => console.log(err)); 45 | } 46 | 47 | export function processChannelLeave() { 48 | let process = new Promise((resolve, reject) => { 49 | webSocketSend(DataChannelLeave(selectedRecipientUUID)); 50 | resolve(); 51 | }); 52 | process.catch((err) => console.log('error', err)); 53 | } 54 | 55 | export function processChannelMessage(inputMessage) { 56 | if(inputMessage == null) { 57 | inputMessage = node(nodeIdInputMessage); 58 | } 59 | console.log('channelMessage', inputMessage) 60 | let process = new Promise((resolve, reject) => webSocketSend(DataChannelMessage(selectedRecipientUUID, inputMessage))); 61 | process.then(() => {}); 62 | process.catch((err) => console.log(err)); 63 | return false; 64 | } 65 | -------------------------------------------------------------------------------- /client/src/websockets/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Data storage for store data (session uuid, user uuid, access key, etc.) and reuse it, as example: localStorage 4 | * 5 | * */ 6 | const storage = window.localStorage; 7 | 8 | export const storageKeySessionUUID = "session.uuid"; 9 | 10 | export const storageKeyUserUUID = "user.uuid"; 11 | export const storageKeyUserAccessKey = "user.accessKey"; 12 | 13 | export function StorageSet(key, value) { 14 | storage.setItem(key, value); 15 | } 16 | 17 | export function StorageGet(key) { 18 | const value = storage.getItem(key); 19 | return value == null?"":value; 20 | } 21 | -------------------------------------------------------------------------------- /client/src/websockets/view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Simple page rendering and DOM/view changes depend on websocket processing 4 | * 5 | * */ 6 | import {StorageGet, storageKeyUserUUID} from "./storage"; 7 | 8 | let viewNodePageSignIn = null; 9 | let viewNodePageChat = null; 10 | 11 | let viewNodeSys = null; 12 | let viewNodeError = null; 13 | let viewNodeUsers = null; 14 | let viewNodeMessages = null; 15 | 16 | const nodeIdPageSignIn = "page-signIn" 17 | const nodeIdPageChat = "page-chat" 18 | 19 | const nodeIdSys = "view-sys"; 20 | const nodeIdUsers = "view-users"; 21 | const nodeIdErrors = "view-errors"; 22 | const nodeIdMessages = "view-messages"; 23 | 24 | const node = (id) => {return document.getElementById(id);} 25 | 26 | export function viewPagesInitialize() { 27 | if(viewNodePageSignIn == null){ 28 | viewNodePageSignIn = node(nodeIdPageSignIn); 29 | } 30 | if(viewNodePageChat == null){ 31 | viewNodePageChat = node(nodeIdPageChat); 32 | } 33 | } 34 | 35 | export function viewShowPageSignIn() { 36 | viewPagesInitialize(); 37 | viewNodePageChat.style.display = "none"; 38 | viewNodePageSignIn.style.display = "block"; 39 | } 40 | 41 | 42 | 43 | 44 | export function viewSysAdd(message) { 45 | if(viewNodeSys == null) { 46 | viewNodeSys =node(nodeIdSys); 47 | } 48 | 49 | viewNodeSys.innerHTML = '
'+message+'
' + viewNodeSys.innerHTML; 50 | } 51 | 52 | export function viewErrorAdd(message) { 53 | if(viewNodeError == null){ 54 | viewNodeError = node(nodeIdErrors); 55 | } 56 | } 57 | 58 | 59 | 60 | 61 | export function buildNodeMessage(message) { 62 | let divSender = ""; 63 | if(typeof message.Sender != "undefined") { 64 | divSender = '
' + message.Sender.Username + '
'; 65 | } 66 | let divRecipient = ""; 67 | if(typeof message.Recipient != "undefined") { 68 | divRecipient = '
' + message.Recipient.Username + '
'; 69 | } 70 | if(divSender === "" && divRecipient === "") { 71 | return false; 72 | } 73 | let div = document.createElement("div"); 74 | div.id = "message-" + message.UUID; 75 | div.innerHTML = divSender + divRecipient + 76 | '
' + message.Message + '
' + 77 | '
' + message.CreatedAt + '
' ; 78 | 79 | return div; 80 | } 81 | 82 | /* 83 | * @params message Object 84 | * */ 85 | export function viewMessagesAdd(message) { 86 | if(viewNodeMessages == null){ 87 | viewNodeMessages = node(nodeIdMessages); 88 | } 89 | 90 | const messageNode = buildNodeMessage(message); 91 | 92 | if(messageNode === false) { 93 | return; 94 | } 95 | const nodes = viewNodeMessages.childNodes; 96 | if(nodes > 0){ 97 | viewNodeMessages.insertBefore(nodes[0], messageNode); 98 | } else { 99 | viewNodeMessages.append(messageNode); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "github.com/joho/godotenv/autoload" 5 | "os" 6 | ) 7 | 8 | const ( 9 | envNameServerAddress = "SERVER_ADDRESS" 10 | envNameClientLocation = "CLIENT_LOCATION" 11 | envNameRedisAddress = "REDIS_ADDRESS" 12 | envNameRedisPassword = "REDIS_PASSWORD" 13 | 14 | defaultRedisAddress = "localhost:6379" 15 | defaultServerAddress = ":40080" 16 | defaultClientLocation = "/usr/local/share/dinamicka/public" 17 | ) 18 | 19 | type Config struct { 20 | ServerAddress string 21 | ClientLocation string 22 | RedisAddress string 23 | RedisPassword string 24 | } 25 | 26 | func NewConfig() *Config { 27 | 28 | addr := os.Getenv(envNameServerAddress) 29 | if port := os.Getenv("PORT"); port != "" { 30 | addr = ":" + port 31 | } 32 | 33 | config := &Config{ 34 | ServerAddress: addr, 35 | ClientLocation: os.Getenv(envNameClientLocation), 36 | RedisAddress: os.Getenv(envNameRedisAddress), 37 | RedisPassword: os.Getenv(envNameRedisPassword), 38 | } 39 | if config.ServerAddress == "" { 40 | config.ServerAddress = defaultServerAddress 41 | } 42 | if config.ClientLocation == "" { 43 | config.ClientLocation = defaultClientLocation 44 | } 45 | if config.RedisAddress == "" { 46 | config.RedisAddress = defaultRedisAddress 47 | } 48 | 49 | return config 50 | } 51 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | redis: 4 | image: redis:5 5 | container_name: chat-redis 6 | hostname: chat-redis 7 | restart: always 8 | networks: 9 | - chat-demo 10 | api: 11 | build: 12 | context: . 13 | env_file: 14 | - .env 15 | image: chat-api 16 | container_name: chat-api 17 | ports: 18 | - 5000:5000 19 | restart: always 20 | depends_on: 21 | - redis 22 | networks: 23 | - chat-demo 24 | networks: 25 | chat-demo: 26 | driver: bridge 27 | 28 | -------------------------------------------------------------------------------- /docs/YTThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/docs/YTThumbnail.png -------------------------------------------------------------------------------- /docs/screenshot000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/docs/screenshot000.png -------------------------------------------------------------------------------- /docs/screenshot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/docs/screenshot001.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis-developer/basic-redis-chat-demo-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.9+incompatible 7 | github.com/gobwas/httphead v0.1.0 // indirect 8 | github.com/gobwas/pool v0.2.1 // indirect 9 | github.com/gobwas/ws v1.0.4 10 | github.com/google/go-cmp v0.5.4 // indirect 11 | github.com/google/uuid v1.2.0 12 | github.com/joho/godotenv v1.3.0 13 | github.com/kr/pretty v0.1.0 // indirect 14 | github.com/onsi/ginkgo v1.15.0 // indirect 15 | github.com/onsi/gomega v1.10.5 // indirect 16 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect 17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 2 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 3 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 4 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 5 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 6 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 7 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 8 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 9 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 10 | github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs= 11 | github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 14 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 15 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 16 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 17 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 18 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 19 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 20 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 24 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 26 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 28 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 29 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 30 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 31 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 36 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 37 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 38 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 39 | github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= 40 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= 41 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 42 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 43 | github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= 44 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 45 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 46 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 47 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 48 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 49 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 50 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 51 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 52 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 53 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 54 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 55 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 56 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 57 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 58 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= 71 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 77 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 78 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 79 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 84 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 85 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 86 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 87 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 88 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 89 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 95 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 96 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 97 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 99 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | -------------------------------------------------------------------------------- /golang-chat-redis.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Golang Chat Redis Service 3 | #After= 4 | 5 | [Service] 6 | User=chat-demo 7 | Group=chat-demo 8 | LimitAS=infinity 9 | LimitRSS=infinity 10 | LimitCORE=infinity 11 | LimitNOFILE=65536 12 | WorkingDirectory=/usr/local/share/chat-demo/ 13 | ExecStart=/usr/local/share/chat-demo/bin 14 | Restart=always 15 | RestartSec=5s 16 | StandardOutput=journal 17 | StandardError=journal 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/ccdab229a04b0ad02a4d75b479b57081aea093b9/images/app_preview_image.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/redis-developer/basic-redis-chat-demo-go/config" 6 | "github.com/redis-developer/basic-redis-chat-demo-go/message" 7 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 8 | "github.com/redis-developer/basic-redis-chat-demo-go/websocket" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | 15 | cnf := config.NewConfig() 16 | 17 | log.Println(fmt.Sprintf("%+v", cnf)) 18 | redisCli := rediscli.NewRedis(cnf.RedisAddress, cnf.RedisPassword) 19 | messageController := message.NewController(redisCli) 20 | http.Handle("/ws", websocket.Handler(redisCli, messageController)) 21 | http.HandleFunc("/links", func(writer http.ResponseWriter, request *http.Request) { 22 | _,_ = writer.Write([]byte(`{"github":"https://github.com/redis-developer/basic-redis-chat-demo-go"}`)) 23 | }) 24 | http.Handle("/", http.FileServer(http.Dir(cnf.ClientLocation))) 25 | log.Fatal(http.ListenAndServe(cnf.ServerAddress, nil)) 26 | } 27 | 28 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Basic Redis chat app in Golang", 3 | "description": "Showcases how to implement chat app in Golang", 4 | "type": "App", 5 | "contributed_by": "Redis", 6 | "repo_url": "https://github.com/redis-developer/basic-redis-chat-demo-go", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/basic-redis-chat-demo-go/master/images/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/basic-redis-chat-demo-go/archive/main.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "true", 11 | "deploy_buttons": [ 12 | { 13 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/basic-redis-chat-demo-go" 14 | }, 15 | { 16 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-chat-demo-go.git" 17 | } 18 | ], 19 | "language": [ 20 | "Go" 21 | ], 22 | "redis_commands": [ 23 | "SET", 24 | "SETEX", 25 | "GET", 26 | "DEL", 27 | "RPUSH", 28 | "LINDEX", 29 | "LLEN", 30 | "LRANGE", 31 | "SUBSCRIBE", 32 | "PUBLISH" 33 | ], 34 | "redis_use_cases": [ 35 | "Pub/Sub" 36 | ], 37 | "redis_features": [], 38 | "app_image_urls": [ 39 | "https://github.com/redis-developer/basic-redis-chat-demo-go/raw/master/docs/screenshot001.png" 40 | ], 41 | "youtube_url": "https://www.youtube.com/watch?v=miK7xDkDXF0", 42 | "special_tags": [], 43 | "verticals": [], 44 | "markdown": "https://github.com/redis-developer/basic-redis-chat-demo-go/raw/master/README.md" 45 | } -------------------------------------------------------------------------------- /message/controller.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 4 | 5 | type Controller struct { 6 | r *rediscli.Redis 7 | } 8 | 9 | func NewController(r *rediscli.Redis) *Controller { 10 | return &Controller{ 11 | r: r, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /message/error.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "errors" 4 | 5 | type IError interface { 6 | Error() (uint32, error) 7 | } 8 | type Error struct { 9 | code uint32 10 | err error 11 | } 12 | 13 | func (err Error) Error() (uint32, error) { 14 | return err.code, err.err 15 | } 16 | 17 | func newError(errCode uint32, err error) IError { 18 | return Error{code: errCode, err: err} 19 | } 20 | 21 | const ( 22 | errCodeOK uint32 = iota 23 | errCodeJSUnmarshal 24 | errCodeWSRead 25 | errCodeWSWrite 26 | errCodeSignIn 27 | errCodeSignUp 28 | errCodeSignOut 29 | errCodeRedisChannelMessage 30 | errCodeRedisChannelUsers 31 | errCodeRedisGetSessionUUID 32 | errCodeRedisGetUserByUUID 33 | errCodeRedisChannelJoin 34 | errCodeUserSetOnline 35 | ) 36 | 37 | var ( 38 | errWSRead = errors.New("could not read websocket connection") 39 | errSignIn = errors.New("could not signIn") 40 | errUserSetOnline = errors.New("could not set user status as OnLine") 41 | ) 42 | -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gobwas/ws" 6 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 7 | "io" 8 | "log" 9 | "net" 10 | "sync" 11 | ) 12 | 13 | type ( 14 | DataType string 15 | ) 16 | 17 | const ( 18 | DataTypeSys DataType = "sys" 19 | DataTypeReady DataType = "ready" 20 | DataTypeError DataType = "error" 21 | DataTypeUsers DataType = "users" 22 | DataTypeSignIn DataType = "signIn" 23 | DataTypeSignUp DataType = "signUp" 24 | DataTypeSignOut DataType = "signOut" 25 | DataTypeAuthorized DataType = "authorized" 26 | DataTypeUnAuthorized DataType = "unauthorized" 27 | DataTypeChannelJoin DataType = "channelJoin" 28 | DataTypeChannelMessage DataType = "channelMessage" 29 | DataTypeChannelMessages DataType = "channelMessages" 30 | DataTypeChannelLeave DataType = "channelLeave" 31 | ) 32 | 33 | type Message struct { 34 | recipientsSessionUUID []string 35 | SUUID string `json:"SUUID,omitempty"` 36 | Type DataType `json:"type"` 37 | UserUUID string `json:"userUUID,omitempty"` 38 | User *rediscli.User `json:"user,omitempty"` 39 | UserAccessKey string `json:"userAccessKey,omitempty"` 40 | Sys *DataSys `json:"sys,omitempty"` 41 | Ready *DataReady `json:"ready,omitempty"` 42 | Error *DataError `json:"error,omitempty"` 43 | Users *DataUsers `json:"users,omitempty"` 44 | SignIn *DataSignIn `json:"signIn,omitempty"` 45 | SignUp *DataSignUp `json:"signUp,omitempty"` 46 | SignOut *DataSignOut `json:"signOut,omitempty"` 47 | Authorized *DataAuthorized `json:"authorized,omitempty"` 48 | ChannelJoin *DataChannelJoin `json:"channelJoin,omitempty"` 49 | ChannelMessage *DataChannelMessage `json:"channelMessage,omitempty"` 50 | ChannelLeave *DataChannelLeave `json:"channelLeave,omitempty"` 51 | } 52 | 53 | type DataAuthorized struct { 54 | UserUUID string `json:"userUUID"` 55 | AccessKey string `json:"accessKey"` 56 | } 57 | 58 | type DataUnAuthorized struct { 59 | UserUUID string `json:"userUUID"` 60 | AccessKey string `json:"accessKey"` 61 | } 62 | 63 | type Channel struct { 64 | conn net.Conn 65 | userUUID string 66 | } 67 | 68 | var channelSessionsJoins = map[string]map[string]Channel{} 69 | var channelSessionsSync = &sync.RWMutex{} 70 | 71 | var sessionChannel = map[string]string{} 72 | var sessionChannelSync = &sync.RWMutex{} 73 | 74 | func channelSessionsAdd(conn net.Conn, channelUUID, sessionUUID, userUUID string) { 75 | channelSessionsSync.Lock() 76 | if _, ok := channelSessionsJoins[channelUUID]; !ok { 77 | channelSessionsJoins[channelUUID] = make(map[string]Channel, 0) 78 | } 79 | channelSessionsJoins[channelUUID][sessionUUID] = Channel{conn: conn, userUUID: userUUID} 80 | channelSessionsSync.Unlock() 81 | 82 | sessionChannelSync.Lock() 83 | sessionChannel[sessionUUID] = channelUUID 84 | sessionChannelSync.Unlock() 85 | 86 | } 87 | 88 | func channelSessionsRemove(sessionUUID string) { 89 | sessionChannelSync.RLock() 90 | channelUUID := sessionChannel[sessionUUID] 91 | sessionChannelSync.RUnlock() 92 | if channelUUID == "" { 93 | return 94 | } 95 | channelSessionsSync.Lock() 96 | if _, ok := channelSessionsJoins[channelUUID]; ok { 97 | delete(channelSessionsJoins[channelUUID], sessionUUID) 98 | } 99 | channelSessionsSync.Unlock() 100 | } 101 | 102 | type Write func(conn io.ReadWriter, op ws.OpCode, message *Message) error 103 | 104 | func channelSessionsSendMessage(skipUserUUID, channelUUID string, write Write, message *Message) { 105 | 106 | channelSessionsSync.RLock() 107 | defer channelSessionsSync.RUnlock() 108 | for _, data := range channelSessionsJoins[channelUUID] { 109 | if skipUserUUID != "" && skipUserUUID == data.userUUID { 110 | log.Println(">>>>>>>>>>>>SKIP",skipUserUUID, fmt.Sprintf("%+v", message)) 111 | continue 112 | } 113 | log.Println(">>>>>>>>>>>>SEND",skipUserUUID, fmt.Sprintf("%+v", message)) 114 | if err := write(data.conn, ws.OpText, message); err != nil { 115 | log.Println(err) 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /message/message_channel_join.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/gobwas/ws" 5 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 6 | "log" 7 | "net" 8 | ) 9 | 10 | type DataChannelJoin struct { 11 | RecipientUUID string `json:"recipientUUID,omitempty"` 12 | Messages []*rediscli.Message `json:"messages,omitempty"` 13 | Users []*rediscli.User `json:"users,omitempty"` 14 | } 15 | 16 | func (p Controller) ChannelJoin(sessionUUID string,conn net.Conn, op ws.OpCode, write Write, message *Message) (*rediscli.ChannelPubSub, IError) { 17 | 18 | errI := p.ChannelLeave(sessionUUID, write, &Message{ 19 | SUUID: message.SUUID, 20 | Type: DataTypeChannelLeave, 21 | UserUUID: message.UserUUID, 22 | ChannelLeave: &DataChannelLeave{ 23 | RecipientUUID: message.ChannelJoin.RecipientUUID, 24 | }, 25 | }) 26 | if errI != nil { 27 | log.Println(errI) 28 | } 29 | 30 | channelSessionsRemove(sessionUUID) 31 | user,err := p.r.UserGet(message.UserUUID) 32 | if err != nil { 33 | return nil, newError(100, err) 34 | } 35 | 36 | channel, channelUUID, err := p.r.ChannelJoin(message.UserUUID, message.ChannelJoin.RecipientUUID) 37 | if err != nil { 38 | return nil, newError(101, err) 39 | } 40 | 41 | messagesLen, err := p.r.ChannelMessagesCount(channelUUID) 42 | if err != nil { 43 | return nil, newError(111,err) 44 | } 45 | 46 | var offset int64 47 | var limit int64 = 10 48 | 49 | if messagesLen > limit { 50 | offset = messagesLen - 10 51 | limit = -1 52 | } 53 | 54 | channelMessages, err := p.r.ChannelMessages(channelUUID, offset, limit) 55 | if err != nil { 56 | return nil, newError(102, err) 57 | } 58 | 59 | channelUsers, err := p.r.ChannelUsers(channelUUID) 60 | if err != nil { 61 | return nil, newError(103, err) 62 | } 63 | 64 | channelSessionsAdd(conn, channelUUID, sessionUUID, message.UserUUID) 65 | 66 | err = write(conn, op, &Message{ 67 | Type: DataTypeChannelJoin, 68 | ChannelJoin: &DataChannelJoin{ 69 | RecipientUUID: message.ChannelJoin.RecipientUUID, 70 | Messages: channelMessages, 71 | Users: channelUsers, 72 | }, 73 | }) 74 | if err != nil { 75 | return nil, newError(104, err) 76 | } 77 | 78 | channelSessionsSendMessage("", channelUUID, write, &Message{ 79 | Type: DataTypeSys, 80 | SUUID: sessionUUID, 81 | UserUUID: message.UserUUID, 82 | User: user, 83 | Sys: &DataSys{ 84 | Type: DataTypeChannelJoin, 85 | ChannelJoin: &DataChannelJoin{ 86 | RecipientUUID: message.ChannelJoin.RecipientUUID, 87 | }, 88 | }, 89 | }) 90 | 91 | return channel, nil 92 | } 93 | -------------------------------------------------------------------------------- /message/message_channel_leave.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type DataChannelLeave struct { 4 | SenderUUID string `json:"senderUUID"` 5 | RecipientUUID string `json:"recipientUUID"` 6 | } 7 | 8 | func (p Controller) ChannelLeave(sessionUUID string, writer Write, message *Message) IError { 9 | 10 | channelUUID, err := p.r.ChannelLeave(message.UserUUID, message.ChannelLeave.RecipientUUID) 11 | if err != nil { 12 | return newError(0, err) 13 | } 14 | 15 | channelSessionsSendMessage("", channelUUID, writer, message) 16 | channelSessionsRemove(sessionUUID) 17 | 18 | return nil 19 | 20 | } 21 | -------------------------------------------------------------------------------- /message/message_channel_message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/gobwas/ws" 5 | "github.com/google/uuid" 6 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 7 | "net" 8 | "time" 9 | ) 10 | 11 | type DataChannelMessage struct { 12 | UUID string `json:"UUID"` 13 | Sender *rediscli.User `json:"Sender,omitempty"` 14 | SenderUUID string `json:"SenderUUID"` 15 | Recipient *rediscli.User `json:"Recipient,omitempty"` 16 | RecipientUUID string `json:"RecipientUUID"` 17 | Message string `json:"Message"` 18 | CreatedAt time.Time `json:"CreatedAt"` 19 | } 20 | 21 | func (p Controller) ChannelMessage(sessionUUID string, conn net.Conn, op ws.OpCode, writer Write, message *Message) IError { 22 | 23 | channelMessage := &rediscli.Message{ 24 | UUID: uuid.NewString(), 25 | SenderUUID: message.UserUUID, 26 | RecipientUUID: message.ChannelMessage.RecipientUUID, 27 | Message: message.ChannelMessage.Message, 28 | CreatedAt: time.Now(), 29 | } 30 | 31 | channelUUID, err := p.r.ChannelMessage(channelMessage) 32 | if err != nil { 33 | return nil 34 | } 35 | 36 | channelSessionsSendMessage(message.UserUUID, channelUUID, writer, message) 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /message/message_error.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type DataError struct { 4 | Code uint32 `json:"code"` 5 | Error string `json:"error"` 6 | Payload interface{} `json:"payload"` 7 | } 8 | 9 | func (p Controller) Error(code uint32, err error, sessionUID string, payload interface{}) *Message { 10 | return &Message{ 11 | recipientsSessionUUID: []string{sessionUID}, 12 | Type: DataTypeError, 13 | Error: &DataError{ 14 | Code: code, 15 | Error: err.Error(), 16 | Payload: payload, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /message/message_ready.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type DataReady struct { 4 | SessionUUID string `json:"sessionUUID"` 5 | } 6 | 7 | func (p Controller) Ready(sessionUUID string) *Message { 8 | return &Message{ 9 | Type: DataTypeReady, 10 | Ready: &DataReady{ 11 | SessionUUID: sessionUUID, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /message/message_signin.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gobwas/ws" 6 | "log" 7 | "net" 8 | "sync" 9 | ) 10 | 11 | type DataSignIn struct { 12 | UUID string `json:"uuid"` 13 | Username string `json:"username"` 14 | Password string `json:"password,omitempty"` 15 | } 16 | 17 | var usersConn = map[string]net.Conn{} 18 | var usersConnSync = &sync.RWMutex{} 19 | 20 | func (p Controller) SignIn(sessionUUID string, conn net.Conn, op ws.OpCode, write Write, message *Message) IError { 21 | 22 | log.Println("SignIn", sessionUUID, fmt.Sprintf("%+v", message)) 23 | 24 | user, err := p.r.UserAuthorize(message.SignIn.Username, message.SignIn.Password) 25 | if err != nil { 26 | log.Println("SignIn", err) 27 | return newError(errCodeSignIn, err) 28 | } 29 | 30 | err = write(conn, op, &Message{ 31 | Type: DataTypeAuthorized, 32 | Authorized: &DataAuthorized{ 33 | UserUUID: user.UUID, 34 | AccessKey: user.AccessKey, 35 | }, 36 | }) 37 | if err != nil { 38 | return newError(0, err) 39 | } 40 | 41 | err = p.r.UserSetOnline(user.UUID) 42 | if err != nil { 43 | log.Println(fmt.Errorf("%s:%w", errUserSetOnline, err), sessionUUID, message) 44 | } 45 | 46 | usersConnSync.Lock() 47 | usersConn[message.UserUUID] = conn 48 | for _, conn := range usersConn { 49 | err := write(conn, op, p.SysSignIn(user)) 50 | if err != nil { 51 | log.Println(err) 52 | } 53 | } 54 | usersConnSync.Unlock() 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /message/message_signout.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/gobwas/ws" 5 | "net" 6 | ) 7 | 8 | type DataSignOut struct { 9 | UUID string `json:"uuid"` 10 | } 11 | 12 | func (p Controller) SignOut(sessionUUID string, conn net.Conn, op ws.OpCode, write Write, message *Message) IError { 13 | _, err := p.r.UserGet(message.UserUUID) 14 | if err != nil { 15 | return newError(errCodeSignOut, err) 16 | } 17 | 18 | p.r.UserSignOut(message.UserUUID) 19 | 20 | err = write(conn, op, &Message{ 21 | Type: DataTypeSignOut, 22 | SignOut: &DataSignOut{ 23 | UUID: message.SignOut.UUID, 24 | }, 25 | }) 26 | if err != nil { 27 | return newError(0, err) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /message/message_signup.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/gobwas/ws" 5 | "net" 6 | ) 7 | 8 | type DataSignUp struct { 9 | Username string `json:"username"` 10 | Password string `json:"password"` 11 | } 12 | 13 | func (p Controller) SignUp(sessionUUID string, conn net.Conn, op ws.OpCode, write Write, message *Message) IError { 14 | user, err := p.r.UserCreate(message.SignUp.Username, message.SignUp.Password) 15 | if err != nil { 16 | return newError(0, err) 17 | } 18 | 19 | err = write(conn, op, &Message{ 20 | Type: DataTypeAuthorized, 21 | Authorized: &DataAuthorized{ 22 | UserUUID: user.UUID, 23 | AccessKey: user.AccessKey, 24 | }, 25 | }) 26 | if err != nil { 27 | return newError(0, err) 28 | } 29 | 30 | return nil 31 | 32 | } 33 | -------------------------------------------------------------------------------- /message/message_sys.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 4 | 5 | type DataSys struct { 6 | Type DataType `json:"type"` 7 | Message string `json:"message,omitempty"` 8 | SignIn *DataSignIn `json:"signIn,omitempty"` 9 | ChannelJoin *DataChannelJoin `json:"channelJoin,omitempty"` 10 | ChannelLeave *DataChannelLeave `json:"channelLeave,omitempty"` 11 | } 12 | 13 | func SysMessage(message string) *Message { 14 | return &Message{ 15 | Type: DataTypeSys, 16 | Sys: &DataSys{ 17 | Type: DataTypeSys, 18 | Message: message, 19 | }, 20 | } 21 | } 22 | 23 | func (p Controller) SysSignIn(user *rediscli.User) *Message { 24 | return &Message{ 25 | Type: DataTypeSys, 26 | Sys: &DataSys{ 27 | Type: DataTypeSignIn, 28 | SignIn: &DataSignIn{ 29 | UUID: user.UUID, 30 | Username: user.Username, 31 | }, 32 | }, 33 | } 34 | } 35 | 36 | func (p Controller) SysChannelJoin(user *rediscli.User, recipientsUUID []string) *Message { 37 | return &Message{ 38 | recipientsSessionUUID: recipientsUUID, 39 | Type: DataTypeSys, 40 | Sys: &DataSys{ 41 | Type: DataTypeSignIn, 42 | SignIn: &DataSignIn{ 43 | UUID: user.UUID, 44 | Username: user.Username, 45 | }, 46 | }, 47 | } 48 | } 49 | 50 | func (p Controller) SysChannelLeave(user *rediscli.User, recipientsUUID []string) *Message { 51 | return &Message{ 52 | recipientsSessionUUID: recipientsUUID, 53 | Type: DataTypeSys, 54 | Sys: &DataSys{ 55 | Type: DataTypeChannelLeave, 56 | ChannelLeave: &DataChannelLeave{ 57 | SenderUUID: user.UUID, 58 | }, 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /message/message_users.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "github.com/gobwas/ws" 6 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 7 | "net" 8 | ) 9 | 10 | type DataUsers struct { 11 | Total int `json:"total"` 12 | Received int `json:"received"` 13 | Users []*rediscli.User `json:"users"` 14 | } 15 | 16 | func (p *Controller) Users(sessionUUID string, conn net.Conn, op ws.OpCode, write Write) IError { 17 | 18 | values, err := p.r.UserAll() 19 | if err == redis.Nil { 20 | return nil 21 | } 22 | if err != nil { 23 | return newError(0, err) 24 | } 25 | 26 | users := make([]*rediscli.User, 0, len(values)) 27 | 28 | for i := range values { 29 | 30 | user := &rediscli.User{ 31 | UUID: values[i].UUID, 32 | Username: values[i].Username, 33 | OnLine: p.r.UserIsOnline(values[i].UUID), 34 | } 35 | users = append(users, user) 36 | } 37 | 38 | err = write(conn, op, &Message{ 39 | Type: DataTypeUsers, 40 | Users: &DataUsers{ 41 | Total: len(users), 42 | Received: len(users), 43 | Users: users, 44 | }, 45 | }) 46 | if err != nil { 47 | return newError(0, err) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /rediscli/channel.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/go-redis/redis" 9 | "github.com/google/uuid" 10 | "log" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | keyChannelUsers = "channelUsers" 17 | keyChannelMessages = "channelMessages" 18 | keyChannelSenderRecipient = "channelSenderRecipient" 19 | ) 20 | 21 | type Message struct { 22 | UUID string `json:"UUID"` 23 | SenderUUID string `json:"SenderUUID"` 24 | Sender *User `json:"Sender,omitempty"` 25 | RecipientUUID string `json:"RecipientUUID"` 26 | Recipient *User `json:"Recipient,omitempty"` 27 | Message string `json:"Message"` 28 | CreatedAt time.Time `json:"CreatedAt"` 29 | } 30 | 31 | func (r *Redis) getKeyChannelUsers(channelUUID string) string { 32 | return fmt.Sprintf("%s.%s", keyChannelUsers, channelUUID) 33 | } 34 | 35 | func (r *Redis) getKeyChannelMessages(channelUUID string) string { 36 | return fmt.Sprintf("%s.%s", keyChannelMessages, channelUUID) 37 | } 38 | 39 | func (r *Redis) getKeyChannelSenderRecipient(senderUUID, recipientUUID string) string { 40 | if recipientUUID == "" { 41 | recipientUUID = "public" 42 | } 43 | return fmt.Sprintf("%s.%s.%s", keyChannelSenderRecipient, senderUUID, recipientUUID) 44 | } 45 | 46 | func (r *Redis) getChannelUUID(senderUUID, recipientUUID string) (string, error) { 47 | if senderUUID == "" { 48 | return "", errors.New("empty sender UUID") 49 | } 50 | if recipientUUID == "" { 51 | return "public", nil 52 | } 53 | keySenderRecipient := r.getKeyChannelSenderRecipient(senderUUID, recipientUUID) 54 | channelUUID, err := r.client.Get(keySenderRecipient).Result() 55 | if err == redis.Nil { 56 | keyRecipientSender := r.getKeyChannelSenderRecipient(recipientUUID, senderUUID) 57 | channelUUID, err = r.client.Get(keyRecipientSender).Result() 58 | if err == redis.Nil { 59 | channelUUID = uuid.NewString() 60 | if err := r.client.Set(keySenderRecipient, channelUUID, 0).Err(); err != nil { 61 | return "", err 62 | } 63 | if err := r.client.Set(keyRecipientSender, channelUUID, 0).Err(); err != nil { 64 | return "", err 65 | } 66 | return channelUUID, nil 67 | } else if err != nil { 68 | return "", err 69 | } 70 | return channelUUID, nil 71 | } else if err != nil { 72 | return "", err 73 | } 74 | return channelUUID, nil 75 | } 76 | 77 | func (r *Redis) channelJoin(channelUUID, senderUUID, recipientUUID string) error { 78 | key := r.getKeyChannelUsers(channelUUID) 79 | if err := r.client.HSet(key, senderUUID, time.Now().String()).Err(); err != nil { 80 | return err 81 | } 82 | if recipientUUID != "" { 83 | if err := r.client.HSet(key, recipientUUID, time.Now().String()).Err(); err != nil { 84 | return err 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | func (r *Redis) addChannelPubSub(channelUUID string, pubSub *redis.PubSub) *ChannelPubSub { 91 | channelPubSub := &ChannelPubSub{ 92 | close: make(chan struct{}, 1), 93 | closed: make(chan struct{}, 1), 94 | pubSub: pubSub, 95 | } 96 | r.channelsPubSubSync.Lock() 97 | if _, ok := r.channelsPubSub[channelUUID]; !ok { 98 | r.channelsPubSub[channelUUID] = channelPubSub 99 | } 100 | r.channelsPubSubSync.Unlock() 101 | return channelPubSub 102 | } 103 | 104 | func (r *Redis) getChannelPubSub(channelUUID string) *ChannelPubSub { 105 | r.channelsPubSubSync.RLock() 106 | pubSub, ok := r.channelsPubSub[channelUUID] 107 | r.channelsPubSubSync.RUnlock() 108 | if !ok { 109 | return nil 110 | } 111 | return pubSub 112 | } 113 | 114 | func (r *Redis) ChannelJoin(senderUUID, recipientUUID string) (*ChannelPubSub, string, error) { 115 | 116 | channelUUID, err := r.getChannelUUID(senderUUID, recipientUUID) 117 | if err != nil { 118 | return nil, "", err 119 | } 120 | 121 | err = r.channelJoin(channelUUID, senderUUID, recipientUUID) 122 | if err != nil { 123 | return nil, "", err 124 | } 125 | pubSub := r.client.Subscribe(channelUUID) 126 | channel := r.addChannelPubSub(channelUUID, pubSub) 127 | return channel, channelUUID, nil 128 | } 129 | 130 | func (r *Redis) ChannelMessage(message *Message) (string, error) { 131 | channelUUID, err := r.getChannelUUID(message.SenderUUID, message.RecipientUUID) 132 | if err != nil { 133 | return "", err 134 | } 135 | 136 | buff := bytes.NewBufferString("") 137 | enc := json.NewEncoder(buff) 138 | err = enc.Encode(message) 139 | if err != nil { 140 | return "", err 141 | } 142 | 143 | err = r.client.Publish(channelUUID, buff.String()).Err() 144 | if err != nil { 145 | return "", err 146 | } 147 | key := r.getKeyChannelMessages(channelUUID) 148 | err = r.client.RPush(key, buff.String()).Err() 149 | if err != nil { 150 | return "", err 151 | } 152 | return channelUUID, nil 153 | } 154 | 155 | func (r *Redis) ChannelLeave(senderUUID, recipientUUID string) (string, error) { 156 | 157 | channelUUID, err := r.getChannelUUID(senderUUID, recipientUUID) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | defer func() { 163 | r.channelsPubSubSync.Lock() 164 | delete(r.channelsPubSub, channelUUID) 165 | r.channelsPubSubSync.Unlock() 166 | }() 167 | 168 | r.channelsPubSubSync.RLock() 169 | channel, ok := r.channelsPubSub[channelUUID] 170 | r.channelsPubSubSync.RUnlock() 171 | 172 | if !ok { 173 | return "", errors.New("channel not found") 174 | } 175 | 176 | close(channel.close) 177 | 178 | timeout := time.NewTimer(time.Second * 3) 179 | select { 180 | case <-channel.closed: 181 | return channelUUID, nil 182 | case <-timeout.C: 183 | return "", errors.New("channel closed with timeout") 184 | } 185 | 186 | } 187 | 188 | func (r *Redis) ChannelMessagesCount(channelUUID string) (int64, error) { 189 | key := r.getKeyChannelMessages(channelUUID) 190 | return r.client.LLen(key).Result() 191 | } 192 | 193 | func (r *Redis) ChannelMessages(channelUUID string, offset, limit int64) ([]*Message, error) { 194 | 195 | key := r.getKeyChannelMessages(channelUUID) 196 | 197 | log.Println("ChannelMessages", key, offset, limit) 198 | 199 | values, err := r.client.LRange(key, offset, limit).Result() 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | messages := make([]*Message, 0, len(values)) 205 | for i := range values { 206 | message := &Message{} 207 | dec := json.NewDecoder(strings.NewReader(values[i])) 208 | err := dec.Decode(message) 209 | if err != nil { 210 | return nil, err 211 | } 212 | if message.SenderUUID != "" { 213 | user, err := r.getUserFromListByUUID(message.SenderUUID) 214 | if err == nil { 215 | message.Sender = &User{ 216 | UUID: user.UUID, 217 | Username: user.Username, 218 | } 219 | } 220 | } 221 | if message.RecipientUUID != "" { 222 | user, err := r.getUserFromListByUUID(message.RecipientUUID) 223 | if err == nil { 224 | message.Recipient = &User{ 225 | UUID: user.UUID, 226 | Username: user.Username, 227 | } 228 | } 229 | } 230 | messages = append(messages, message) 231 | } 232 | 233 | return messages, nil 234 | 235 | } 236 | 237 | func (r *Redis) ChannelUsers(channelUUID string) ([]*User, error) { 238 | 239 | key := r.getKeyChannelUsers(channelUUID) 240 | 241 | values, err := r.client.HGetAll(key).Result() 242 | if err != nil { 243 | return nil, err 244 | } 245 | 246 | users := make([]*User, 0) 247 | 248 | for userUUID, _ := range values { 249 | user, err := r.getUserFromListByUUID(userUUID) 250 | if err != nil { 251 | log.Println(err) 252 | continue 253 | } 254 | users = append(users, user) 255 | } 256 | 257 | return users, nil 258 | 259 | } 260 | 261 | /* 262 | func (r *Redis) ChannelUsersUUID(channelUUID string) ([]string, error) { 263 | 264 | keyChannelUsers := r.keyChannelUsers(channelUUID) 265 | n, err := r.client.LLen(keyChannelUsers).Result() 266 | if err != nil { 267 | return nil, err 268 | } 269 | return r.client.LRange(keyChannelUsers, 0, n).Result() 270 | } 271 | */ 272 | /* 273 | func redisChannelMessage(senderUUID, recipientUUID, textMessage string) error { 274 | keyChannelMessage := redisKeyChannelMessage(senderUUID, recipientUUID) 275 | message := Message{ 276 | UUID: uuid.NewString(), 277 | SenderUUID: senderUUID, 278 | RecipientUUID: recipientUUID, 279 | Message: textMessage, 280 | CreatedAt: time.Now(), 281 | } 282 | data, err := json.Marshal(message) 283 | if err != nil { 284 | return fmt.Errorf("could not marshal json: %w", err) 285 | } 286 | err = r.HSet(keyChannelMessage, message.UUID, string(data)).Err() 287 | if err != nil { 288 | return fmt.Errorf("could not HSET data: %w", err) 289 | } 290 | return nil 291 | } 292 | 293 | func redisChannelMessages(senderUUID, recipientUUID string) ([]Message, error) { 294 | keyChannelMessage := redisKeyChannelMessage(senderUUID, recipientUUID) 295 | values, err := r.HGetAll(keyChannelMessage).Result() 296 | if err != nil { 297 | return nil, err 298 | } 299 | messages := make([]Message, 0, len(values)) 300 | for i := range values { 301 | message := Message{} 302 | err := json.Unmarshal([]byte(values[i]), &message) 303 | if err != nil { 304 | return nil, err 305 | } 306 | messages = append(messages, message) 307 | } 308 | return messages, nil 309 | } 310 | 311 | func redisChannelUsers(senderUUID, recipientUUID string) ([]User, error) { 312 | key := redisKeyChannelUsers(senderUUID, recipientUUID) 313 | log.Println("redisChannelUsers:", key) 314 | values, err := r.HGetAll(key).Result() 315 | if err != nil { 316 | return nil, fmt.Errorf("could not GHETALL: %w", err) 317 | } 318 | 319 | log.Println("values:", fmt.Sprintf("%+v", values)) 320 | 321 | users := make([]User, 0, len(values)) 322 | for i := range values { 323 | user := User{} 324 | err := json.Unmarshal([]byte(values[i]), &user) 325 | if err != nil { 326 | return nil, fmt.Errorf("could not unmarshal [%+v] : %w", values[i], err) 327 | } 328 | users = append(users, user) 329 | } 330 | 331 | return users, nil 332 | } 333 | */ 334 | -------------------------------------------------------------------------------- /rediscli/channel_test.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "log" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestRedis_ChannelJoinPrivate(t *testing.T) { 13 | 14 | senderUUID := "TEST_SENDER" 15 | recipientUUID := "TEST_RECIPIENT" 16 | 17 | chMessageX, _, err := testRedisInstance.ChannelJoin(senderUUID, recipientUUID) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | chMessageY, _, err := testRedisInstance.ChannelJoin(recipientUUID, senderUUID) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | channelUUIDX, err := testRedisInstance.getChannelUUID(senderUUID, recipientUUID) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | channelUUIDY, err := testRedisInstance.getChannelUUID(recipientUUID, senderUUID) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | if channelUUIDX != channelUUIDY { 38 | t.Fatalf("expected channelX [%s] equal channelY [%s]", channelUUIDX, channelUUIDY) 39 | } 40 | 41 | wg := sync.WaitGroup{} 42 | wg.Add(3) 43 | 44 | go func() { 45 | defer wg.Done() 46 | for i := 0; i < 5; i++ { 47 | time.Sleep(time.Millisecond * 100) 48 | message := &Message{ 49 | UUID: uuid.NewString(), //fmt.Sprintf("%s%d", id, i+1), 50 | SenderUUID: senderUUID, 51 | RecipientUUID: recipientUUID, 52 | Message: fmt.Sprintf("Helo %s #%d", recipientUUID, i+1), 53 | CreatedAt: time.Now(), 54 | } 55 | _,err := testRedisInstance.ChannelMessage(message) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | } 60 | }() 61 | 62 | go func() { 63 | defer wg.Done() 64 | 65 | for i := 0; i < 5; i++ { 66 | data := <-chMessageX.Channel() 67 | log.Println(fmt.Sprintf("X >>> %+v", data)) 68 | } 69 | 70 | }() 71 | 72 | go func() { 73 | defer wg.Done() 74 | 75 | for i := 0; i < 5; i++ { 76 | data := <-chMessageY.Channel() 77 | log.Println(fmt.Sprintf("Y >>> %+v", data)) 78 | } 79 | 80 | }() 81 | 82 | wg.Wait() 83 | 84 | } 85 | 86 | func TestRedis_ChannelJoinPublic(t *testing.T) { 87 | 88 | senderUUID := "TEST_SENDER" 89 | recipientUUID := "" 90 | 91 | chMessage, _, err := testRedisInstance.ChannelJoin(senderUUID, recipientUUID) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | _, err = testRedisInstance.getChannelUUID(senderUUID, recipientUUID) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | wg := sync.WaitGroup{} 102 | wg.Add(2) 103 | 104 | go func() { 105 | defer wg.Done() 106 | for i := 0; i < 5; i++ { 107 | time.Sleep(time.Millisecond * 100) 108 | message := &Message{ 109 | UUID: uuid.NewString(), //fmt.Sprintf("%s%d", id, i+1), 110 | SenderUUID: senderUUID, 111 | RecipientUUID: recipientUUID, 112 | Message: fmt.Sprintf("Helo %s #%d", recipientUUID, i+1), 113 | CreatedAt: time.Now(), 114 | } 115 | _,err := testRedisInstance.ChannelMessage(message) 116 | if err != nil { 117 | t.Error(err) 118 | } 119 | } 120 | }() 121 | 122 | go func() { 123 | defer wg.Done() 124 | 125 | for i := 0; i < 5; i++ { 126 | data := <-chMessage.Channel() 127 | log.Println(fmt.Sprintf("X >>> %+v", data)) 128 | } 129 | 130 | }() 131 | 132 | wg.Wait() 133 | 134 | } 135 | -------------------------------------------------------------------------------- /rediscli/connection.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | keyUserSession = "userSession" 10 | ) 11 | func (r *Redis) getKeyUserSession(userSessionUUID string) string { 12 | return fmt.Sprintf("%s.%s", keyUserSession, userSessionUUID) 13 | } 14 | 15 | func (r *Redis) AddConnection(userSessionUUID string) error { 16 | key := r.getKeyUserSession(userSessionUUID) 17 | return r.client.Set(key, time.Now().String(), time.Hour).Err() 18 | } 19 | 20 | func (r *Redis) DelConnection(userSessionUUID string) error { 21 | key := r.getKeyUserSession(userSessionUUID) 22 | return r.client.Del(key).Err() 23 | } 24 | -------------------------------------------------------------------------------- /rediscli/redis.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | type Redis struct { 10 | client *redis.Client 11 | channelsPubSub map[string]*ChannelPubSub 12 | channelsPubSubSync *sync.RWMutex 13 | } 14 | 15 | type ChannelPubSub struct { 16 | close chan struct{} 17 | closed chan struct{} 18 | pubSub *redis.PubSub 19 | } 20 | 21 | func (channel *ChannelPubSub) Channel() <-chan *redis.Message { 22 | return channel.pubSub.Channel() 23 | } 24 | 25 | func (channel *ChannelPubSub) Close() <-chan struct{} { 26 | return channel.close 27 | } 28 | 29 | func (channel *ChannelPubSub) Closed() <-chan struct{} { 30 | return channel.closed 31 | } 32 | 33 | func NewRedis(addr, passwd string) *Redis { 34 | 35 | log.Println("Initialized redis client", addr,passwd) 36 | 37 | opt := &redis.Options{ 38 | Addr: addr, 39 | } 40 | 41 | if passwd != "" { 42 | opt.Password = passwd 43 | } 44 | 45 | c := redis.NewClient(opt) 46 | if err := c.Ping().Err(); err != nil { 47 | panic(err) 48 | } 49 | 50 | r := &Redis{ 51 | client: c, 52 | channelsPubSub: make(map[string]*ChannelPubSub, 0), 53 | channelsPubSubSync: &sync.RWMutex{}, 54 | } 55 | return r 56 | } 57 | -------------------------------------------------------------------------------- /rediscli/user.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/go-redis/redis" 10 | "github.com/google/uuid" 11 | "log" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const ( 18 | keyUsers = "users" 19 | keyUserStatus = "userStatus" 20 | keyUserChannels = "userChannels" 21 | keyUserAccessKey = "userAccessKey" 22 | keyUsersUUIDListIndex = "usersUUIDListIndex" 23 | keyUsersUsernameListIndex = "usersUsernameListIndex" 24 | ) 25 | 26 | type User struct { 27 | UUID string `json:"UUID"` 28 | Username string `json:"Username"` 29 | Password string `json:"Password,omitempty"` 30 | AccessKey string `json:"AccessKey,omitempty"` 31 | OnLine bool `json:"OnLine"` 32 | SessionUUID string `json:"-"` 33 | } 34 | 35 | func (r *Redis) getKeyUsers() string { 36 | return keyUsers 37 | } 38 | 39 | func (r *Redis) getKeyUsersUUIDListIndex(userUUID string) string { 40 | return fmt.Sprintf("%s.%s", keyUsersUUIDListIndex, userUUID) 41 | } 42 | 43 | func (r *Redis) getKeyUsersUsernameListIndex(username string) string { 44 | return fmt.Sprintf("%s.%x", keyUsersUsernameListIndex, md5.Sum([]byte(username))) 45 | } 46 | 47 | func (r *Redis) getUserIndexByUsername(username string) (int64, error) { 48 | 49 | log.Println("getUserIndexByUsername", username) 50 | 51 | key := r.getKeyUsersUsernameListIndex(username) 52 | value, err := r.client.Get(key).Result() 53 | if err != nil { 54 | return 0, err 55 | } 56 | index, err := strconv.ParseInt(value, 10, 64) 57 | if err != nil { 58 | return 0, err 59 | } 60 | return index, nil 61 | } 62 | 63 | func (r *Redis) getUserIndexByUUID(userUUID string) (int64, error) { 64 | 65 | log.Println("getUserIndexByUUID", userUUID) 66 | 67 | key := r.getKeyUsersUUIDListIndex(userUUID) 68 | value, err := r.client.Get(key).Result() 69 | if err != nil { 70 | return 0, fmt.Errorf("getUserIndexByUUID: %w", err) 71 | } 72 | index, err := strconv.ParseInt(value, 10, 64) 73 | if err != nil { 74 | return 0, fmt.Errorf("getUserIndexByUUID: %w", err) 75 | } 76 | return index, nil 77 | } 78 | 79 | func (r *Redis) getKeyUserStatus(userUUID string) string { 80 | return fmt.Sprintf("%s.%s", keyUserStatus, userUUID) 81 | } 82 | 83 | func (r *Redis) getKeyUserChannels(userUUID string) string { 84 | return fmt.Sprintf("%s.%s", keyUserChannels, userUUID) 85 | } 86 | 87 | func (r *Redis) getKeyUserAccessKey(userUUID string) string { 88 | return fmt.Sprintf("%s.%s", keyUserAccessKey, userUUID) 89 | } 90 | 91 | func (r *Redis) UserAuthorize(username, password string) (*User, error) { 92 | 93 | log.Println("UserAuthorize", fmt.Sprintf("[%s|%s]", username, password)) 94 | 95 | user, err := r.UserGet(username) 96 | if errors.Is(err, redis.Nil) { 97 | user, err = r.UserCreate(username, password) 98 | if err != nil { 99 | return nil, err 100 | } 101 | } else if err != nil { 102 | return nil, err 103 | } 104 | 105 | log.Println("UserAuthorize", fmt.Sprintf("%+v", user)) 106 | 107 | if user.Password != password { 108 | return nil, errors.New("wrong password") 109 | } 110 | 111 | user.AccessKey, err = r.UserUpdateAccessKey(user.UUID) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | err = r.UserSetOnline(user.UUID) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return user, nil 122 | 123 | } 124 | 125 | func (r *Redis) addUser(user *User) error { 126 | buff := bytes.NewBufferString("") 127 | enc := json.NewEncoder(buff) 128 | err := enc.Encode(user) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | key := r.getKeyUsers() 134 | 135 | elements, err := r.client.RPush(key, buff.String()).Result() 136 | if err != nil { 137 | return nil 138 | } 139 | 140 | index := elements - 1 141 | keyUserUsernameIndex := r.getKeyUsersUsernameListIndex(user.Username) 142 | keyUserUUIDIndex := r.getKeyUsersUUIDListIndex(user.UUID) 143 | 144 | err = r.client.Set(keyUserUsernameIndex, fmt.Sprintf("%d", index), 0).Err() 145 | if err != nil { 146 | return err 147 | } 148 | 149 | err = r.client.Set(keyUserUUIDIndex, fmt.Sprintf("%d", index), 0).Err() 150 | if err != nil { 151 | r.client.Del(keyUserUsernameIndex) 152 | return err 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func (r *Redis) getUserFromList(userIndex int64) (*User, error) { 159 | 160 | key := r.getKeyUsers() 161 | 162 | value, err := r.client.LIndex(key, userIndex).Result() 163 | if err != nil { 164 | return nil, fmt.Errorf("getUserFromList[%d]: %w", userIndex, err) 165 | } 166 | 167 | user := &User{} 168 | 169 | dec := json.NewDecoder(strings.NewReader(value)) 170 | err = dec.Decode(user) 171 | if err != nil { 172 | return nil, fmt.Errorf("getUserFromList[%d]: %w", userIndex, err) 173 | } 174 | 175 | return user, nil 176 | } 177 | 178 | func (r *Redis) getUserFromListByUsername(username string) (*User, error) { 179 | 180 | log.Println("getUserFromListByUsername", username) 181 | 182 | userIndex, err := r.getUserIndexByUsername(username) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | user, err := r.getUserFromList(userIndex) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | user.OnLine = r.UserIsOnline(user.UUID) 193 | 194 | return user, nil 195 | 196 | } 197 | 198 | func (r *Redis) getUserFromListByUUID(userUUID string) (*User, error) { 199 | 200 | log.Println("getUserFromListByUUID", userUUID) 201 | 202 | userIndex, err := r.getUserIndexByUUID(userUUID) 203 | if err != nil { 204 | return nil, fmt.Errorf("getUserFromListByUUID[%s]: %w", userUUID, err) 205 | } 206 | 207 | user, err := r.getUserFromList(userIndex) 208 | if err != nil { 209 | return nil, fmt.Errorf("getUserFromListByUUID[%s]: %w", userUUID, err) 210 | } 211 | 212 | user.OnLine = r.UserIsOnline(user.UUID) 213 | 214 | return user, nil 215 | 216 | } 217 | 218 | func (r *Redis) UserCreate(username, password string) (*User, error) { 219 | 220 | log.Println("UserCreate", fmt.Sprintf("[%s|%s]", username, password)) 221 | 222 | if user, err := r.getUserFromListByUsername(username); err == nil { 223 | return user, nil 224 | } 225 | 226 | user := &User{ 227 | UUID: uuid.NewString(), 228 | Username: username, 229 | Password: password, 230 | } 231 | 232 | if err := r.addUser(user); err != nil { 233 | return nil, err 234 | } 235 | 236 | return user, nil 237 | 238 | } 239 | 240 | func (r *Redis) UserGet(userUUID string) (*User, error) { 241 | log.Println("UserGet", userUUID) 242 | user, err := r.getUserFromListByUUID(userUUID) 243 | if err != nil { 244 | return nil, fmt.Errorf("UserGET[%s]: %w", userUUID, err) 245 | } 246 | return user, nil 247 | } 248 | 249 | func (r *Redis) UserAll() ([]*User, error) { 250 | 251 | key := r.getKeyUsers() 252 | 253 | items, err := r.client.LLen(key).Result() 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | values, err := r.client.LRange(key, 0, items).Result() 259 | 260 | users := make([]*User, 0, items) 261 | 262 | for i := range values { 263 | user := &User{} 264 | dec := json.NewDecoder(strings.NewReader(values[i])) 265 | err = dec.Decode(user) 266 | if err != nil { 267 | return nil, fmt.Errorf("[%s]: %w", values[i], err) 268 | } 269 | users = append(users, user) 270 | } 271 | 272 | return users, nil 273 | 274 | } 275 | 276 | func (r *Redis) UserDeleteAccessKey(userUUID string) { 277 | key := r.getKeyUserAccessKey(userUUID) 278 | _ = r.client.Del(key) 279 | } 280 | 281 | func (r *Redis) UserUpdateAccessKey(userUUID string) (string, error) { 282 | key := r.getKeyUserAccessKey(userUUID) 283 | accessKey := uuid.New().String() 284 | 285 | err := r.client.Set(key, accessKey, 0).Err() 286 | if err != nil { 287 | return "", err 288 | } 289 | return accessKey, nil 290 | } 291 | 292 | func (r *Redis) UserSetOnline(userUUID string) error { 293 | key := r.getKeyUserStatus(userUUID) 294 | return r.client.Set(key, time.Now().String(), time.Minute).Err() 295 | } 296 | 297 | func (r *Redis) UserSetOffline(userUUID string) { 298 | key := r.getKeyUserStatus(userUUID) 299 | r.client.Del(key) 300 | } 301 | 302 | func (r *Redis) UserIsOnline(userUUID string) bool { 303 | key := r.getKeyUserStatus(userUUID) 304 | err := r.client.Get(key).Err() 305 | if err == nil { 306 | return true 307 | } 308 | return false 309 | } 310 | 311 | func (r *Redis) UserSignOut(userUUID string) { 312 | keyAccessKey := r.getKeyUserAccessKey(userUUID) 313 | r.UserDeleteAccessKey(keyAccessKey) 314 | r.UserSetOffline(userUUID) 315 | } 316 | -------------------------------------------------------------------------------- /rediscli/user_test.go: -------------------------------------------------------------------------------- 1 | package rediscli 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | var testRedisInstance = NewRedis("localhost:44444", "") 10 | /* 11 | func TestRedis_UserStatus(t *testing.T) { 12 | 13 | testCases := []struct { 14 | userUUID string 15 | status UserStatus 16 | expected UserStatus 17 | }{ 18 | { 19 | userUUID: "test user set status online", 20 | status: UserStatusOnline, 21 | expected: UserStatusOnline, 22 | }, { 23 | userUUID: "test user set status offline", 24 | status: UserStatusOffline, 25 | expected: UserStatusOffline, 26 | }, 27 | } 28 | 29 | for i := range testCases { 30 | 31 | key := testRedisInstance.keyUsersStatus() 32 | 33 | err := testRedisInstance.UserSetStatus(testCases[i].userUUID, testCases[i].status) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | value, err := testRedisInstance.client.HGet(key, testCases[i].userUUID).Result() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if value != string(testCases[i].expected) { 44 | t.Fatalf("expected [%s], actual [%s]", testCases[i].expected, value) 45 | } 46 | 47 | } 48 | 49 | }/* 50 | 51 | func TestUserMap(t *testing.T) { 52 | 53 | user := &User{ 54 | UUID: "test user map", 55 | Username: "test user map", 56 | } 57 | 58 | if _, err := testRedisInstance.userMapGet("not exist user"); err != redis.Nil { 59 | t.Fatalf("expected error [%s], actual [%s]", redis.Nil, err) 60 | } 61 | 62 | err := testRedisInstance.userMapSet(user) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | userUUID, err := testRedisInstance.userMapGet(user.Username) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if userUUID != user.UUID { 72 | t.Fatalf("expected user uuid [%s], actual [%s]", user.UUID, userUUID) 73 | } 74 | }*/ 75 | 76 | func TestUserCreate(t *testing.T) { 77 | 78 | username := "test user" 79 | password := "test password" 80 | 81 | user, err := testRedisInstance.UserCreate(username, password) 82 | if err != nil { 83 | t.Fatal("userCreate", err) 84 | } 85 | 86 | user, err = testRedisInstance.UserGet(user.UUID) 87 | if err != nil { 88 | t.Fatal("userGet", err) 89 | } 90 | 91 | if user.Username != username { 92 | log.Fatalf("expecte username [%s], actual username [%s]", username, user.Username) 93 | } 94 | 95 | users, err := testRedisInstance.UserAll() 96 | if err != nil { 97 | log.Fatalln("usersAll", err) 98 | } 99 | 100 | for i := range users { 101 | log.Println(fmt.Sprintf("%+v", users[i])) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /websocket/README.md: -------------------------------------------------------------------------------- 1 | ## Into 2 | Backend application base on websocket 3 | 4 | All communications between client and server processed with websocket 5 | 6 | Data storage made on redis 7 | ## Open WS 8 | `const ws = WebSocket('ws://localhost:8080/ws')` 9 | ## WebSocket Events 10 | ##### Produced on websocket open 11 | `ws.onopen = function(event){};` 12 | ##### Produced on websocket closed 13 | `ws.onclose = function(event){};` 14 | ##### Produced on websocket catch error 15 | `ws.onerror = function(event){};` 16 | ##### Produced on websocket received message from server 17 | `ws.onmessage = function(event){};` 18 | ## Send message 19 | #### Write a message to websocket 20 | `ws.send(JSON.stringify(body))` 21 | 22 | The `body` must be `JSON Object`, websocket server expected message as `JSON Object` as `string` 23 | 24 | ## Accept messages from websocket 25 | `ws.onmessage = function(event){const body = JSON.parse(event.data);};` 26 | 27 | The `body` contained `Stringified JSON Object` 28 | 29 | ## Kind of messages 30 | ### Ready 31 | #### Client connected and ready to send/receive messages 32 | > ***Response*** 33 | ``` 34 | { 35 | "type": "ready", 36 | "ready": { 37 | "sessionUUID": "Session UUID" 38 | } 39 | } 40 | ``` 41 | ### Error 42 | #### Server error response 43 | > ***Response*** 44 | ``` 45 | { 46 | "type": "error", 47 | "error": { 48 | "code":1, 49 | "message": "Error message" 50 | } 51 | } 52 | ``` 53 | The server reply error response if request could not be processed, all kinds of messages will return error response with different `error.code` and `error.message` 54 | 55 | This response useful for easy global error check and UI-render general error message 56 | ### User SignIn 57 | #### Make user login 58 | > ***Request*** 59 | ``` 60 | { 61 | "SUUID": "Session UUID", 62 | "type": "signIn", 63 | "signId": { 64 | "username": "User name", 65 | "password": "User password" 66 | } 67 | } 68 | ``` 69 | > ***Response*** 70 | ``` 71 | { 72 | "type": "authorized": 73 | "authorized": { 74 | "userUUID": "user UUID, 75 | "accessKey": "User Access Key" 76 | } 77 | } 78 | ``` 79 | User will be created if not exists 80 | ### User SignUp 81 | #### Create user account 82 | > ***Request*** 83 | ``` 84 | { 85 | "SUUID":"Session UUID", 86 | "type": "signUp", 87 | "signUp": { 88 | "username": "User name", 89 | "password": "User password" 90 | } 91 | } 92 | ``` 93 | > ***Response*** 94 | ``` 95 | { 96 | "type": "authorized", 97 | "authorized": { 98 | "userUUID": 1, 99 | "accessKey": "User Access Key" 100 | } 101 | } 102 | ``` 103 | ### User SignOut 104 | #### Make user logout 105 | > ***Request*** 106 | ``` 107 | { 108 | "SUUID": "Session UUID", 109 | "type": "signOut", 110 | "userUUID: "User UUID", 111 | "userAccessKey": "User Access Key" 112 | } 113 | ``` 114 | > 115 | > ***Response*** 116 | ``` 117 | { 118 | "type": "unauthorized", 119 | "unauthorized": { 120 | "userUUID": "User UUID" 121 | } 122 | } 123 | ``` 124 | ### Users List 125 | #### Read users list 126 | > ***Request*** 127 | ``` 128 | { 129 | "SUUID": "Session UUID", 130 | "type": "users", 131 | "userUUID: "User UUID", 132 | "userAccessKey": "User Access Key" 133 | } 134 | ``` 135 | > 136 | > ***Response*** 137 | ``` 138 | { 139 | "type": "users", 140 | "users": { 141 | "total": 0, 142 | "received": 0, 143 | "users": [ 144 | { 145 | "UUID": "User UUID", 146 | "Username": "Username", 147 | "Password": "Password", 148 | "OnLine": true 149 | } 150 | ] 151 | } 152 | } 153 | ``` 154 | ### Join to channel 155 | #### Connect user to channel for read and write messages 156 | > ***Request*** 157 | ``` 158 | { 159 | "SUUID": "Session UUID", 160 | "userUUID": "User UUID", 161 | "userAccessKey": "User Access Key", 162 | "type": "channelJoin", 163 | "channelJoin": { 164 | "recipientUUID": "User UUID" 165 | } 166 | } 167 | ``` 168 | > ***Response*** 169 | ``` 170 | { 171 | "type": "channelJoin", 172 | "channelJoin": { 173 | "messages": [ 174 | { 175 | "UUID: "Message UUID", 176 | "senderUUID": "User UUID", 177 | "recipientUUID": "User UUID", 178 | "message": "text", 179 | "created_at": "time" 180 | } 181 | ], 182 | "users": [ 183 | { 184 | "UUID": "User UUID", 185 | "username": "username" 186 | } 187 | ] 188 | } 189 | } 190 | ``` 191 | When `recipientUUID` equal to `0` user will be joined to public channel 192 | ### Send message 193 | #### Write a message from user to public or private channel 194 | > ***Request*** 195 | ``` 196 | { 197 | "SUUID": "Session UUID", 198 | "userUUID": "User UUID", 199 | "userAccessKey": "User Access Key", 200 | "type": "channelMessage", 201 | "channelMessage": { 202 | "recipientUUID": "User UUID", 203 | "message": "Message text" 204 | } 205 | } 206 | ``` 207 | > ***Response*** 208 | ``` 209 | { 210 | "type": "channelMessage", 211 | "channelMessage": { 212 | "UUID": "Message UUID" 213 | "senderUUID": "User UUID", 214 | "recipientUUID": "User UUID", 215 | "message": "Message text" 216 | } 217 | } 218 | ``` 219 | When `recipientUUID` equal to `0` the message will be sent to public channel 220 | ### Read messages 221 | #### List a messages from public or private channel 222 | > ***Request*** 223 | ``` 224 | { 225 | "SUUID": "Session UUID", 226 | "userUUID": "User UUID", 227 | "userAccessKey": "User Access Key", 228 | "type": "channelMessages", 229 | "channelMessages": { 230 | "recipientUUID": "User UUID", 231 | "offset": 1, 232 | "limit": 1 233 | } 234 | } 235 | ``` 236 | > ***Response*** 237 | ``` 238 | { 239 | "type": "channelRead", 240 | "channelMessages": { 241 | "messagesTotal": 1, 242 | "messagesReceived": 1, 243 | "messages": [ 244 | { 245 | "senderUUID": "User UUID", 246 | "recipientUUID": "User UUID", 247 | "message": "Message text", 248 | "createdAt": "created time" 249 | } 250 | ] 251 | } 252 | } 253 | ``` 254 | When `recipientUUID` equal to `0` the messages will be read from public channel 255 | 256 | The `channelMessages.limit` should be less that `100`, default if `10` 257 | 258 | The `channelMessages.offset` is the entries offset, default is `0` 259 | 260 | All messages ordered from new to older 261 | ### Leave channel 262 | #### Exit from a channel and stop to receive messages from it 263 | > ***Request*** 264 | ``` 265 | { 266 | "SUUID": "Session UUID", 267 | "userUUID": "User UUID", 268 | "userAccessKey": "User Access Key", 269 | "type": "channelLeave" 270 | "channelLeave": { 271 | "recipientUUID": "User UUID" 272 | } 273 | } 274 | ``` 275 | > ***Response*** 276 | ``` 277 | { 278 | "type": "channelLeave", 279 | "channelLeave": { 280 | "recipientUUID": "User UUID" 281 | } 282 | } 283 | ``` 284 | The `recipientUUID` could not be equal `0`, user can not leave public channel 285 | -------------------------------------------------------------------------------- /websocket/connection.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | var connectionsSync sync.RWMutex 10 | 11 | type Connection struct { 12 | conn io.ReadWriter 13 | userSessionID string 14 | } 15 | 16 | var connections = map[string]Connection{} 17 | 18 | func connectionAdd(conn net.Conn, userSessionUUID string) { 19 | connectionsSync.Lock() 20 | connections[userSessionUUID] = Connection{ 21 | userSessionID: userSessionUUID, 22 | conn: conn, 23 | } 24 | connectionsSync.Unlock() 25 | } 26 | 27 | func connectionDel(userSessionUUID string) { 28 | connectionsSync.Lock() 29 | delete(connections, userSessionUUID) 30 | connectionsSync.Unlock() 31 | } 32 | -------------------------------------------------------------------------------- /websocket/error.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import "errors" 4 | 5 | type IError interface { 6 | Error() (uint32, error) 7 | } 8 | 9 | const ( 10 | errCode uint32 = iota 11 | errCodeJSUnmarshal 12 | errCodeWSRead 13 | errCodeWSWrite 14 | errCodeSignIn 15 | errCodeSignUp 16 | errCodeSignOut 17 | errCodeRedisChannelMessage 18 | errCodeRedisChannelUsers 19 | errCodeRedisGetSessionUUID 20 | errCodeRedisGetUserByUUID 21 | errCodeRedisChannelJoin 22 | ) 23 | 24 | var ( 25 | errWSRead = errors.New("could not read websocket connection") 26 | errSignIn = errors.New("could not signIn") 27 | ) 28 | -------------------------------------------------------------------------------- /websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gobwas/ws" 7 | "github.com/gobwas/ws/wsutil" 8 | "github.com/google/uuid" 9 | "github.com/redis-developer/basic-redis-chat-demo-go/message" 10 | "github.com/redis-developer/basic-redis-chat-demo-go/rediscli" 11 | "io" 12 | "log" 13 | "net" 14 | "net/http" 15 | "strings" 16 | ) 17 | 18 | func Write(conn io.ReadWriter, op ws.OpCode, message *message.Message) error { 19 | 20 | data, err := json.Marshal(message) 21 | if err != nil { 22 | log.Println(err) 23 | return nil 24 | } 25 | log.Println("write socket message:", string(data)) 26 | err = wsutil.WriteServerMessage(conn, op, data) 27 | if err != nil { 28 | log.Println(err) 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func NewConnection(conn net.Conn, r *rediscli.Redis, c *message.Controller, initErr chan error) { 36 | userSessionUUID := uuid.NewString() 37 | 38 | err := r.AddConnection(userSessionUUID) 39 | if err != nil { 40 | initErr <- err 41 | return 42 | } 43 | 44 | connectionAdd(conn, userSessionUUID) 45 | defer func() { 46 | conn.Close() 47 | err := r.DelConnection(userSessionUUID) 48 | if err != nil { 49 | log.Println(err) 50 | } 51 | connectionDel(userSessionUUID) 52 | 53 | }() 54 | 55 | err = Write(conn, ws.OpText, c.Ready(userSessionUUID)) 56 | if err != nil { 57 | initErr <- err 58 | return 59 | } 60 | 61 | initErr <- nil 62 | 63 | for { 64 | msg := &message.Message{} 65 | 66 | if data, op, err := wsutil.ReadClientData(conn); err != nil { 67 | log.Println(err) 68 | return 69 | //response := makeError(errCodeWSRead, fmt.Errorf("%s: %w", errWSRead, err)) 70 | //wsWrite(ch, conn, op, response) 71 | } else if err = json.Unmarshal(data, msg); err != nil { 72 | response := c.Error(errCodeJSUnmarshal, err, userSessionUUID, msg) 73 | err = Write(conn, op, response) 74 | } else { 75 | 76 | var receivedErr IError 77 | 78 | log.Println("Received message:", string(data)) 79 | switch msg.Type { 80 | case message.DataTypeSignIn: 81 | receivedErr = c.SignIn(userSessionUUID, conn, op, Write, msg) 82 | case message.DataTypeSignUp: 83 | receivedErr = c.SignUp(userSessionUUID, conn, op, Write, msg) 84 | case message.DataTypeSignOut: 85 | receivedErr = c.SignOut(userSessionUUID, conn, op, Write, msg) 86 | case message.DataTypeUsers: 87 | receivedErr = c.Users(userSessionUUID, conn, op, Write) 88 | case message.DataTypeChannelJoin: 89 | channelPubSub := new(rediscli.ChannelPubSub) 90 | channelPubSub, receivedErr = c.ChannelJoin(userSessionUUID, conn, op, Write, msg) 91 | if channelPubSub != nil { 92 | go chatReceiver(conn, channelPubSub, r, c) 93 | } 94 | case message.DataTypeChannelMessage: 95 | receivedErr = c.ChannelMessage(userSessionUUID, conn, op, Write, msg) 96 | case message.DataTypeChannelLeave: 97 | receivedErr = c.ChannelLeave(userSessionUUID, Write, msg) 98 | default: 99 | err := Write(conn, op, c.Error(errCode, fmt.Errorf("unknow request data type: %s", msg.Type), msg.UserUUID, msg)) 100 | if err != nil { 101 | log.Println(err) 102 | continue 103 | } 104 | } 105 | 106 | if receivedErr != nil { 107 | log.Println(receivedErr) 108 | code, err := receivedErr.Error() 109 | err = Write(conn, op, c.Error(code, err, userSessionUUID, string(data))) 110 | log.Println(receivedErr) 111 | } 112 | } 113 | } 114 | } 115 | 116 | func Handler(r *rediscli.Redis, c *message.Controller) http.HandlerFunc { 117 | return func(writer http.ResponseWriter, request *http.Request) { 118 | conn, _, _, err := ws.UpgradeHTTP(request, writer) 119 | if err != nil { 120 | log.Println(err) 121 | writer.WriteHeader(http.StatusInternalServerError) 122 | _,_ = fmt.Fprintf(writer, "%s", err) 123 | return 124 | } 125 | chInitErr := make(chan error, 1) 126 | go NewConnection(conn, r, c, chInitErr) 127 | if err = <- chInitErr; err != nil { 128 | log.Println(err) 129 | writer.WriteHeader(http.StatusInternalServerError) 130 | _,_ = fmt.Fprintf(writer, "%s", err) 131 | return 132 | } 133 | } 134 | } 135 | 136 | func chatReceiver(conn net.Conn, channel *rediscli.ChannelPubSub, r *rediscli.Redis, c *message.Controller) { 137 | 138 | defer channel.Closed() 139 | for { 140 | select { 141 | case data := <-channel.Channel(): 142 | msg := &message.DataChannelMessage{} 143 | dec := json.NewDecoder(strings.NewReader(data.Payload)) 144 | err := dec.Decode(msg) 145 | if err != nil { 146 | log.Println(err) 147 | } else { 148 | 149 | if msg.SenderUUID != "" { 150 | user, err := r.UserGet(msg.SenderUUID) 151 | if err == nil { 152 | msg.Sender = &rediscli.User{ 153 | UUID: user.UUID, 154 | Username: user.Username, 155 | } 156 | } 157 | } 158 | 159 | if msg.RecipientUUID != "" { 160 | user, err := r.UserGet(msg.RecipientUUID) 161 | if err == nil { 162 | msg.Recipient = &rediscli.User{ 163 | UUID: user.UUID, 164 | Username: user.Username, 165 | } 166 | } 167 | } 168 | 169 | err := Write(conn, ws.OpText, &message.Message{ 170 | Type: message.DataTypeChannelMessage, 171 | ChannelMessage: msg, 172 | }) 173 | if err != nil { 174 | log.Println(err) 175 | } 176 | } 177 | case <-channel.Close(): 178 | log.Println("Close channel") 179 | return 180 | } 181 | } 182 | 183 | } 184 | --------------------------------------------------------------------------------