├── .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 |
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 |
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 |
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 |
41 | {!noinfo && (
42 |
43 |
{user.username}
44 |
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 |
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 |
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 |
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 |
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 |
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 |

53 |
54 |
55 |
70 |
71 |
72 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------