├── .env ├── .github └── workflows │ └── deploy-github-pages.yml ├── .gitignore ├── LICENCE ├── README.md ├── catalog-info.yaml ├── package.json ├── public ├── archive_icon.png ├── document_icon.png ├── double_tick.png ├── double_tick_as_read.png ├── favicon.ico ├── icon_copy.png ├── icon_delete.png ├── icon_edit.png ├── icon_freeze.png ├── icon_not_delivered.png ├── icon_pin.png ├── icon_reschedule.png ├── icon_send-message.png ├── icon_thread.png ├── icon_unpin.png ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── operator_icon.png ├── profile_img.png ├── report_icon.png ├── robots.txt └── unread_icon.png ├── react-sample.gif └── src ├── App.js ├── Home.js ├── constants └── constants.js ├── index.css ├── index.js ├── samples ├── BasicGroupChannelSample.js ├── BasicOpenChannelSample.js ├── GroupChannelArchive.js ├── GroupChannelBanUnbanUsers.js ├── GroupChannelCategorizeByCustomType.js ├── GroupChannelCategorizeMessagesByCustomType.js ├── GroupChannelDisplayOGTags.js ├── GroupChannelFreezeUnfreeze.js ├── GroupChannelMarkMessagesAsRead.js ├── GroupChannelMembersListOrder.js ├── GroupChannelMessageThreading.js ├── GroupChannelMuteUnmuteUsers.js ├── GroupChannelOperatorsList.js ├── GroupChannelPinnedMessages.js ├── GroupChannelPolls.js ├── GroupChannelReactToAMessage.js ├── GroupChannelRegisterUnregisterOperator.js ├── GroupChannelReportAMessageUserChannel.js ├── GroupChannelRetrieveAListOfBannedOrMutedUsers.js ├── GroupChannelRetrieveNumberOfMembersHaventReadMessage.js ├── GroupChannelRetrieveNumberOfMembersHaventReceivedMessage.js ├── GroupChannelRetrieveOnlineStatus.js ├── GroupChannelScheduledMessages.js ├── GroupChannelSendAnAdminMessage.js ├── GroupChannelStructuredData.js ├── GroupChannelTypes.js ├── GroupChannelTypingIndicatorSample.js ├── GroupChannelUpdateDeleteMessageByOperator.js ├── GroupChannelUserDoNotDisturbOrSnooze.js ├── GroupChannelUserProfileUpdate.js ├── GroupChannelUsersOnlineStatus.js ├── OpenChannelAddExtraDataToMessage.js ├── OpenChannelBanUnbanUsers.js ├── OpenChannelCategorizeByCustomType.js ├── OpenChannelCategorizeMessagesByCustomType.js ├── OpenChannelCopyMessage.js ├── OpenChannelDisplayOGTags.js ├── OpenChannelFreeze.js ├── OpenChannelMembersListOrder.js ├── OpenChannelMessageThreading.js ├── OpenChannelMetadataAndMetacounter.js ├── OpenChannelMuteUnmuteUsers.js ├── OpenChannelRegisterUnregisterOperator.js ├── OpenChannelReportAMessageUserChannel.js ├── OpenChannelSendAnAdminMessage.js ├── OpenChannelSendAndReceiveVariousTypesOfFiles.js ├── OpenChannelStructuredData.js ├── OpenChannelThumbnails.js ├── OpenChannelUpdateDeleteMessageByOperator.js ├── OpenChannelUserDoNotDisturbOrSnooze.js ├── OpenChannelUserProfileUpdate.js └── OpenChannelUsersOnlineStatus.js └── utils └── messageUtils.js /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_SEND_BIRD_TOKEN=7c3cec032dc7d99179470ac86bfc1b27b89fdc01 2 | REACT_APP_SEND_BIRD_ID=6B7BD614-BE69-41C1-8948-8AE067E3B366 -------------------------------------------------------------------------------- /.github/workflows/deploy-github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy React App to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 또는 원하는 브랜치 이름 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '18.x' # 또는 프로젝트에 맞는 Node.js 버전 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Build 24 | run: yarn build 25 | env: 26 | CI: false # ESLint 에러 무시 27 | PUBLIC_URL: https://sendbird.github.io/sendbird-chat-sample-react 28 | 29 | - name: Deploy to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build 34 | -------------------------------------------------------------------------------- /.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 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .idea/ 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # build files 28 | yarn.lock 29 | package-lock.json 30 | 31 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sendbird 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sendbird Chat SDK React Sample 2 | > Note: This sample is a Chat SDK sample, not the UIKit React sample. 3 | > [Visit here if you are looking for the **UIKit React sample**.](https://github.com/sendbird/sendbird-uikit-react/tree/main/samples) 4 | 5 | ![React sample](/react-sample.gif?raw=true "react sample") 6 | 7 | ## Overview 8 | A simple react app that demonstrates how to use the Sendbird [Chat SDK](https://sendbird.com/docs/chat). 9 | 10 | ## Requirement 11 | A Sendbird [account](https://dashboard.sendbird.com/auth/signup). 12 | Node version >16 is required. Download [here](https://nodejs.org/en/). 13 | 14 | ## 🔒 Security tip 15 | When a new Sendbird application is created in the dashboard the default security settings are set permissive to simplify running samples and implementing your first code. 16 | 17 | Before launching make sure to review the security tab under ⚙️ Settings -> Security, and set Access token permission to Read Only or Disabled so that unauthenticated users can not login as someone else. And review the Access Control lists. Most apps will want to disable "Allow retrieving user list" as that could expose usage numbers and other information. 18 | 19 | 20 | ## Setup 21 | Replace {YOUR_SENDBIRD_APP_ID} in constants.js with yout Sendbird app ID. 22 | To get the ID of your Sendbird application, sign in to your dashboard, select the application, go to the Settings > Application > General, and then check the Application ID. 23 | 24 | ## Install 25 | run `npm install` 26 | 27 | ## Running the app 28 | 29 | run `npm start` 30 | 31 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 32 | 33 | ## Considerations in real world app 34 | - In this sample repo users are connecting to sendbird using a user ID (Sendbird Dashboard --> Security --> Read & Write). Read & Write is not secure and will create a new user automatically from the SDK if none exists. In production be sure to change the Sendbird Dashboard security settings to Deny login, and [authenticate users](https://sendbird.com/docs/chat/v3/javascript/guides/authentication#2-connect-to-sendbird-server-with-a-user-id-and-a-token) with a Sendbird generated Session Token. 35 | - Typescript types are available from the Sendbird Chat SDK if you are building with TS. 36 | - Chat is based around user generated input so consider mitigations against XSS attacks. 37 | - Pagination of channel and message lists will be a must have in any real world application. 38 | 39 | 40 | 41 | # Gotchas 42 | - Hot reload can cause issues with the Sendbird Websocket connection while developing. Work around here https://github.com/facebook/create-react-app/issues/2519#issuecomment-318867289 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: sendbird-chat-sample-react 5 | description: SendBird Chat SDK sample for React 6 | annotations: 7 | github.com/project-slug: sendbird/sendbird-chat-sample-react 8 | spec: 9 | type: library 10 | lifecycle: production 11 | owner: dep-client-platform 12 | system: sendbird-chat 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sendbird-chat-sample-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@sendbird/chat": "^4.2.3", 7 | "moment": "^2.29.1", 8 | "react": "^17.0.2", 9 | "react-dom": "^17.0.2", 10 | "react-router-dom": "^6.2.1", 11 | "react-scripts": "5.0.0", 12 | "uuid": "^8.3.2" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/archive_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/archive_icon.png -------------------------------------------------------------------------------- /public/document_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/document_icon.png -------------------------------------------------------------------------------- /public/double_tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/double_tick.png -------------------------------------------------------------------------------- /public/double_tick_as_read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/double_tick_as_read.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/favicon.ico -------------------------------------------------------------------------------- /public/icon_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_copy.png -------------------------------------------------------------------------------- /public/icon_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_delete.png -------------------------------------------------------------------------------- /public/icon_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_edit.png -------------------------------------------------------------------------------- /public/icon_freeze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_freeze.png -------------------------------------------------------------------------------- /public/icon_not_delivered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_not_delivered.png -------------------------------------------------------------------------------- /public/icon_pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_pin.png -------------------------------------------------------------------------------- /public/icon_reschedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_reschedule.png -------------------------------------------------------------------------------- /public/icon_send-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_send-message.png -------------------------------------------------------------------------------- /public/icon_thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_thread.png -------------------------------------------------------------------------------- /public/icon_unpin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/icon_unpin.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/operator_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/operator_icon.png -------------------------------------------------------------------------------- /public/profile_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/profile_img.png -------------------------------------------------------------------------------- /public/report_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/report_icon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/unread_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/public/unread_icon.png -------------------------------------------------------------------------------- /react-sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendbird/sendbird-chat-sample-react/6628b653e4cfd194e0c53df481a1449a7ffdc240/react-sample.gif -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import Home from './Home'; 4 | // Basic samples 5 | import BasicOpenChannelSample from './samples/BasicOpenChannelSample'; 6 | import BasicGroupChannelSample from './samples/BasicGroupChannelSample'; 7 | 8 | // Open channel samples 9 | import OpenChannelCategorizeByCustomType from './samples/OpenChannelCategorizeByCustomType'; 10 | import OpenChannelThumbnails from './samples/OpenChannelThumbnails'; 11 | import OpenChannelMessageThreading from './samples/OpenChannelMessageThreading'; 12 | import OpenChannelSendAnAdminMessage from './samples/OpenChannelSendAnAdminMessage' 13 | import OpenChannelFreeze from './samples/OpenChannelFreeze'; 14 | import OpenChannelSendAndReceiveVariousTypesOfFiles from './samples/OpenChannelSendAndReceiveVariousTypesOfFiles' 15 | import OpenChannelCopyMessage from './samples/OpenChannelCopyMessage'; 16 | import OpenChannelDisplayOGTags from './samples/OpenChannelDisplayOGTags'; 17 | import OpenChannelMetadataAndMetacounter from './samples/OpenChannelMetadataAndMetacounter'; 18 | import OpenChannelAddExtraDataToMessage from './samples/OpenChannelAddExtraDataToMessage'; 19 | import OpenChannelReportAMessageUserChannel from './samples/OpenChannelReportAMessageUserChannel' 20 | import OpenChannelUsersOnlineStatus from './samples/OpenChannelUsersOnlineStatus'; 21 | import OpenChannelCategorizeMessagesByCustomType from './samples/OpenChannelCategorizeMessagesByCustomType'; 22 | import OpenChannelUserProfileUpdate from './samples/OpenChannelUserProfileUpdate'; 23 | import OpenChannelStructuredData from './samples/OpenChannelStructuredData'; 24 | import OpenChannelUserDoNotDisturbOrSnooze from './samples/OpenChannelUserDoNotDisturbOrSnooze'; 25 | import OpenChannelRegisterUnregisterOperator from './samples/OpenChannelRegisterUnregisterOperator'; 26 | import OpenChannelMuteUnmuteUsers from './samples/OpenChannelMuteUnmuteUsers'; 27 | import OpenChannelBanUnbanUsers from './samples/OpenChannelBanUnbanUsers'; 28 | import OpenChannelUpdateDeleteMessageByOperator from './samples/OpenChannelUpdateDeleteMessageByOperator'; 29 | import OpenChannelMembersListOrder from './samples/OpenChannelMembersListOrder'; 30 | 31 | // Group channel samples 32 | import GroupChannelTypingIndicatorSample from './samples/GroupChannelTypingIndicatorSample'; 33 | import GroupChannelMessageThreading from './samples/GroupChannelMessageThreading' 34 | import GroupChannelSendAnAdminMessage from './samples/GroupChannelSendAnAdminMessage'; 35 | import GroupChannelFreezeUnfreeze from './samples/GroupChannelFreezeUnfreeze'; 36 | import GroupChannelDisplayOGTags from './samples/GroupChannelDisplayOGTags'; 37 | import GroupChannelMarkMessagesAsRead from './samples/GroupChannelMarkMessagesAsRead'; 38 | import GroupChannelReactToAMessage from './samples/GroupChannelReactToAMessage'; 39 | import GroupChannelCategorizeByCustomType from './samples/GroupChannelCategorizeByCustomType'; 40 | import GroupChannelReportAMessageUserChannel from './samples/GroupChannelReportAMessageUserChannel' 41 | import GroupChannelCategorizeMessagesByCustomType from './samples/GroupChannelCategorizeMessagesByCustomType'; 42 | import GroupChannelRetrieveOnlineStatus from './samples/GroupChannelRetrieveOnlineStatus'; 43 | import GroupChannelRegisterUnregisterOperator from './samples/GroupChannelRegisterUnregisterOperator'; 44 | import GroupChannelReactChannelTypes from './samples/GroupChannelTypes'; 45 | import GroupChannelUpdateDeleteMessageByOperator from './samples/GroupChannelUpdateDeleteMessageByOperator'; 46 | import GroupChannelArchive from './samples/GroupChannelArchive' 47 | import GroupChannelMuteUnmuteUsers from './samples/GroupChannelMuteUnmuteUsers'; 48 | import GroupChannelBanUnbanUsers from './samples/GroupChannelBanUnbanUsers'; 49 | import GroupChannelRetrieveNumberOfMembersHaventReceivedMessage from './samples/GroupChannelRetrieveNumberOfMembersHaventReceivedMessage'; 50 | import GroupChannelOperatorsList from './samples/GroupChannelOperatorsList'; 51 | import GroupChannelMembersListOrder from './samples/GroupChannelMembersListOrder'; 52 | import GroupChannelRetrieveAListOfBannedOrMutedUsers from './samples/GroupChannelRetrieveAListOfBannedOrMutedUsers'; 53 | import GroupChannelUserProfileUpdate from './samples/GroupChannelUserProfileUpdate'; 54 | import GroupChannelRetrieveNumberOfMembersHaventReadMessage from './samples/GroupChannelRetrieveNumberOfMembersHaventReadMessage'; 55 | import GroupChannelUserDoNotDisturbOrSnooze from './samples/GroupChannelUserDoNotDisturbOrSnooze'; 56 | import GroupChannelStructuredData from './samples/GroupChannelStructuredData'; 57 | import GroupChannelPolls from './samples/GroupChannelPolls.js'; 58 | import GroupChannelUsersOnlineStatus from './samples/GroupChannelUsersOnlineStatus'; 59 | import GroupChannelScheduledMessages from './samples/GroupChannelScheduledMessages'; 60 | import GroupChannelPinnedMessages from './samples/GroupChannelPinnedMessages'; 61 | 62 | const App = () => { 63 | return ( 64 |
65 | 66 | } /> 67 | } /> 68 | } /> 69 | } /> 70 | } /> 71 | } /> 72 | } /> 73 | } /> 74 | } /> 75 | } /> 76 | } /> 77 | } /> 78 | } /> 79 | } /> 80 | } /> 81 | } /> 82 | } /> 83 | } /> 84 | } /> 85 | } /> 86 | } /> 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | } /> 92 | } /> 93 | } /> 94 | } /> 95 | } /> 96 | } /> 97 | } /> 98 | } /> 99 | } /> 100 | } /> 101 | } /> 102 | } /> 103 | } /> 104 | } /> 105 | } /> 106 | } /> 107 | } /> 108 | } /> 109 | } /> 110 | } /> 111 | } /> 112 | } /> 113 | } /> 114 | } /> 115 | } /> 116 | } /> 117 | } /> 118 | } /> 119 | } /> 120 | } /> 121 | 122 |
123 | ); 124 | } 125 | 126 | export default App; 127 | -------------------------------------------------------------------------------- /src/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 |
8 |

Basic Code Samples

9 | 13 |
14 |
15 |

Open Channel Code Samples

16 | 39 |
40 |
41 |

Group Channel Code Samples

42 | 74 |
75 |
76 | ); 77 | } 78 | 79 | export default Home; 80 | -------------------------------------------------------------------------------- /src/constants/constants.js: -------------------------------------------------------------------------------- 1 | export const SENDBIRD_INFO = { appId: 'FEA2129A-EA73-4EB9-9E0B-EC738E7EB768' }; 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root, #root>div { 2 | height: 100% 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | img{ 15 | width:300px; 16 | } 17 | 18 | button{ 19 | border: none; 20 | color: white; 21 | padding: 14px 32px; 22 | text-align: center; 23 | text-decoration: none; 24 | display: inline-block; 25 | font-size: 16px; 26 | border-radius:2px; 27 | cursor: pointer; 28 | background-color: #424242; 29 | } 30 | 31 | input{ 32 | width: 30%; 33 | padding: 6px; 34 | border: 1px solid #cbcbcb; 35 | outline:none; 36 | } 37 | 38 | 39 | p { 40 | margin: 0; 41 | } 42 | 43 | code { 44 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 45 | monospace; 46 | } 47 | .container{ 48 | height:100%; 49 | background:white; 50 | display: flex; 51 | } 52 | 53 | .channel-type{ 54 | padding: 12px; 55 | color: #6210CC; 56 | } 57 | 58 | .group-card { 59 | width: fit-content; 60 | padding: 15px; 61 | margin-top: 20px; 62 | border: 1px solid white; 63 | border-radius: 10px; 64 | } 65 | 66 | .card { 67 | width: fit-content; 68 | padding: 15px; 69 | margin-left: 60px; 70 | border: 1px solid black; 71 | border-radius: 10px; 72 | } 73 | 74 | .card-name { 75 | font-weight: bold; 76 | } 77 | 78 | .channel-list { 79 | background: white; 80 | flex:1; 81 | border-right: 1px solid #cbcbcb; 82 | overflow-y: scroll; 83 | } 84 | 85 | .channel-list-item{ 86 | background: #fcfcfc; 87 | padding:12px; 88 | border-top: 1px solid #ececec; 89 | display:flex; 90 | justify-content: space-between; 91 | align-items: center; 92 | min-height: 42px; 93 | } 94 | 95 | .channel-list-item:hover{ 96 | background:#f0f0f0; 97 | } 98 | 99 | .channel-list-item:hover .channel-icon{ 100 | visibility: visible; 101 | } 102 | 103 | .sort-channels { 104 | width: 100%; 105 | font-size: 16px; 106 | padding: 7px; 107 | cursor: pointer; 108 | } 109 | 110 | .unarchive { 111 | transform: rotate(180deg); 112 | } 113 | 114 | .archived-channel { 115 | padding: 10px; 116 | } 117 | 118 | .last-message{ 119 | font-weight:300; 120 | } 121 | 122 | .channel-list-item-name{ 123 | font-weight: 600; 124 | cursor: pointer; 125 | overflow: hidden; 126 | max-width: 75ch; 127 | text-overflow: ellipsis; 128 | white-space: nowrap; 129 | flex:1; 130 | } 131 | 132 | .channel-list-item-sup { 133 | margin: 0 0 0 5px; 134 | font-size: 10px; 135 | } 136 | 137 | .channel-icon{ 138 | visibility:hidden; 139 | width: 20px; 140 | } 141 | 142 | .channel{ 143 | background: white; 144 | flex: 3; 145 | display: flex; 146 | flex-direction: column; 147 | overflow-y: scroll; 148 | margin-bottom: 102px; 149 | } 150 | 151 | .channel-fixed-header { 152 | margin-top: 160px; 153 | } 154 | 155 | .thread { 156 | max-width: 35vw; 157 | } 158 | 159 | .thread, .thread-input { 160 | border-left: 1px solid lightgray; 161 | } 162 | 163 | .underline { 164 | width: 100%; 165 | border-bottom: 1px solid lightgray; 166 | } 167 | 168 | .threads { 169 | max-width: 35vw; 170 | } 171 | 172 | .threads, .threads-input { 173 | border-left: 1px solid lightgray; 174 | } 175 | 176 | .underline { 177 | width: 100%; 178 | border-bottom: 1px solid lightgray; 179 | } 180 | 181 | .channel-controls { 182 | display: flex; 183 | align-items: center; 184 | justify-content: space-between; 185 | } 186 | 187 | .leave-channel{ 188 | margin-left: 12px; 189 | padding: 8px; 190 | } 191 | 192 | .freeze-channel { 193 | margin-right: 12px; 194 | } 195 | 196 | .freeze-channel input { 197 | width: auto; 198 | } 199 | 200 | .message-list{ 201 | height: 100%; 202 | display: flex; 203 | flex-direction: column; 204 | 205 | } 206 | 207 | .message-item{ 208 | margin: 12px; 209 | display:flex; 210 | flex-direction: row-reverse; 211 | justify-content: flex-end; 212 | } 213 | 214 | .message.message-from-you{ 215 | margin-left: auto; 216 | background: #39a9ff; 217 | flex-direction: row; 218 | margin-right: 12px; 219 | } 220 | 221 | .message-item.message-from-you{ 222 | flex-direction: row; 223 | } 224 | 225 | .oc-message, .oc-message-og-tags{ 226 | padding:12px; 227 | display:flex; 228 | } 229 | 230 | .oc-message-og-tags { 231 | flex-direction: column; 232 | max-width: 50%; 233 | padding: 5px; 234 | margin-left: 7vw; 235 | margin-top: -10px; 236 | box-sizing: border-box; 237 | border: 1px solid black; 238 | } 239 | 240 | .oc-message, .message-og-tags { 241 | padding:12px 0px; 242 | display:flex; 243 | } 244 | 245 | .message-og-tags { 246 | flex-direction: column; 247 | padding: 5px; 248 | box-sizing: border-box; 249 | } 250 | 251 | .og-tags-title { 252 | margin: 0 0 10px; 253 | } 254 | 255 | .og-tags-description, .og-tags-url, .metadata-modal-list { 256 | margin: 0 0 5px; 257 | } 258 | 259 | .og-tags-img, .show-users-status-btn, .members-invite-btn { 260 | width: 100%; 261 | } 262 | 263 | .oc-admin-message { 264 | background: rgba(255, 0, 0, 0.05); 265 | } 266 | 267 | .report-notification { 268 | background: rgba(0, 255, 0, 0.5); 269 | color: white; 270 | width: 100%; 271 | text-align: center; 272 | cursor: pointer; 273 | margin: 5px 0; 274 | padding: 5px; 275 | } 276 | 277 | .message{ 278 | border-radius: 8px; 279 | width:70%; 280 | background: #4c4c4c; 281 | color:white; 282 | padding: 24px; 283 | margin-left: 12px; 284 | } 285 | 286 | .admin-message { 287 | background: rgba(255, 0, 0, 0.05); 288 | width: 100%; 289 | color: black; 290 | margin-left: 0; 291 | } 292 | 293 | .message-info{ 294 | margin-bottom:4px; 295 | display: flex; 296 | justify-content: space-between; 297 | line-height: 24px; 298 | } 299 | 300 | .message-icon{ 301 | visibility:hidden; 302 | width: 14px; 303 | filter: brightness(0) invert(1); 304 | } 305 | 306 | .oc-message-icon, .oc-channel-list-icon{ 307 | visibility:hidden; 308 | width: 14px; 309 | } 310 | 311 | 312 | .message:hover .message-icon, .channel-list-item:hover .oc-channel-list-icon, .member-item:hover .message-icon, .double_tick-icon, .member-item-wrapper .message-icon { 313 | visibility: visible; 314 | } 315 | 316 | .double_tick-icon-read { 317 | filter: none; 318 | } 319 | 320 | .mute-button { 321 | margin-top: 3px; 322 | } 323 | 324 | .member-item .message-icon { 325 | filter: none; 326 | } 327 | 328 | .oc-document-message, .message-user-info, .react-button-wrapper, .reactions-list, .metadata-modal-list, .create-channel-wrapper { 329 | display: flex; 330 | } 331 | 332 | .reactions-list { 333 | list-style: none; 334 | margin: 0; 335 | padding-left: 0; 336 | background: white; 337 | border-radius: 8px; 338 | } 339 | 340 | .reactions-list li { 341 | margin-right: 2px; 342 | padding: 4px; 343 | } 344 | 345 | .react-button-wrapper { 346 | margin: 5px 5px 0 0; 347 | width: 100%; 348 | justify-content: flex-end; 349 | } 350 | 351 | .react-button-img { 352 | width: 20px; 353 | filter: none; 354 | } 355 | 356 | .reactions { 357 | margin: 5px 0 0; 358 | } 359 | 360 | .reactions-item { 361 | cursor: pointer; 362 | margin-right: 5px; 363 | } 364 | 365 | .reactions-item-inner { 366 | font-size: 10px; 367 | } 368 | 369 | .oc-document-message-icon { 370 | width: 24px; 371 | } 372 | 373 | .oc-message:hover .oc-message-icon, .message-input .oc-message-icon, .metadata-modal-list .oc-message-icon { 374 | visibility: visible; 375 | } 376 | 377 | .oc-message-sender-name{ 378 | color:#6210CC; 379 | margin-left:4px; 380 | font-weight: 600; 381 | margin-right:4px; 382 | } 383 | 384 | .message-sender-name{ 385 | font-weight: 600; 386 | margin-right:4px; 387 | } 388 | 389 | .channel-header{ 390 | padding: 12px; 391 | font-size: 24px; 392 | font-weight: 800; 393 | } 394 | 395 | .channel-header-wrapper { 396 | position: fixed; 397 | width: 100%; 398 | top: 0; 399 | } 400 | 401 | .members-list{ 402 | border-left: 1px solid #cbcbcb; 403 | padding: 12px; 404 | } 405 | 406 | .member-item{ 407 | padding: 8px 0px 0px 0px; 408 | cursor: pointer; 409 | font-weight: 600; 410 | border-bottom: 1px solid #cfcfcf; 411 | padding-bottom: 4px; 412 | display:flex; 413 | align-items: center; 414 | justify-content: space-between; 415 | } 416 | 417 | .member-item-wrapper { 418 | display: flex; 419 | flex-direction: column; 420 | margin: 5px 0; 421 | } 422 | 423 | .member-item:hover{ 424 | background:#f0f0f0; 425 | } 426 | 427 | .member-item-name{ 428 | margin-left:12px; 429 | } 430 | 431 | .member-list-btn { 432 | margin: 0 0 5px; 433 | } 434 | 435 | .member-selected{ 436 | color: #62EEAB; 437 | } 438 | 439 | .operators-list { 440 | padding: 20px 0; 441 | font-weight: bold; 442 | } 443 | 444 | .operator-item { 445 | padding: 8px 0px 0px 0px; 446 | cursor: pointer; 447 | font-weight: 600; 448 | border-bottom: 1px solid #cfcfcf; 449 | padding-bottom: 4px; 450 | display:flex; 451 | align-items: center; 452 | justify-content: space-between; 453 | } 454 | 455 | .operator-item:hover { 456 | background:#f0f0f0; 457 | } 458 | 459 | .message-input{ 460 | position: fixed; 461 | bottom: 0px; 462 | display: flex; 463 | border-top: 1px solid #cbcbcb; 464 | width: 100%; 465 | padding: 12px; 466 | background: white; 467 | } 468 | 469 | .overlay{ 470 | position: fixed; 471 | display: flex; 472 | justify-content: center; 473 | align-items: center; 474 | width: 100%; 475 | height: 100%; 476 | background: #ecececb0; 477 | z-index: 1; 478 | } 479 | 480 | .overlay-content{ 481 | overflow-y: scroll; 482 | max-height: 400px; 483 | padding:80px; 484 | opacity: 1; 485 | background: white; 486 | border: 1px solid #cbcbcb; 487 | width: 40%; 488 | } 489 | 490 | .custom-type-messages { 491 | position: fixed; 492 | display: flex; 493 | justify-content: center; 494 | align-items: center; 495 | width: 100%; 496 | height: 100%; 497 | z-index: 1; 498 | } 499 | 500 | .custom-type-messages-content { 501 | overflow-y: scroll; 502 | max-height: 400px; 503 | padding:20px; 504 | opacity: 1; 505 | background: white; 506 | border: 1px solid #cbcbcb; 507 | width: 40%; 508 | } 509 | 510 | .custom-type-button { 511 | margin: 12px 0 0 12px; 512 | } 513 | 514 | .form-input{ 515 | margin-top: 12px; 516 | margin-bottom: 12px; 517 | padding:18px; 518 | width:100%; 519 | border-radius: 2px; 520 | } 521 | 522 | .form-button{ 523 | margin-right:4px; 524 | } 525 | 526 | .user-submit-button{ 527 | background-color: #6210CC; 528 | } 529 | 530 | .create-channel-wrapper { 531 | align-items: baseline; 532 | } 533 | 534 | .channel-create-button{ 535 | background-color: #62EEAB; 536 | width: 100%; 537 | } 538 | 539 | .create-channel-checkbox { 540 | width: auto; 541 | margin: 0 5px 0 10px; 542 | } 543 | 544 | .select-custom-type { 545 | padding: 12px; 546 | } 547 | 548 | .select-custom-type select { 549 | width: 100%; 550 | font-size: 16px; 551 | padding-top: 12px; 552 | outline: none; 553 | border: none; 554 | cursor: pointer; 555 | } 556 | 557 | .send-message-button{ 558 | border-radius: 0px 4px 4px 0px; 559 | margin-right:8px; 560 | } 561 | 562 | .control-button { 563 | background: inherit; 564 | padding:4px; 565 | margin-right:1px; 566 | } 567 | 568 | .error{ 569 | background:rgb(26, 22, 22); 570 | display:flex; 571 | padding:24px; 572 | width:100%; 573 | } 574 | 575 | .file-upload-label, .user-profile-image-upload-label, .number-of-undelivered-members-btn { 576 | cursor: pointer; 577 | } 578 | 579 | .file-upload-label img{ 580 | width: 46px; 581 | vertical-align: middle; 582 | } 583 | 584 | .message-type-add { 585 | cursor: pointer; 586 | padding: 12px; 587 | } 588 | 589 | .message-type-label { 590 | margin: 0 10px; 591 | } 592 | 593 | .message-type-select { 594 | font-size: 16px; 595 | padding: 7px; 596 | outline: none; 597 | cursor: pointer; 598 | } 599 | 600 | .profile-image-fallback{ 601 | width: 50px; 602 | background: #e9e9e9; 603 | height: 50px; 604 | border-radius: 25px; 605 | display: flex; 606 | align-items: center; 607 | justify-content: center; 608 | } 609 | 610 | .profile-image{ 611 | width: 50px; 612 | height: 50px; 613 | border-radius: 25px; 614 | object-fit: cover; 615 | } 616 | 617 | .typing-indicator { 618 | position: fixed; 619 | bottom:80px; 620 | background-color: #fff; 621 | opacity: 0.8; 622 | margin-left: 20px; 623 | margin-top:20px; 624 | font-weight: 500; 625 | } 626 | 627 | .display-none { 628 | display: none; 629 | } 630 | 631 | .message-input-column { 632 | flex-direction: column; 633 | } 634 | 635 | .freeze-notification { 636 | position: sticky; 637 | top:0; 638 | width: 100%; 639 | line-height: 30px; 640 | opacity: 70%; 641 | background-color: rgb(173,201,255); 642 | text-align: center; 643 | } 644 | 645 | .frozen-icon { 646 | width: 20px; 647 | margin-right: 10px; 648 | } 649 | 650 | .underline { 651 | width: 100%; 652 | border-bottom: 1px solid lightgray; 653 | } 654 | 655 | .user-copied-message { 656 | position: fixed; 657 | right: 20px; 658 | } 659 | 660 | .image-size-error { 661 | width: 100%; 662 | background: rgba(255, 0, 0, 0.2); 663 | padding: 5px; 664 | margin: 5px 0; 665 | text-align: center; 666 | } 667 | 668 | .channel-header-wrapper { 669 | display: flex; 670 | justify-content: space-between; 671 | } 672 | 673 | .create-metadata-conteiner { 674 | display: flex; 675 | justify-content: space-between; 676 | align-items: baseline; 677 | padding: 12px; 678 | } 679 | 680 | .create-metadata-title { 681 | margin: 0; 682 | padding: 5px; 683 | font-size: 16px; 684 | } 685 | 686 | .create-metadata-btn { 687 | padding: 5px 10px; 688 | } 689 | 690 | .metadata-modal-input { 691 | display: flex; 692 | margin: 0 0 10px; 693 | } 694 | 695 | .choise-report-obj { 696 | cursor: pointer; 697 | color: #6210CC; 698 | text-decoration: underline; 699 | } 700 | 701 | .number-of-undelivered-members { 702 | padding: 5px 0; 703 | display: flex; 704 | justify-content: space-between; 705 | } 706 | 707 | .number-of-undelivered-message-btn:hover::after { 708 | content: attr(data-title); 709 | position: absolute; 710 | border: 1px solid gray; 711 | background-color: darkgray; 712 | border-radius: 5px; 713 | padding: 2px 4px; 714 | font-size: 10px; 715 | } 716 | 717 | .banned-muted-users-list { 718 | margin-top: 20px; 719 | } 720 | 721 | .show-users-status-btn { 722 | display: block; 723 | margin-top: 5px; 724 | } 725 | 726 | .user-online-status { 727 | margin: 0 0 0 5px; 728 | } 729 | .user-profile-title { 730 | margin-top: 0; 731 | } 732 | 733 | .user-profile-image-wrapper, .user-profile-nickname-wrapper { 734 | display: flex; 735 | align-items: center; 736 | justify-content: space-between; 737 | padding: 5px; 738 | } 739 | 740 | .user-profile-nickname-wrapper { 741 | margin: 0 0 10px; 742 | } 743 | 744 | .user-profile-image { 745 | border-radius: 50%; 746 | padding: 5px; 747 | width: 50px; 748 | height: 50px; 749 | object-fit: cover; 750 | } 751 | 752 | .user-profile-nickname-input { 753 | width: auto; 754 | } 755 | 756 | .user-profile-nickname-button { 757 | border: none; 758 | background: transparent; 759 | color: black; 760 | } 761 | 762 | .user-profile-image-upload-label, .user-profile-nickname-button { 763 | font-family: Arial, Helvetica, sans-serif; 764 | font-weight: 700; 765 | padding: 5px; 766 | } 767 | 768 | .number-of-undelivered-members { 769 | padding: 5px 0; 770 | display: flex; 771 | justify-content: space-between; 772 | } 773 | 774 | .number-of-undelivered-message-btn:hover::after { 775 | content: attr(data-title); 776 | position: absolute; 777 | border: 1px solid gray; 778 | background-color: darkgray; 779 | border-radius: 5px; 780 | padding: 2px 4px; 781 | font-size: 10px; 782 | } 783 | 784 | .do-not-disturb-wrapper, .do-not-disturb-start-wrapper, .do-not-disturb-end-wrapper, .notifications-snooze-wrapper, .notifications-snooze-input-wrapper { 785 | display: flex; 786 | } 787 | 788 | .do-not-disturb-wrapper, .notifications-snooze-wrapper { 789 | flex-direction: column; 790 | } 791 | 792 | .do-not-disturb-wrapper, .do-not-disturb-start-wrapper, .do-not-disturb-end-wrapper, .notifications-snooze-wrapper, .notifications-snooze-input-wrapper { 793 | margin-bottom: 10px; 794 | } 795 | 796 | .notifications-overlay-content { 797 | padding: 40px; 798 | width: 50%; 799 | max-height: 500px; 800 | } 801 | 802 | .notification-modal-label, .notification-modal-input { 803 | margin-right: 10px; 804 | } 805 | 806 | .notification-modal-input { 807 | width: 40%; 808 | } 809 | 810 | .open-poll-btn { 811 | padding: 5px; 812 | background: transparent; 813 | color: black; 814 | margin-left: 2px; 815 | } 816 | 817 | .create-poll-modal, .create-poll-modal_inputs { 818 | display: flex; 819 | flex-direction: column; 820 | } 821 | 822 | .create-poll-modal { 823 | position: relative; 824 | } 825 | 826 | .create-poll-modal input { 827 | padding: 14px 32px 14px 6px; 828 | } 829 | 830 | .create-poll-modal_inputs { 831 | justify-content: space-between; 832 | margin-bottom: 10px; 833 | } 834 | 835 | .create-poll-modal .input_wrapper { 836 | margin-bottom: 5px; 837 | } 838 | 839 | .create-poll-modal .input_wrapper input[type="text"] { 840 | width: 60%; 841 | } 842 | 843 | .create-poll-modal h3 { 844 | text-align: center; 845 | } 846 | 847 | .option-add_btn { 848 | border: 1px solid #cbcbcb; 849 | border-radius: 0 4px 4px 0; 850 | font-size: 14px; 851 | } 852 | 853 | .poll-close_btn { 854 | position: absolute; 855 | top: 40px; 856 | right: 40px; 857 | cursor: pointer; 858 | } 859 | 860 | .poll-create_btn { 861 | margin: 0 auto; 862 | } 863 | 864 | .poll-options { 865 | width: 100%; 866 | } 867 | 868 | .options_item { 869 | padding: 0 4px; 870 | margin-right: 4px; 871 | background: #cbcbcb; 872 | color: white; 873 | border-radius: 4px; 874 | cursor: pointer; 875 | } 876 | 877 | .oc-members-list { 878 | margin-bottom: 80px; 879 | overflow-x: hidden; 880 | overflow-y: scroll; 881 | } 882 | 883 | .scheduled-messages-list-modal { 884 | position: relative; 885 | } 886 | 887 | .close-button { 888 | background: none; 889 | padding: 0; 890 | position: absolute; 891 | top: 24px; 892 | right: 24px; 893 | } 894 | 895 | .update-input { 896 | display: flex; 897 | } 898 | 899 | .update-input input { 900 | width: 50%; 901 | } 902 | 903 | .scheduled-message-header { 904 | font-weight: 800; 905 | margin-bottom: 30px; 906 | } 907 | 908 | .schedule-message { 909 | margin-top: 20px; 910 | } 911 | 912 | .cancel-button { 913 | margin-right: 10px; 914 | } 915 | 916 | .scheduled-messages-settings-modal { 917 | z-index: 2; 918 | } 919 | 920 | .settings-header { 921 | margin-bottom: 30px; 922 | font-weight: 800; 923 | } 924 | 925 | .schedule-message-error { 926 | color: red; 927 | margin-bottom: 20px; 928 | } 929 | 930 | [data-tooltip] { 931 | position: relative; 932 | } 933 | 934 | [data-tooltip]::after { 935 | content: attr(data-tooltip); 936 | position: absolute; 937 | left: 0; 938 | top: 0; 939 | opacity: 0; 940 | transition: 1s; 941 | pointer-events: none; 942 | padding: 2px; 943 | background-color: #424242; 944 | border-radius: 4px; 945 | font-size: 10px; 946 | width: max-content; 947 | z-index: 100; 948 | } 949 | 950 | [data-tooltip]:hover::after { 951 | opacity: 1; 952 | top: -1.5em; 953 | } 954 | 955 | .option-icon { 956 | visibility:hidden; 957 | width: 14px; 958 | filter: brightness(0) invert(1); 959 | } 960 | 961 | .option_wrapper:hover .option-icon { 962 | visibility: visible; 963 | } 964 | 965 | .create-poll-modal .option input[type="text"] { 966 | width: 55%; 967 | } 968 | 969 | .add-new-option { 970 | background: white; 971 | border-radius: 4px; 972 | color: black; 973 | padding: 5px; 974 | font-size: 12px; 975 | margin-top: 10px; 976 | } 977 | 978 | .poll-message { 979 | position: relative; 980 | } 981 | 982 | .poll-status { 983 | position: absolute; 984 | bottom: 24px; 985 | right: 24px; 986 | font-size: 10px; 987 | padding: 5px; 988 | } 989 | 990 | .option_wrapper label, .add-new-option { 991 | margin-right: 10px; 992 | } 993 | 994 | .poll-status_wrapper { 995 | width: 100%; 996 | min-height: 25px; 997 | } 998 | 999 | .vote { 1000 | margin-right: 5px; 1001 | padding: 5px; 1002 | font-size: 10px; 1003 | background: white; 1004 | color: black; 1005 | border-radius: 50%; 1006 | } 1007 | 1008 | .pin-checkbox { 1009 | display: inline-block; 1010 | } 1011 | 1012 | .message-img { 1013 | max-width: 100%; 1014 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { HashRouter as Router } from "react-router-dom"; 6 | 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/samples/BasicOpenChannelSample.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const BasicOpenChannelSample = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false 34 | }); 35 | 36 | //need to access state in message reeived callback 37 | const stateRef = useRef(); 38 | stateRef.current = state; 39 | 40 | const channelRef = useRef(); 41 | 42 | const scrollToBottom = (item, smooth) => { 43 | item?.scrollTo({ 44 | top: item.scrollHeight, 45 | behavior: smooth 46 | }) 47 | } 48 | 49 | useEffect(() => { 50 | scrollToBottom(channelRef.current) 51 | }, [state.currentlyJoinedChannel]) 52 | 53 | useEffect(() => { 54 | scrollToBottom(channelRef.current, 'smooth') 55 | }, [state.messages]) 56 | 57 | const onError = (error) => { 58 | updateState({ ...state, error: error.message }); 59 | console.log(error); 60 | } 61 | 62 | const handleJoinChannel = async (channelUrl) => { 63 | if (state.currentlyJoinedChannel?.url === channelUrl) { 64 | return null; 65 | } 66 | const { channels } = state; 67 | updateState({ ...state, loading: true }); 68 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 69 | await channelToJoin.enter(); 70 | const [messages, error] = await loadMessages(channelToJoin); 71 | 72 | if (error) { 73 | return onError(error); 74 | } 75 | 76 | // setup connection event handlers 77 | const connectionHandler = new ConnectionHandler(); 78 | 79 | connectionHandler.onReconnectSucceeded = async () => { 80 | const [messages, error] = await loadMessages(channelToJoin); 81 | 82 | updateState({ ...stateRef.current, messages: messages }); 83 | } 84 | 85 | sb.addConnectionHandler(uuid(), connectionHandler); 86 | 87 | 88 | //listen for incoming messages 89 | const channelHandler = new OpenChannelHandler(); 90 | channelHandler.onMessageUpdated = (channel, message) => { 91 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 92 | const updatedMessages = [...stateRef.current.messages]; 93 | updatedMessages[messageIndex] = message; 94 | updateState({ ...stateRef.current, messages: updatedMessages }); 95 | } 96 | 97 | channelHandler.onMessageReceived = (channel, message) => { 98 | const updatedMessages = [...stateRef.current.messages, message]; 99 | updateState({ ...stateRef.current, messages: updatedMessages }); 100 | }; 101 | 102 | channelHandler.onMessageDeleted = (channel, message) => { 103 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 104 | return messageObject.messageId !== message; 105 | }); 106 | updateState({ ...stateRef.current, messages: updatedMessages }); 107 | } 108 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 109 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 110 | } 111 | 112 | const handleLeaveChannel = async () => { 113 | const { currentlyJoinedChannel } = state; 114 | await currentlyJoinedChannel.exit(); 115 | 116 | updateState({ ...state, currentlyJoinedChannel: null }) 117 | } 118 | 119 | const handleCreateChannel = async () => { 120 | const { channelNameInputValue } = state; 121 | const [openChannel, error] = await createChannel(channelNameInputValue); 122 | if (error) { 123 | return onError(error); 124 | } 125 | const updatedChannels = [openChannel, ...state.channels]; 126 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 127 | } 128 | 129 | const handleDeleteChannel = async (channelUrl) => { 130 | const [channel, error] = await deleteChannel(channelUrl); 131 | if (error) { 132 | return onError(error); 133 | } 134 | const updatedChannels = state.channels.filter((channel) => { 135 | return channel.url !== channelUrl; 136 | }); 137 | updateState({ ...state, channels: updatedChannels }); 138 | } 139 | 140 | const handleUpdateChannel = async () => { 141 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 142 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 143 | if (error) { 144 | return onError(error); 145 | } 146 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 147 | const updatedChannels = [...channels]; 148 | updatedChannels[indexToReplace] = updatedChannel; 149 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 150 | } 151 | 152 | const toggleChannelDetails = (channel) => { 153 | if (channel) { 154 | updateState({ ...state, currentlyUpdatingChannel: channel }); 155 | } else { 156 | updateState({ ...state, currentlyUpdatingChannel: null }); 157 | } 158 | } 159 | 160 | const toggleShowCreateChannel = () => { 161 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 162 | } 163 | 164 | const onChannelNamenIputChange = (e) => { 165 | const channelNameInputValue = e.currentTarget.value; 166 | updateState({ ...state, channelNameInputValue }); 167 | } 168 | 169 | const onUserNameInputChange = (e) => { 170 | const userNameInputValue = e.currentTarget.value; 171 | updateState({ ...state, userNameInputValue }); 172 | } 173 | 174 | const onUserIdInputChange = (e) => { 175 | const userIdInputValue = e.currentTarget.value; 176 | updateState({ ...state, userIdInputValue }); 177 | } 178 | 179 | const onMessageInputChange = (e) => { 180 | const messageInputValue = e.currentTarget.value; 181 | updateState({ ...state, messageInputValue }); 182 | } 183 | 184 | const sendMessage = async () => { 185 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 186 | 187 | if (messageToUpdate) { 188 | const userMessageUpdateParams = {}; 189 | userMessageUpdateParams.message = state.messageInputValue; 190 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 191 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 192 | messages[messageIndex] = updatedMessage; 193 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 194 | } else { 195 | const userMessageParams = {}; 196 | userMessageParams.message = state.messageInputValue; 197 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 198 | const updatedMessages = [...messages, message]; 199 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 200 | }).onFailed((error) => { 201 | console.log(error) 202 | console.log("failed") 203 | }); 204 | } 205 | } 206 | 207 | const onFileInputChange = async (e) => { 208 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 209 | const { currentlyJoinedChannel, messages } = state; 210 | const fileMessageParams = {}; 211 | fileMessageParams.file = e.currentTarget.files[0]; 212 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 213 | const updatedMessages = [...messages, message]; 214 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 215 | }).onFailed((error) => { 216 | console.log(error) 217 | console.log("failed") 218 | }); 219 | } 220 | } 221 | 222 | const handleDeleteMessage = async (messageToDelete) => { 223 | const { currentlyJoinedChannel } = state; 224 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 225 | } 226 | 227 | const updateMessage = async (message) => { 228 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 229 | } 230 | 231 | const setupUser = async () => { 232 | const { userNameInputValue, userIdInputValue } = state; 233 | const sendbirdChat = await SendbirdChat.init({ 234 | appId: SENDBIRD_INFO.appId, 235 | localCacheEnabled: true, 236 | modules: [new OpenChannelModule()] 237 | }); 238 | 239 | try { 240 | await sendbirdChat.connect(userIdInputValue); 241 | } catch (e) { 242 | console.log("error", e) 243 | } 244 | 245 | await sendbirdChat.setChannelInvitationPreference(true); 246 | 247 | const userUpdateParams = {}; 248 | userUpdateParams.nickname = userNameInputValue; 249 | userUpdateParams.userId = userIdInputValue; 250 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams).then(data => console.log(data)); 251 | 252 | sb = sendbirdChat; 253 | 254 | 255 | 256 | updateState({ ...state, loading: true }); 257 | const [channels, error] = await loadChannels(); 258 | if (error) { 259 | return onError(error); 260 | } 261 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 262 | } 263 | 264 | if (state.loading) { 265 | return
Loading...
266 | } 267 | 268 | if (state.error) { 269 | return
{state.error} check console for more information.
270 | } 271 | 272 | console.log('- - - - State object very useful for debugging - - - -'); 273 | console.log(state); 274 | 275 | return ( 276 | <> 277 | 284 | 290 | 295 | 300 | 305 | 310 | 316 | 317 | 318 | ); 319 | }; 320 | 321 | // Chat UI Components 322 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 323 | return ( 324 |
325 |
326 |

Open Channels

327 | 328 |
329 | {channels.map(channel => { 330 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 331 | return ( 332 |
333 |
{ handleJoinChannel(channel.url) }}> 335 | {channel.name} 336 |
337 | {userIsOperator && 338 |
339 | 342 | 345 |
346 | } 347 |
348 | ); 349 | })} 350 |
351 | ); 352 | } 353 | 354 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 355 | if (currentlyJoinedChannel) { 356 | return
357 | {currentlyJoinedChannel.name} 358 |
359 | 360 |
361 |
{children}
362 |
; 363 | } 364 | return
; 365 | } 366 | 367 | const ChannelHeader = ({ children }) => { 368 | return
{children}
; 369 | } 370 | 371 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 372 | return messages.map(message => { 373 | return ( 374 |
375 | 380 |
); 381 | }) 382 | } 383 | 384 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 385 | if (!message.sender) return null; if (message.url) { 386 | return ( 387 |
388 |
{timestampToTime(message.createdAt)}
389 |
{message.sender.nickname}{' '}
390 | 391 |
392 | ); 393 | } 394 | 395 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 396 | return ( 397 |
398 |
{timestampToTime(message.createdAt)}
399 | 400 |
{message.sender.nickname}{':'}
401 |
{message.message}
402 | 403 | {messageSentByCurrentUser && <> 404 | 407 | 410 | } 411 |
412 | ); 413 | } 414 | 415 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 416 | return ( 417 |
418 | handleEnterPress(event, sendMessage))} 423 | /> 424 |
425 | 426 | 427 | { }} 434 | /> 435 |
436 |
437 | ); 438 | } 439 | 440 | const ChannelDetails = ({ 441 | currentlyUpdatingChannel, 442 | toggleChannelDetails, 443 | handleUpdateChannel, 444 | onChannelNamenIputChange 445 | }) => { 446 | if (currentlyUpdatingChannel) { 447 | return
448 |
449 |

Update Channel Details

450 |
Channel name
451 | 452 | 453 | 454 |
455 |
; 456 | } 457 | return null; 458 | } 459 | 460 | const ChannelCreate = ({ 461 | showChannelCreate, 462 | toggleShowCreateChannel, 463 | handleCreateChannel, 464 | onChannelNamenIputChange 465 | }) => { 466 | if (showChannelCreate) { 467 | return
468 |
469 |
470 |

Create Channel

471 |
472 |
Name
473 | handleEnterPress(event, handleCreateChannel)} /> 474 |
475 | 476 | 477 |
478 |
479 |
; 480 | } 481 | return null; 482 | } 483 | 484 | const CreateUserForm = ({ 485 | setupUser, 486 | settingUpUser, 487 | userNameInputValue, 488 | userIdInputValue, 489 | onUserNameInputChange, 490 | onUserIdInputChange 491 | }) => { 492 | if (settingUpUser) { 493 | return
494 |
handleEnterPress(event, setupUser)}> 495 |
User ID
496 | 497 | 501 | 502 |
User Nickname
503 | 507 | 508 |
509 | 515 |
516 |
517 |
518 | } else { 519 | return null; 520 | } 521 | } 522 | 523 | 524 | // Helpful functions that call Sendbird 525 | const loadChannels = async () => { 526 | try { 527 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 528 | const channels = await openChannelQuery.next(); 529 | return [channels, null]; 530 | 531 | } catch (error) { 532 | return [null, error]; 533 | } 534 | } 535 | 536 | const loadMessages = async (channel) => { 537 | try { 538 | 539 | //list all messages 540 | const messageListParams = {}; 541 | messageListParams.nextResultSize = 20; 542 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 543 | return [messages, null]; 544 | } catch (error) { 545 | return [null, error] 546 | } 547 | } 548 | 549 | const createChannel = async (channelName) => { 550 | try { 551 | const openChannelParams = {}; 552 | openChannelParams.name = channelName; 553 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 554 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 555 | return [openChannel, null]; 556 | } catch (error) { 557 | return [null, error]; 558 | } 559 | } 560 | 561 | const deleteChannel = async (channelUrl) => { 562 | try { 563 | const channel = await sb.openChannel.getChannel(channelUrl); 564 | await channel.delete(); 565 | return [channel, null]; 566 | } catch (error) { 567 | return [null, error]; 568 | } 569 | } 570 | 571 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 572 | try { 573 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 574 | const openChannelParams = {}; 575 | openChannelParams.name = channelNameInputValue; 576 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 577 | const updatedChannel = await channel.updateChannel(openChannelParams); 578 | return [updatedChannel, null]; 579 | } catch (error) { 580 | return [null, error]; 581 | } 582 | } 583 | 584 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 585 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 586 | } 587 | 588 | export default BasicOpenChannelSample; 589 | -------------------------------------------------------------------------------- /src/samples/OpenChannelCategorizeByCustomType.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelCategorizeByCustomType = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | currentCustomType: 'all', 25 | sortedChannelsByType: [], 26 | showChannelCreate: false, 27 | messageInputValue: "", 28 | userNameInputValue: "", 29 | userIdInputValue: "", 30 | channelNameInputValue: "", 31 | settingUpUser: true, 32 | file: null, 33 | messageToUpdate: null, 34 | loading: false, 35 | error: false 36 | }); 37 | 38 | //need to access state in message reeived callback 39 | const stateRef = useRef(); 40 | stateRef.current = state; 41 | 42 | const channelRef = useRef(); 43 | 44 | const scrollToBottom = (item, smooth) => { 45 | item?.scrollTo({ 46 | top: item.scrollHeight, 47 | behavior: smooth 48 | }) 49 | } 50 | 51 | useEffect(() => { 52 | scrollToBottom(channelRef.current) 53 | }, [state.currentlyJoinedChannel]) 54 | 55 | useEffect(() => { 56 | scrollToBottom(channelRef.current, 'smooth') 57 | }, [state.messages]) 58 | 59 | const onError = (error) => { 60 | updateState({ ...state, error: error.message }); 61 | console.log(error); 62 | } 63 | 64 | const handleJoinChannel = async (channelUrl) => { 65 | if (state.currentlyJoinedChannel?.url === channelUrl) { 66 | return null; 67 | } 68 | const { channels } = state; 69 | updateState({ ...state, loading: true }); 70 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 71 | await channelToJoin.enter(); 72 | const [messages, error] = await loadMessages(channelToJoin); 73 | if (error) { 74 | return onError(error); 75 | 76 | } 77 | 78 | // setup connection event handlers 79 | const connectionHandler = new ConnectionHandler(); 80 | 81 | connectionHandler.onReconnectSucceeded = async () => { 82 | const [messages, error] = await loadMessages(channelToJoin); 83 | 84 | updateState({ ...stateRef.current, messages: messages }); 85 | } 86 | 87 | sb.addConnectionHandler(uuid(), connectionHandler); 88 | 89 | //listen for incoming messages 90 | const channelHandler = new OpenChannelHandler(); 91 | channelHandler.onMessageUpdated = (channel, message) => { 92 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 93 | const updatedMessages = [...stateRef.current.messages]; 94 | updatedMessages[messageIndex] = message; 95 | updateState({ ...stateRef.current, messages: updatedMessages }); 96 | } 97 | 98 | channelHandler.onMessageReceived = (channel, message) => { 99 | const updatedMessages = [...stateRef.current.messages, message]; 100 | updateState({ ...stateRef.current, messages: updatedMessages }); 101 | }; 102 | 103 | channelHandler.onMessageDeleted = (channel, message) => { 104 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 105 | return messageObject.messageId !== message; 106 | }); 107 | updateState({ ...stateRef.current, messages: updatedMessages }); 108 | } 109 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 110 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 111 | } 112 | 113 | const handleLeaveChannel = async () => { 114 | const { currentlyJoinedChannel } = state; 115 | await currentlyJoinedChannel.exit(); 116 | 117 | updateState({ ...state, currentlyJoinedChannel: null }) 118 | 119 | } 120 | 121 | const handleCreateChannel = async () => { 122 | const { channelNameInputValue } = state; 123 | const [openChannel, error] = await createChannel(channelNameInputValue); 124 | if (error) { 125 | return onError(error); 126 | } 127 | const updatedChannels = [openChannel, ...state.channels]; 128 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 129 | } 130 | 131 | const handleDeleteChannel = async (channelUrl) => { 132 | const [channel, error] = await deleteChannel(channelUrl); 133 | if (error) { 134 | return onError(error); 135 | } 136 | const updatedChannels = state.channels.filter((channel) => { 137 | return channel.url !== channelUrl; 138 | }); 139 | updateState({ ...state, channels: updatedChannels }); 140 | } 141 | 142 | const handleChangeCustomType = (e) => { 143 | updateState({ ...state, currentCustomType: e.target.value }) 144 | } 145 | 146 | const handleUpdateChannel = async () => { 147 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 148 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 149 | if (error) { 150 | return onError(error); 151 | } 152 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 153 | const updatedChannels = [...channels]; 154 | updatedChannels[indexToReplace] = updatedChannel; 155 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 156 | } 157 | 158 | const toggleChannelDetails = (channel) => { 159 | if (channel) { 160 | updateState({ ...state, currentlyUpdatingChannel: channel }); 161 | } else { 162 | updateState({ ...state, currentlyUpdatingChannel: null }); 163 | } 164 | } 165 | 166 | const toggleShowCreateChannel = () => { 167 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 168 | } 169 | 170 | const onChannelNamenIputChange = (e) => { 171 | const channelNameInputValue = e.currentTarget.value; 172 | updateState({ ...state, channelNameInputValue }); 173 | } 174 | 175 | const onUserNameInputChange = (e) => { 176 | const userNameInputValue = e.currentTarget.value; 177 | updateState({ ...state, userNameInputValue }); 178 | } 179 | 180 | const onUserIdInputChange = (e) => { 181 | const userIdInputValue = e.currentTarget.value; 182 | updateState({ ...state, userIdInputValue }); 183 | } 184 | 185 | const onMessageInputChange = (e) => { 186 | const messageInputValue = e.currentTarget.value; 187 | updateState({ ...state, messageInputValue }); 188 | } 189 | 190 | const sendMessage = async () => { 191 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 192 | 193 | if (messageToUpdate) { 194 | const userMessageUpdateParams = {}; 195 | userMessageUpdateParams.message = state.messageInputValue; 196 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 197 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 198 | messages[messageIndex] = updatedMessage; 199 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 200 | } else { 201 | const userMessageParams = {}; 202 | userMessageParams.message = state.messageInputValue; 203 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 204 | const updatedMessages = [...messages, message]; 205 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 206 | }).onFailed((error) => { 207 | console.log(error) 208 | console.log("failed") 209 | }); 210 | } 211 | } 212 | 213 | const onFileInputChange = async (e) => { 214 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 215 | const { currentlyJoinedChannel, messages } = state; 216 | const fileMessageParams = {}; 217 | fileMessageParams.file = e.currentTarget.files[0]; 218 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 219 | const updatedMessages = [...messages, message]; 220 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 221 | 222 | }).onFailed((error) => { 223 | console.log(error) 224 | console.log("failed") 225 | }); 226 | } 227 | } 228 | 229 | const handleDeleteMessage = async (messageToDelete) => { 230 | const { currentlyJoinedChannel } = state; 231 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 232 | } 233 | 234 | const updateMessage = async (message) => { 235 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 236 | } 237 | 238 | const setupUser = async () => { 239 | const { userNameInputValue, userIdInputValue } = state; 240 | const sendbirdChat = await SendbirdChat.init({ 241 | appId: SENDBIRD_INFO.appId, 242 | localCacheEnabled: true, 243 | modules: [new OpenChannelModule()] 244 | }); 245 | 246 | try { 247 | await sendbirdChat.connect(userIdInputValue); 248 | } catch (e) { 249 | console.log("error", e) 250 | } 251 | await sendbirdChat.setChannelInvitationPreference(true); 252 | 253 | const userUpdateParams = {}; 254 | userUpdateParams.nickname = userNameInputValue; 255 | userUpdateParams.userId = userIdInputValue; 256 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 257 | 258 | sb = sendbirdChat; 259 | updateState({ ...state, loading: true }); 260 | const [channels, error] = await loadChannels(); 261 | 262 | if (error) { 263 | return onError(error); 264 | } 265 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 266 | } 267 | 268 | if (state.loading) { 269 | return
Loading...
270 | } 271 | 272 | if (state.error) { 273 | return
{state.error} check console for more information.
274 | } 275 | 276 | console.log('- - - - State object very useful for debugging - - - -'); 277 | console.log(state); 278 | 279 | return ( 280 | <> 281 | 288 | 296 | 301 | 306 | 311 | 316 | 323 | 324 | 325 | ); 326 | }; 327 | 328 | // Chat UI Components 329 | const ChannelList = ({ channels, currentCustomType, handleChangeCustomType, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 330 | return ( 331 |
332 |
333 |

Open Channels

334 | 335 |
336 |
337 |

Custom type:

338 | 348 |
349 | {channels 350 | .filter(channel => currentCustomType === 'all' ? channel : channel.customType === currentCustomType) 351 | .map(channel => { 352 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 353 | return ( 354 |
355 |
{ handleJoinChannel(channel.url) }}> 357 | {channel.name} 358 |
359 | {userIsOperator && 360 |
361 | 364 | 367 |
} 368 |
369 | ); 370 | }) 371 | } 372 |
373 | ); 374 | } 375 | 376 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 377 | if (currentlyJoinedChannel) { 378 | return
379 | {currentlyJoinedChannel.name} 380 |
381 | 382 |
383 |
{children}
384 |
; 385 | } 386 | return
; 387 | } 388 | 389 | const ChannelHeader = ({ children }) => { 390 | return
{children}
; 391 | } 392 | 393 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 394 | return messages.map(message => { 395 | return ( 396 |
397 | 402 |
403 | ); 404 | }) 405 | } 406 | 407 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 408 | if (!message.sender) return null; if (message.url) { 409 | return ( 410 |
411 |
{timestampToTime(message.createdAt)}
412 |
{message.sender.nickname}{' '}
413 | 414 |
415 | ); 416 | } 417 | 418 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 419 | return ( 420 |
421 |
{timestampToTime(message.createdAt)}
422 | 423 |
{message.sender.nickname}{':'}
424 |
{message.message}
425 | 426 | {messageSentByCurrentUser && <> 427 | 430 | 433 | } 434 |
435 | ); 436 | } 437 | 438 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 439 | return ( 440 |
441 | handleEnterPress(event, sendMessage))} 446 | /> 447 |
448 | 449 | 450 | { }} 457 | /> 458 |
459 |
460 | ); 461 | } 462 | 463 | const ChannelDetails = ({ 464 | currentlyUpdatingChannel, 465 | toggleChannelDetails, 466 | handleUpdateChannel, 467 | onChannelNamenIputChange 468 | }) => { 469 | if (currentlyUpdatingChannel) { 470 | return
471 |
472 |

Update Channel Details

473 |
Channel name
474 | 475 | 476 | 477 |
478 |
; 479 | } 480 | return null; 481 | } 482 | 483 | const ChannelCreate = ({ 484 | showChannelCreate, 485 | toggleShowCreateChannel, 486 | handleCreateChannel, 487 | onChannelNamenIputChange 488 | }) => { 489 | if (showChannelCreate) { 490 | return
491 |
492 |
493 |

Create Channel

494 |
495 |
Name
496 | handleEnterPress(event, handleCreateChannel)} /> 497 |
498 | 499 | 500 |
501 |
502 |
; 503 | } 504 | return null; 505 | } 506 | 507 | const CreateUserForm = ({ 508 | setupUser, 509 | settingUpUser, 510 | userNameInputValue, 511 | userIdInputValue, 512 | onUserNameInputChange, 513 | onUserIdInputChange 514 | }) => { 515 | if (settingUpUser) { 516 | return
517 |
handleEnterPress(event, setupUser)}> 518 |
User ID
519 | 524 |
User Nickname
525 | 530 |
531 | 535 |
536 |
537 |
538 | } else { 539 | return null; 540 | } 541 | } 542 | 543 | // Helpful functions that call Sendbird 544 | const loadChannels = async () => { 545 | try { 546 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 547 | const channels = await openChannelQuery.next(); 548 | return [channels, null]; 549 | } catch (error) { 550 | return [null, error]; 551 | } 552 | } 553 | 554 | const loadMessages = async (channel) => { 555 | try { 556 | 557 | //list all messages 558 | const messageListParams = {}; 559 | messageListParams.nextResultSize = 20; 560 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 561 | return [messages, null]; 562 | } catch (error) { 563 | return [null, error] 564 | } 565 | } 566 | 567 | 568 | const createChannel = async (channelName) => { 569 | try { 570 | const openChannelParams = {}; 571 | openChannelParams.name = channelName; 572 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 573 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 574 | return [openChannel, null]; 575 | } catch (error) { 576 | return [null, error]; 577 | } 578 | } 579 | 580 | const deleteChannel = async (channelUrl) => { 581 | try { 582 | const channel = await sb.openChannel.getChannel(channelUrl); 583 | await channel.delete(); 584 | return [channel, null]; 585 | } catch (error) { 586 | return [null, error]; 587 | } 588 | } 589 | 590 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 591 | try { 592 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 593 | const openChannelParams = {}; 594 | openChannelParams.name = channelNameInputValue; 595 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 596 | const updatedChannel = await channel.updateChannel(openChannelParams); 597 | return [updatedChannel, null]; 598 | } catch (error) { 599 | return [null, error]; 600 | } 601 | } 602 | 603 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 604 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 605 | } 606 | 607 | export default OpenChannelCategorizeByCustomType; 608 | -------------------------------------------------------------------------------- /src/samples/OpenChannelDisplayOGTags.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelDisplayOGTags = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false 34 | }); 35 | 36 | //need to access state in message reeived callback 37 | const stateRef = useRef(); 38 | stateRef.current = state; 39 | 40 | const channelRef = useRef(); 41 | 42 | const scrollToBottom = (item, smooth) => { 43 | item?.scrollTo({ 44 | top: item.scrollHeight, 45 | behavior: smooth 46 | }) 47 | } 48 | 49 | useEffect(() => { 50 | scrollToBottom(channelRef.current) 51 | }, [state.currentlyJoinedChannel]) 52 | 53 | useEffect(() => { 54 | scrollToBottom(channelRef.current, 'smooth') 55 | }, [state.messages]) 56 | 57 | const onError = (error) => { 58 | updateState({ ...state, error: error.message }); 59 | console.log(error); 60 | } 61 | 62 | const handleJoinChannel = async (channelUrl) => { 63 | if (state.currentlyJoinedChannel?.url === channelUrl) { 64 | return null; 65 | } 66 | const { channels } = state; 67 | updateState({ ...state, loading: true }); 68 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 69 | await channelToJoin.enter(); 70 | const [messages, error] = await loadMessages(channelToJoin); 71 | if (error) { 72 | return onError(error); 73 | } 74 | 75 | // setup connection event handlers 76 | const connectionHandler = new ConnectionHandler(); 77 | 78 | connectionHandler.onReconnectSucceeded = async () => { 79 | const [messages, error] = await loadMessages(channelToJoin); 80 | 81 | updateState({ ...stateRef.current, messages: messages }); 82 | } 83 | 84 | sb.addConnectionHandler(uuid(), connectionHandler); 85 | 86 | 87 | //listen for incoming messages 88 | const channelHandler = new OpenChannelHandler(); 89 | channelHandler.onMessageUpdated = (channel, message) => { 90 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 91 | const updatedMessages = [...stateRef.current.messages]; 92 | updatedMessages[messageIndex] = message; 93 | updateState({ ...stateRef.current, messages: updatedMessages }); 94 | } 95 | 96 | channelHandler.onMessageReceived = (channel, message) => { 97 | const updatedMessages = [...stateRef.current.messages, message]; 98 | updateState({ ...stateRef.current, messages: updatedMessages }); 99 | }; 100 | 101 | channelHandler.onMessageDeleted = (channel, message) => { 102 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 103 | return messageObject.messageId !== message; 104 | }); 105 | updateState({ ...stateRef.current, messages: updatedMessages }); 106 | } 107 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 108 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 109 | } 110 | 111 | const handleLeaveChannel = async () => { 112 | const { currentlyJoinedChannel } = state; 113 | await currentlyJoinedChannel.exit(); 114 | 115 | updateState({ ...state, currentlyJoinedChannel: null }) 116 | } 117 | 118 | const handleCreateChannel = async () => { 119 | const { channelNameInputValue } = state; 120 | const [openChannel, error] = await createChannel(channelNameInputValue); 121 | if (error) { 122 | return onError(error); 123 | } 124 | const updatedChannels = [openChannel, ...state.channels]; 125 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 126 | } 127 | 128 | const handleDeleteChannel = async (channelUrl) => { 129 | const [channel, error] = await deleteChannel(channelUrl); 130 | if (error) { 131 | return onError(error); 132 | } 133 | const updatedChannels = state.channels.filter((channel) => { 134 | return channel.url !== channelUrl; 135 | }); 136 | updateState({ ...state, channels: updatedChannels }); 137 | } 138 | 139 | const handleUpdateChannel = async () => { 140 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 141 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 142 | if (error) { 143 | return onError(error); 144 | } 145 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 146 | const updatedChannels = [...channels]; 147 | updatedChannels[indexToReplace] = updatedChannel; 148 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 149 | } 150 | 151 | const toggleChannelDetails = (channel) => { 152 | if (channel) { 153 | updateState({ ...state, currentlyUpdatingChannel: channel }); 154 | } else { 155 | updateState({ ...state, currentlyUpdatingChannel: null }); 156 | } 157 | } 158 | 159 | const toggleShowCreateChannel = () => { 160 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 161 | } 162 | 163 | const onChannelNamenIputChange = (e) => { 164 | const channelNameInputValue = e.currentTarget.value; 165 | updateState({ ...state, channelNameInputValue }); 166 | } 167 | 168 | const onUserNameInputChange = (e) => { 169 | const userNameInputValue = e.currentTarget.value; 170 | updateState({ ...state, userNameInputValue }); 171 | } 172 | 173 | const onUserIdInputChange = (e) => { 174 | const userIdInputValue = e.currentTarget.value; 175 | updateState({ ...state, userIdInputValue }); 176 | } 177 | 178 | const onMessageInputChange = (e) => { 179 | const messageInputValue = e.currentTarget.value; 180 | updateState({ ...state, messageInputValue }); 181 | } 182 | 183 | const sendMessage = async () => { 184 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 185 | 186 | if (messageToUpdate) { 187 | const userMessageUpdateParams = {}; 188 | userMessageUpdateParams.message = state.messageInputValue; 189 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 190 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 191 | messages[messageIndex] = updatedMessage; 192 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 193 | } else { 194 | const userMessageParams = {}; 195 | userMessageParams.message = state.messageInputValue; 196 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 197 | const updatedMessages = [...messages, message]; 198 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 199 | }).onFailed((error) => { 200 | console.log(error) 201 | console.log("failed") 202 | }); 203 | } 204 | } 205 | 206 | const onFileInputChange = async (e) => { 207 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 208 | const { currentlyJoinedChannel, messages } = state; 209 | const fileMessageParams = {}; 210 | fileMessageParams.file = e.currentTarget.files[0]; 211 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 212 | const updatedMessages = [...messages, message]; 213 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 214 | }).onFailed((error) => { 215 | console.log(error) 216 | console.log("failed") 217 | }); 218 | } 219 | } 220 | 221 | const handleDeleteMessage = async (messageToDelete) => { 222 | const { currentlyJoinedChannel } = state; 223 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 224 | } 225 | 226 | const updateMessage = async (message) => { 227 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 228 | } 229 | 230 | const setupUser = async () => { 231 | const { userNameInputValue, userIdInputValue } = state; 232 | const sendbirdChat = await SendbirdChat.init({ 233 | appId: SENDBIRD_INFO.appId, 234 | localCacheEnabled: true, 235 | modules: [new OpenChannelModule()] 236 | }); 237 | 238 | try { 239 | await sendbirdChat.connect(userIdInputValue); 240 | } catch (e) { 241 | console.log("error", e) 242 | } 243 | await sendbirdChat.setChannelInvitationPreference(true); 244 | 245 | const userUpdateParams = {}; 246 | userUpdateParams.nickname = userNameInputValue; 247 | userUpdateParams.userId = userIdInputValue; 248 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 249 | 250 | sb = sendbirdChat; 251 | updateState({ ...state, loading: true }); 252 | const [channels, error] = await loadChannels(); 253 | if (error) { 254 | return onError(error); 255 | } 256 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 257 | } 258 | 259 | if (state.loading) { 260 | return
Loading...
261 | } 262 | 263 | if (state.error) { 264 | return
{state.error} check console for more information.
265 | } 266 | 267 | console.log('- - - - State object very useful for debugging - - - -'); 268 | console.log(state); 269 | 270 | return ( 271 | <> 272 | 279 | 285 | 290 | 295 | 300 | 305 | 312 | 313 | 314 | ); 315 | }; 316 | 317 | // Chat UI Components 318 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 319 | return ( 320 |
321 |
322 |

Open Channels

323 | 324 |
325 | { 326 | channels.map(channel => { 327 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 328 | return ( 329 |
330 |
{ handleJoinChannel(channel.url) }}> 332 | {channel.name} 333 |
334 | {userIsOperator && 335 |
336 | 339 | 342 |
343 | } 344 |
345 | ); 346 | }) 347 | } 348 |
349 | ); 350 | } 351 | 352 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 353 | if (currentlyJoinedChannel) { 354 | return
355 | {currentlyJoinedChannel.name} 356 |
357 | 358 |
359 |
{children}
360 |
; 361 | } 362 | return
; 363 | } 364 | 365 | const ChannelHeader = ({ children }) => { 366 | return
{children}
; 367 | } 368 | 369 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 370 | return messages.map(message => { 371 | return ( 372 |
373 | 378 |
379 | ); 380 | }) 381 | } 382 | 383 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 384 | if (!message.sender) return null; if (message.url) { 385 | return ( 386 |
387 |
{timestampToTime(message.createdAt)}
388 |
{message.sender.nickname}{' '}
389 | 390 |
391 | ); 392 | } 393 | 394 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 395 | return ( 396 | <> 397 |
398 |
{timestampToTime(message.createdAt)}
399 |
{message.sender.nickname}{':'}
400 |
{message.message}
401 | 402 | {messageSentByCurrentUser && <> 403 | 406 | 409 | } 410 |
411 | {message.ogMetaData &&
412 | {message.ogMetaData.url} 413 |

{message.ogMetaData.title}

414 |

{message.ogMetaData.description}

415 | 416 |
} 417 | 418 | ); 419 | } 420 | 421 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 422 | return ( 423 |
424 | handleEnterPress(event, sendMessage))} 429 | /> 430 |
431 | 432 | 433 | { }} 440 | /> 441 |
442 |
443 | ); 444 | } 445 | 446 | const ChannelDetails = ({ 447 | currentlyUpdatingChannel, 448 | toggleChannelDetails, 449 | handleUpdateChannel, 450 | onChannelNamenIputChange 451 | }) => { 452 | if (currentlyUpdatingChannel) { 453 | return
454 |
455 |

Update Channel Details

456 |
Channel name
457 | 458 | 459 | 460 |
461 |
; 462 | } 463 | return null; 464 | } 465 | 466 | const ChannelCreate = ({ 467 | showChannelCreate, 468 | toggleShowCreateChannel, 469 | handleCreateChannel, 470 | onChannelNamenIputChange 471 | }) => { 472 | if (showChannelCreate) { 473 | return
474 |
475 |
476 |

Create Channel

477 |
478 |
Name
479 | handleEnterPress(event, handleCreateChannel)} /> 480 |
481 | 482 | 483 |
484 |
485 |
; 486 | } 487 | return null; 488 | } 489 | 490 | const CreateUserForm = ({ 491 | setupUser, 492 | settingUpUser, 493 | userNameInputValue, 494 | userIdInputValue, 495 | onUserNameInputChange, 496 | onUserIdInputChange 497 | }) => { 498 | if (settingUpUser) { 499 | return
500 |
handleEnterPress(event, setupUser)}> 501 |
User ID
502 | 507 |
User Nickname
508 | 513 |
514 | 518 |
519 |
520 |
521 | } else { 522 | return null; 523 | } 524 | } 525 | 526 | // Helpful functions that call Sendbird 527 | const loadChannels = async () => { 528 | try { 529 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 530 | const channels = await openChannelQuery.next(); 531 | return [channels, null]; 532 | } catch (error) { 533 | return [null, error]; 534 | } 535 | } 536 | 537 | const loadMessages = async (channel) => { 538 | try { 539 | 540 | //list all messages 541 | const messageListParams = {}; 542 | messageListParams.nextResultSize = 20; 543 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 544 | return [messages, null]; 545 | } catch (error) { 546 | return [null, error] 547 | } 548 | } 549 | 550 | const createChannel = async (channelName) => { 551 | try { 552 | const openChannelParams = {}; 553 | openChannelParams.name = channelName; 554 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 555 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 556 | return [openChannel, null]; 557 | } catch (error) { 558 | return [null, error]; 559 | } 560 | } 561 | 562 | const deleteChannel = async (channelUrl) => { 563 | try { 564 | const channel = await sb.openChannel.getChannel(channelUrl); 565 | await channel.delete(); 566 | return [channel, null]; 567 | } catch (error) { 568 | return [null, error]; 569 | } 570 | } 571 | 572 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 573 | try { 574 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 575 | const openChannelParams = {}; 576 | openChannelParams.name = channelNameInputValue; 577 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 578 | const updatedChannel = await channel.updateChannel(openChannelParams); 579 | return [updatedChannel, null]; 580 | } catch (error) { 581 | return [null, error]; 582 | } 583 | } 584 | 585 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 586 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 587 | } 588 | 589 | export default OpenChannelDisplayOGTags; 590 | -------------------------------------------------------------------------------- /src/samples/OpenChannelFreeze.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelFreeze = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false 34 | }); 35 | 36 | //need to access state in message reeived callback 37 | const stateRef = useRef(); 38 | stateRef.current = state; 39 | 40 | const channelRef = useRef(); 41 | 42 | const scrollToBottom = (item, smooth) => { 43 | item?.scrollTo({ 44 | top: item.scrollHeight, 45 | behavior: smooth 46 | }) 47 | } 48 | 49 | useEffect(() => { 50 | scrollToBottom(channelRef.current) 51 | }, [state.currentlyJoinedChannel]) 52 | 53 | useEffect(() => { 54 | scrollToBottom(channelRef.current, 'smooth') 55 | }, [state.messages]) 56 | 57 | const onError = (error) => { 58 | updateState({ ...state, error: error.message }); 59 | console.log(error); 60 | } 61 | 62 | const handleJoinChannel = async (channelUrl) => { 63 | if (state.currentlyJoinedChannel?.url === channelUrl) { 64 | return null; 65 | } 66 | const { channels } = state; 67 | updateState({ ...state, loading: true }); 68 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 69 | await channelToJoin.enter(); 70 | const [messages, error] = await loadMessages(channelToJoin); 71 | if (error) { 72 | return onError(error); 73 | } 74 | 75 | // setup connection event handlers 76 | const connectionHandler = new ConnectionHandler(); 77 | 78 | connectionHandler.onReconnectSucceeded = async () => { 79 | const [messages, error] = await loadMessages(channelToJoin); 80 | 81 | updateState({ ...stateRef.current, messages: messages }); 82 | } 83 | 84 | sb.addConnectionHandler(uuid(), connectionHandler); 85 | 86 | 87 | //listen for incoming messages 88 | const channelHandler = new OpenChannelHandler(); 89 | channelHandler.onMessageUpdated = (channel, message) => { 90 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 91 | const updatedMessages = [...stateRef.current.messages]; 92 | updatedMessages[messageIndex] = message; 93 | updateState({ ...stateRef.current, messages: updatedMessages }); 94 | } 95 | 96 | channelHandler.onMessageReceived = (channel, message) => { 97 | const updatedMessages = [...stateRef.current.messages, message]; 98 | updateState({ ...stateRef.current, messages: updatedMessages }); 99 | }; 100 | 101 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 102 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 103 | } 104 | 105 | const handleLeaveChannel = async () => { 106 | const { currentlyJoinedChannel } = state; 107 | await currentlyJoinedChannel.exit(); 108 | 109 | updateState({ ...state, currentlyJoinedChannel: null }) 110 | } 111 | 112 | const handleCreateChannel = async () => { 113 | const { channelNameInputValue } = state; 114 | const [openChannel, error] = await createChannel(channelNameInputValue); 115 | if (error) { 116 | return onError(error); 117 | } 118 | const updatedChannels = [openChannel, ...state.channels]; 119 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 120 | } 121 | 122 | const handleDeleteChannel = async (channelUrl) => { 123 | const [channel, error] = await deleteChannel(channelUrl); 124 | if (error) { 125 | return onError(error); 126 | } 127 | const updatedChannels = state.channels.filter((channel) => { 128 | return channel.url !== channelUrl; 129 | }); 130 | updateState({ ...state, channels: updatedChannels }); 131 | } 132 | 133 | const handleUpdateChannel = async () => { 134 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 135 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 136 | if (error) { 137 | return onError(error); 138 | } 139 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 140 | const updatedChannels = [...channels]; 141 | updatedChannels[indexToReplace] = updatedChannel; 142 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 143 | } 144 | 145 | const toggleChannelDetails = (channel) => { 146 | if (channel) { 147 | updateState({ ...state, currentlyUpdatingChannel: channel }); 148 | } else { 149 | updateState({ ...state, currentlyUpdatingChannel: null }); 150 | } 151 | } 152 | 153 | const toggleShowCreateChannel = () => { 154 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 155 | } 156 | 157 | const onChannelNamenIputChange = (e) => { 158 | const channelNameInputValue = e.currentTarget.value; 159 | updateState({ ...state, channelNameInputValue }); 160 | } 161 | 162 | const onUserNameInputChange = (e) => { 163 | const userNameInputValue = e.currentTarget.value; 164 | updateState({ ...state, userNameInputValue }); 165 | } 166 | 167 | const onUserIdInputChange = (e) => { 168 | const userIdInputValue = e.currentTarget.value; 169 | updateState({ ...state, userIdInputValue }); 170 | } 171 | 172 | const onMessageInputChange = (e) => { 173 | const messageInputValue = e.currentTarget.value; 174 | updateState({ ...state, messageInputValue }); 175 | } 176 | 177 | const sendMessage = async () => { 178 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 179 | 180 | if (messageToUpdate) { 181 | const userMessageUpdateParams = {}; 182 | userMessageUpdateParams.message = state.messageInputValue; 183 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 184 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 185 | messages[messageIndex] = updatedMessage; 186 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 187 | } else { 188 | const userMessageParams = {}; 189 | userMessageParams.message = state.messageInputValue; 190 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 191 | const updatedMessages = [...messages, message]; 192 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 193 | 194 | }).onFailed((error) => { 195 | console.log(error) 196 | console.log("failed") 197 | }); 198 | } 199 | } 200 | 201 | const onFileInputChange = async (e) => { 202 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 203 | const { currentlyJoinedChannel, messages } = state; 204 | const fileMessageParams = {}; 205 | fileMessageParams.file = e.currentTarget.files[0]; 206 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 207 | const updatedMessages = [...messages, message]; 208 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 209 | }).onFailed((error) => { 210 | console.log(error) 211 | console.log("failed") 212 | }); 213 | } 214 | } 215 | 216 | const handleDeleteMessage = async (messageToDelete) => { 217 | const { currentlyJoinedChannel, messages } = state; 218 | await deleteMessage(currentlyJoinedChannel, messageToDelete); 219 | const updatedMessages = messages.filter((message) => { 220 | return message.messageId !== messageToDelete.messageId; 221 | }); 222 | updateState({ ...state, messages: updatedMessages }); 223 | } 224 | 225 | const updateMessage = async (message) => { 226 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 227 | } 228 | 229 | const setupUser = async () => { 230 | const { userNameInputValue, userIdInputValue } = state; 231 | const sendbirdChat = await SendbirdChat.init({ 232 | appId: SENDBIRD_INFO.appId, 233 | localCacheEnabled: true, 234 | modules: [new OpenChannelModule()] 235 | }); 236 | 237 | try { 238 | await sendbirdChat.connect(userIdInputValue); 239 | } catch (e) { 240 | console.log("error", e) 241 | } 242 | await sendbirdChat.setChannelInvitationPreference(true); 243 | 244 | const userUpdateParams = {}; 245 | userUpdateParams.nickname = userNameInputValue; 246 | userUpdateParams.userId = userIdInputValue; 247 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 248 | 249 | sb = sendbirdChat; 250 | updateState({ ...state, loading: true }); 251 | const [channels, error] = await loadChannels(); 252 | if (error) { 253 | return onError(error); 254 | } 255 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 256 | } 257 | 258 | if (state.loading) { 259 | return
Loading...
260 | } 261 | 262 | if (state.error) { 263 | return
{state.error} check console for more information.
264 | } 265 | 266 | console.log('- - - - State object very useful for debugging - - - -'); 267 | console.log(state); 268 | 269 | return ( 270 | <> 271 | 278 | 284 | 289 | 294 | 299 | {state.currentlyJoinedChannel?.isFrozen && DisplayFreezeMessage()} 300 | 305 | 312 | 313 | 314 | ); 315 | }; 316 | 317 | // Chat UI Components 318 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 319 | return ( 320 |
321 |
322 |

Open Channels

323 | 324 |
325 | { 326 | channels.map(channel => { 327 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 328 | return ( 329 |
330 |
{ handleJoinChannel(channel.url) }}> 332 | {channel.name} 333 | {channel.isFrozen && } 334 |
335 | {userIsOperator && 336 |
337 | 340 | 343 |
344 | } 345 |
346 | ); 347 | }) 348 | } 349 |
350 | ); 351 | } 352 | 353 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 354 | if (currentlyJoinedChannel) { 355 | return
356 | {currentlyJoinedChannel.name} 357 |
358 | 359 |
360 |
{children}
361 |
; 362 | } 363 | return
; 364 | } 365 | 366 | const ChannelHeader = ({ children }) => { 367 | return
{children}
; 368 | } 369 | 370 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 371 | return messages.map(message => { 372 | return ( 373 |
374 | 379 |
380 | ); 381 | }) 382 | } 383 | 384 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 385 | if (!message.sender) return null; if (message.url) { 386 | return ( 387 |
388 |
{timestampToTime(message.createdAt)}
389 |
{message.sender.nickname}{' '}
390 | 391 |
); 392 | } 393 | 394 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 395 | return ( 396 |
397 |
{timestampToTime(message.createdAt)}
398 |
{message.sender.nickname}{':'}
399 |
{message.message}
400 | 401 | {messageSentByCurrentUser && <> 402 | 405 | 408 | } 409 |
410 | ); 411 | } 412 | 413 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 414 | return ( 415 |
416 | handleEnterPress(event, sendMessage))} 421 | /> 422 |
423 | 424 | 425 | { }} 432 | /> 433 |
434 |
435 | ); 436 | } 437 | 438 | const ChannelDetails = ({ 439 | currentlyUpdatingChannel, 440 | toggleChannelDetails, 441 | handleUpdateChannel, 442 | onChannelNamenIputChange 443 | }) => { 444 | if (currentlyUpdatingChannel) { 445 | return
446 |
447 |

Update Channel Details

448 |
Channel name
449 | 450 | 451 | 452 |
453 |
; 454 | } 455 | return null; 456 | } 457 | 458 | const ChannelCreate = ({ 459 | showChannelCreate, 460 | toggleShowCreateChannel, 461 | handleCreateChannel, 462 | onChannelNamenIputChange 463 | }) => { 464 | if (showChannelCreate) { 465 | return
466 |
467 |
468 |

Create Channel

469 |
470 |
Name
471 | handleEnterPress(event, handleCreateChannel)} /> 472 |
473 | 474 | 475 |
476 |
477 |
; 478 | } 479 | return null; 480 | } 481 | 482 | const CreateUserForm = ({ 483 | setupUser, 484 | settingUpUser, 485 | userNameInputValue, 486 | userIdInputValue, 487 | onUserNameInputChange, 488 | onUserIdInputChange 489 | }) => { 490 | if (settingUpUser) { 491 | return
492 |
handleEnterPress(event, setupUser)}> 493 |
User ID
494 | 499 |
User Nickname
500 | 505 |
506 | 510 |
511 |
512 |
513 | } else { 514 | return null; 515 | } 516 | } 517 | 518 | const DisplayFreezeMessage = () => ( 519 |
Channel Frozen
520 | ) 521 | 522 | // Helpful functions that call Sendbird 523 | const loadChannels = async () => { 524 | try { 525 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 526 | const channels = await openChannelQuery.next(); 527 | return [channels, null]; 528 | } catch (error) { 529 | return [null, error]; 530 | } 531 | } 532 | 533 | const loadMessages = async (channel) => { 534 | try { 535 | 536 | //list all messages 537 | const messageListParams = {}; 538 | messageListParams.nextResultSize = 20; 539 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 540 | return [messages, null]; 541 | } catch (error) { 542 | return [null, error] 543 | } 544 | } 545 | 546 | const createChannel = async (channelName) => { 547 | try { 548 | const openChannelParams = {}; 549 | openChannelParams.name = channelName; 550 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 551 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 552 | return [openChannel, null]; 553 | } catch (error) { 554 | return [null, error]; 555 | } 556 | } 557 | 558 | const deleteChannel = async (channelUrl) => { 559 | try { 560 | const channel = await sb.openChannel.getChannel(channelUrl); 561 | await channel.delete(); 562 | return [channel, null]; 563 | } catch (error) { 564 | return [null, error]; 565 | } 566 | } 567 | 568 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 569 | try { 570 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 571 | const openChannelParams = {}; 572 | openChannelParams.name = channelNameInputValue; 573 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 574 | const updatedChannel = await channel.updateChannel(openChannelParams); 575 | return [updatedChannel, null]; 576 | } catch (error) { 577 | return [null, error]; 578 | } 579 | } 580 | 581 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 582 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 583 | } 584 | 585 | export default OpenChannelFreeze; 586 | -------------------------------------------------------------------------------- /src/samples/OpenChannelSendAnAdminMessage.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelSendAnAdminMessage = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false 34 | }); 35 | 36 | //need to access state in message reeived callback 37 | const stateRef = useRef(); 38 | stateRef.current = state; 39 | 40 | const channelRef = useRef(); 41 | 42 | const scrollToBottom = (item, smooth) => { 43 | item?.scrollTo({ 44 | top: item.scrollHeight, 45 | behavior: smooth 46 | }) 47 | } 48 | 49 | useEffect(() => { 50 | scrollToBottom(channelRef.current) 51 | }, [state.currentlyJoinedChannel]) 52 | 53 | useEffect(() => { 54 | scrollToBottom(channelRef.current, 'smooth') 55 | }, [state.messages]) 56 | 57 | const onError = (error) => { 58 | updateState({ ...state, error: error.message }); 59 | console.log(error); 60 | } 61 | 62 | const handleJoinChannel = async (channelUrl) => { 63 | if (state.currentlyJoinedChannel?.url === channelUrl) { 64 | return null; 65 | } 66 | const { channels } = state; 67 | updateState({ ...state, loading: true }); 68 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 69 | await channelToJoin.enter(); 70 | const [messages, error] = await loadMessages(channelToJoin); 71 | if (error) { 72 | return onError(error); 73 | } 74 | 75 | // setup connection event handlers 76 | const connectionHandler = new ConnectionHandler(); 77 | 78 | connectionHandler.onReconnectSucceeded = async () => { 79 | const [messages, error] = await loadMessages(channelToJoin); 80 | 81 | updateState({ ...stateRef.current, messages: messages }); 82 | } 83 | 84 | sb.addConnectionHandler(uuid(), connectionHandler); 85 | 86 | //listen for incoming messages 87 | const channelHandler = new OpenChannelHandler(); 88 | channelHandler.onMessageUpdated = (channel, message) => { 89 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 90 | const updatedMessages = [...stateRef.current.messages]; 91 | updatedMessages[messageIndex] = message; 92 | updateState({ ...stateRef.current, messages: updatedMessages }); 93 | } 94 | 95 | channelHandler.onMessageReceived = (channel, message) => { 96 | const updatedMessages = [...stateRef.current.messages, message]; 97 | updateState({ ...stateRef.current, messages: updatedMessages }); 98 | }; 99 | 100 | channelHandler.onMessageDeleted = (channel, message) => { 101 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 102 | return messageObject.messageId !== message; 103 | }); 104 | updateState({ ...stateRef.current, messages: updatedMessages }); 105 | } 106 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 107 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 108 | } 109 | 110 | const handleLeaveChannel = async () => { 111 | const { currentlyJoinedChannel } = state; 112 | await currentlyJoinedChannel.exit(); 113 | 114 | updateState({ ...state, currentlyJoinedChannel: null }) 115 | } 116 | 117 | const handleCreateChannel = async () => { 118 | const { channelNameInputValue } = state; 119 | const [openChannel, error] = await createChannel(channelNameInputValue); 120 | if (error) { 121 | return onError(error); 122 | } 123 | const updatedChannels = [openChannel, ...state.channels]; 124 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 125 | } 126 | 127 | const handleDeleteChannel = async (channelUrl) => { 128 | const [channel, error] = await deleteChannel(channelUrl); 129 | if (error) { 130 | return onError(error); 131 | } 132 | const updatedChannels = state.channels.filter((channel) => { 133 | return channel.url !== channelUrl; 134 | }); 135 | updateState({ ...state, channels: updatedChannels }); 136 | } 137 | 138 | const handleUpdateChannel = async () => { 139 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 140 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 141 | if (error) { 142 | return onError(error); 143 | } 144 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 145 | const updatedChannels = [...channels]; 146 | updatedChannels[indexToReplace] = updatedChannel; 147 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 148 | } 149 | 150 | const toggleChannelDetails = (channel) => { 151 | if (channel) { 152 | updateState({ ...state, currentlyUpdatingChannel: channel }); 153 | } else { 154 | updateState({ ...state, currentlyUpdatingChannel: null }); 155 | } 156 | } 157 | 158 | const toggleShowCreateChannel = () => { 159 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 160 | } 161 | 162 | const onChannelNamenIputChange = (e) => { 163 | const channelNameInputValue = e.currentTarget.value; 164 | updateState({ ...state, channelNameInputValue }); 165 | } 166 | 167 | const onUserNameInputChange = (e) => { 168 | const userNameInputValue = e.currentTarget.value; 169 | updateState({ ...state, userNameInputValue }); 170 | } 171 | 172 | const onUserIdInputChange = (e) => { 173 | const userIdInputValue = e.currentTarget.value; 174 | updateState({ ...state, userIdInputValue }); 175 | } 176 | 177 | const onMessageInputChange = (e) => { 178 | const messageInputValue = e.currentTarget.value; 179 | updateState({ ...state, messageInputValue }); 180 | } 181 | 182 | const sendMessage = async () => { 183 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 184 | 185 | if (messageToUpdate) { 186 | const userMessageUpdateParams = {}; 187 | userMessageUpdateParams.message = state.messageInputValue; 188 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 189 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 190 | messages[messageIndex] = updatedMessage; 191 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 192 | } else { 193 | const userMessageParams = {}; 194 | userMessageParams.message = state.messageInputValue; 195 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 196 | const updatedMessages = [...messages, message]; 197 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 198 | }).onFailed((error) => { 199 | console.log(error) 200 | console.log("failed") 201 | }); 202 | } 203 | } 204 | 205 | const onFileInputChange = async (e) => { 206 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 207 | const { currentlyJoinedChannel, messages } = state; 208 | const fileMessageParams = {}; 209 | fileMessageParams.file = e.currentTarget.files[0]; 210 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 211 | const updatedMessages = [...messages, message]; 212 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 213 | }).onFailed((error) => { 214 | console.log(error) 215 | console.log("failed") 216 | }); 217 | } 218 | } 219 | 220 | const handleDeleteMessage = async (messageToDelete) => { 221 | const { currentlyJoinedChannel } = state; 222 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 223 | } 224 | 225 | const updateMessage = async (message) => { 226 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 227 | } 228 | 229 | const setupUser = async () => { 230 | const { userNameInputValue, userIdInputValue } = state; 231 | const sendbirdChat = await SendbirdChat.init({ 232 | appId: SENDBIRD_INFO.appId, 233 | localCacheEnabled: true, 234 | modules: [new OpenChannelModule()] 235 | }); 236 | 237 | try { 238 | await sendbirdChat.connect(userIdInputValue); 239 | } catch (e) { 240 | console.log("error", e) 241 | } 242 | await sendbirdChat.setChannelInvitationPreference(true); 243 | 244 | const userUpdateParams = {}; 245 | userUpdateParams.nickname = userNameInputValue; 246 | userUpdateParams.userId = userIdInputValue; 247 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 248 | 249 | sb = sendbirdChat; 250 | updateState({ ...state, loading: true }); 251 | const [channels, error] = await loadChannels(); 252 | if (error) { 253 | return onError(error); 254 | } 255 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 256 | } 257 | 258 | if (state.loading) { 259 | return
Loading...
260 | } 261 | 262 | if (state.error) { 263 | return
{state.error} check console for more information.
264 | } 265 | 266 | console.log('- - - - State object very useful for debugging - - - -'); 267 | console.log(state); 268 | 269 | return ( 270 | <> 271 | 278 | 284 | 289 | 294 | 299 | 303 | 310 | 311 | 312 | ); 313 | }; 314 | 315 | // Chat UI Components 316 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 317 | return ( 318 |
319 |
320 |

Open Channels

321 | 322 |
323 | { 324 | channels.map(channel => { 325 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 326 | return ( 327 |
328 |
{ handleJoinChannel(channel.url) }} 331 | > 332 | {channel.name} 333 |
334 | { 335 | userIsOperator && 336 |
337 | 341 | 344 |
345 | } 346 |
347 | ); 348 | }) 349 | } 350 |
351 | ); 352 | } 353 | 354 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 355 | if (currentlyJoinedChannel) { 356 | return
357 | {currentlyJoinedChannel.name} 358 |
359 | 360 |
361 |
{children}
362 |
; 363 | } 364 | return
; 365 | } 366 | 367 | const ChannelHeader = ({ children }) => { 368 | return
{children}
; 369 | } 370 | 371 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 372 | return messages.map(message => { 373 | return ( 374 |
375 | 380 |
381 | ); 382 | }) 383 | } 384 | 385 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 386 | if (message.url) { 387 | return ( 388 |
389 |
{timestampToTime(message.createdAt)}
390 |
{message.sender.nickname}{' '}
391 | 392 |
393 | ); 394 | } 395 | 396 | if (message.messageType === "admin") { 397 | return 398 | } 399 | 400 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 401 | return ( 402 |
403 |
{timestampToTime(message.createdAt)}
404 |
{message.sender.nickname}{':'}
405 |
{message.message}
406 | { 407 | messageSentByCurrentUser && 408 | <> 409 | 412 | 415 | 416 | } 417 |
418 | ); 419 | } 420 | 421 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 422 | return ( 423 |
424 | handleEnterPress(event, sendMessage))} 429 | /> 430 |
431 | 432 | 433 | { }} 440 | /> 441 |
442 |
443 | ); 444 | } 445 | 446 | const ChannelDetails = ({ 447 | currentlyUpdatingChannel, 448 | toggleChannelDetails, 449 | handleUpdateChannel, 450 | onChannelNamenIputChange 451 | }) => { 452 | if (currentlyUpdatingChannel) { 453 | return
454 |
455 |

Update Channel Details

456 |
Channel name
457 | 458 | 459 | 460 |
461 |
; 462 | } 463 | return null; 464 | } 465 | 466 | const ChannelCreate = ({ 467 | showChannelCreate, 468 | toggleShowCreateChannel, 469 | handleCreateChannel, 470 | onChannelNamenIputChange 471 | }) => { 472 | if (showChannelCreate) { 473 | return
474 |
475 |
476 |

Create Channel

477 |
478 |
Name
479 | handleEnterPress(event, handleCreateChannel)} /> 480 |
481 | 482 | 483 |
484 |
485 |
; 486 | } 487 | return null; 488 | } 489 | 490 | const CreateUserForm = ({ 491 | setupUser, 492 | settingUpUser, 493 | userNameInputValue, 494 | userIdInputValue, 495 | onUserNameInputChange, 496 | onUserIdInputChange 497 | }) => { 498 | if (settingUpUser) { 499 | return
500 |
handleEnterPress(event, setupUser)}> 501 |
User ID
502 | 507 |
User Nickname
508 | 513 |
514 | 518 |
519 |
520 |
521 | } else { 522 | return null; 523 | } 524 | } 525 | 526 | const AdminMessage = ({ message }) => ( 527 |
528 |
{timestampToTime(message.createdAt)}
529 |
{message.messageType}{':'}
530 |
{message.message}
531 |
532 | ) 533 | 534 | // Helpful functions that call Sendbird 535 | const loadChannels = async () => { 536 | try { 537 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 538 | const channels = await openChannelQuery.next(); 539 | return [channels, null]; 540 | } catch (error) { 541 | return [null, error]; 542 | } 543 | } 544 | 545 | const loadMessages = async (channel) => { 546 | try { 547 | 548 | //list all messages 549 | const messageListParams = {}; 550 | messageListParams.nextResultSize = 20; 551 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 552 | return [messages, null]; 553 | } catch (error) { 554 | return [null, error] 555 | } 556 | } 557 | 558 | const createChannel = async (channelName) => { 559 | try { 560 | const openChannelParams = {}; 561 | openChannelParams.name = channelName; 562 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 563 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 564 | return [openChannel, null]; 565 | } catch (error) { 566 | return [null, error]; 567 | } 568 | } 569 | 570 | const deleteChannel = async (channelUrl) => { 571 | try { 572 | const channel = await sb.openChannel.getChannel(channelUrl); 573 | await channel.delete(); 574 | return [channel, null]; 575 | } catch (error) { 576 | return [null, error]; 577 | } 578 | } 579 | 580 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 581 | try { 582 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 583 | const openChannelParams = {}; 584 | openChannelParams.name = channelNameInputValue; 585 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 586 | const updatedChannel = await channel.updateChannel(openChannelParams); 587 | return [updatedChannel, null]; 588 | } catch (error) { 589 | return [null, error]; 590 | } 591 | } 592 | 593 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 594 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 595 | } 596 | 597 | export default OpenChannelSendAnAdminMessage; 598 | -------------------------------------------------------------------------------- /src/samples/OpenChannelSendAndReceiveVariousTypesOfFiles.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelSendAndReceiveVariousTypesOfFiles = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false, 34 | isImageSizeError: false 35 | }); 36 | 37 | //need to access state in message reeived callback 38 | const stateRef = useRef(); 39 | stateRef.current = state; 40 | 41 | const channelRef = useRef(); 42 | 43 | const scrollToBottom = (item, smooth) => { 44 | item?.scrollTo({ 45 | top: item.scrollHeight, 46 | behavior: smooth 47 | }) 48 | } 49 | 50 | useEffect(() => { 51 | scrollToBottom(channelRef.current) 52 | }, [state.currentlyJoinedChannel]) 53 | 54 | useEffect(() => { 55 | scrollToBottom(channelRef.current, 'smooth') 56 | }, [state.messages]) 57 | 58 | const onError = (error) => { 59 | updateState({ ...state, error: error.message }); 60 | console.log(error); 61 | } 62 | 63 | const handleJoinChannel = async (channelUrl) => { 64 | if (state.currentlyJoinedChannel?.url === channelUrl) { 65 | return null; 66 | } 67 | const { channels } = state; 68 | updateState({ ...state, loading: true }); 69 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 70 | await channelToJoin.enter(); 71 | const [messages, error] = await loadMessages(channelToJoin); 72 | if (error) { 73 | return onError(error); 74 | } 75 | 76 | // setup connection event handlers 77 | const connectionHandler = new ConnectionHandler(); 78 | 79 | connectionHandler.onReconnectSucceeded = async () => { 80 | const [messages, error] = await loadMessages(channelToJoin); 81 | 82 | updateState({ ...stateRef.current, messages: messages }); 83 | } 84 | 85 | sb.addConnectionHandler(uuid(), connectionHandler); 86 | 87 | 88 | //listen for incoming messages 89 | const channelHandler = new OpenChannelHandler(); 90 | channelHandler.onMessageUpdated = (channel, message) => { 91 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 92 | const updatedMessages = [...stateRef.current.messages]; 93 | updatedMessages[messageIndex] = message; 94 | updateState({ ...stateRef.current, messages: updatedMessages }); 95 | } 96 | 97 | channelHandler.onMessageReceived = (channel, message) => { 98 | const updatedMessages = [...stateRef.current.messages, message]; 99 | updateState({ ...stateRef.current, messages: updatedMessages }); 100 | }; 101 | 102 | channelHandler.onMessageDeleted = (channel, message) => { 103 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 104 | return messageObject.messageId !== message; 105 | }); 106 | updateState({ ...stateRef.current, messages: updatedMessages }); 107 | } 108 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 109 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 110 | } 111 | 112 | const handleLeaveChannel = async () => { 113 | const { currentlyJoinedChannel } = state; 114 | await currentlyJoinedChannel.exit(); 115 | updateState({ ...state, currentlyJoinedChannel: null }) 116 | } 117 | 118 | const handleCreateChannel = async () => { 119 | const { channelNameInputValue } = state; 120 | const [openChannel, error] = await createChannel(channelNameInputValue); 121 | if (error) { 122 | return onError(error); 123 | } 124 | const updatedChannels = [openChannel, ...state.channels]; 125 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 126 | } 127 | 128 | const handleDeleteChannel = async (channelUrl) => { 129 | const [channel, error] = await deleteChannel(channelUrl); 130 | if (error) { 131 | return onError(error); 132 | } 133 | const updatedChannels = state.channels.filter((channel) => { 134 | return channel.url !== channelUrl; 135 | }); 136 | updateState({ ...state, channels: updatedChannels }); 137 | } 138 | 139 | const handleUpdateChannel = async () => { 140 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 141 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 142 | if (error) { 143 | return onError(error); 144 | } 145 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 146 | const updatedChannels = [...channels]; 147 | updatedChannels[indexToReplace] = updatedChannel; 148 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 149 | } 150 | 151 | const toggleChannelDetails = (channel) => { 152 | if (channel) { 153 | updateState({ ...state, currentlyUpdatingChannel: channel }); 154 | } else { 155 | updateState({ ...state, currentlyUpdatingChannel: null }); 156 | } 157 | } 158 | 159 | const toggleShowCreateChannel = () => { 160 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 161 | } 162 | 163 | const onChannelNamenIputChange = (e) => { 164 | const channelNameInputValue = e.currentTarget.value; 165 | updateState({ ...state, channelNameInputValue }); 166 | } 167 | 168 | const onUserNameInputChange = (e) => { 169 | const userNameInputValue = e.currentTarget.value; 170 | updateState({ ...state, userNameInputValue }); 171 | } 172 | 173 | const onUserIdInputChange = (e) => { 174 | const userIdInputValue = e.currentTarget.value; 175 | updateState({ ...state, userIdInputValue }); 176 | } 177 | 178 | const onMessageInputChange = (e) => { 179 | const messageInputValue = e.currentTarget.value; 180 | updateState({ ...state, messageInputValue }); 181 | } 182 | 183 | const sendMessage = async () => { 184 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 185 | 186 | if (messageToUpdate) { 187 | const userMessageUpdateParams = {}; 188 | userMessageUpdateParams.message = state.messageInputValue; 189 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 190 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 191 | messages[messageIndex] = updatedMessage; 192 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 193 | } else { 194 | const userMessageParams = {}; 195 | userMessageParams.message = state.messageInputValue; 196 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 197 | const updatedMessages = [...messages, message]; 198 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 199 | }).onFailed((error) => { 200 | console.log(error) 201 | console.log("failed") 202 | }); 203 | } 204 | } 205 | 206 | const onFileInputChange = async (e) => { 207 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 208 | const { currentlyJoinedChannel, messages } = state; 209 | const fileMessageParams = {}; 210 | fileMessageParams.file = e.currentTarget.files[0]; 211 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 212 | message.type = fileMessageParams.mimeType; 213 | const updatedMessages = [...messages, message]; 214 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 215 | }).onFailed((error) => { 216 | console.log(error) 217 | console.log("failed") 218 | updateState({ ...state, isImageSizeError: true }) 219 | }); 220 | } 221 | } 222 | 223 | const handleDeleteMessage = async (messageToDelete) => { 224 | const { currentlyJoinedChannel } = state; 225 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 226 | } 227 | 228 | const updateMessage = async (message) => { 229 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 230 | } 231 | 232 | const setupUser = async () => { 233 | const { userNameInputValue, userIdInputValue } = state; 234 | const sendbirdChat = await SendbirdChat.init({ 235 | appId: SENDBIRD_INFO.appId, 236 | localCacheEnabled: true, 237 | modules: [new OpenChannelModule()] 238 | }); 239 | 240 | try { 241 | await sendbirdChat.connect(userIdInputValue); 242 | } catch (e) { 243 | console.log("error", e) 244 | } 245 | await sendbirdChat.setChannelInvitationPreference(true); 246 | 247 | const userUpdateParams = {}; 248 | userUpdateParams.nickname = userNameInputValue; 249 | userUpdateParams.userId = userIdInputValue; 250 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 251 | 252 | sb = sendbirdChat; 253 | updateState({ ...state, loading: true }); 254 | const [channels, error] = await loadChannels(); 255 | if (error) { 256 | return onError(error); 257 | } 258 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 259 | } 260 | 261 | if (state.loading) { 262 | return
Loading...
263 | } 264 | 265 | if (state.error) { 266 | return
{state.error} check console for more information.
267 | } 268 | 269 | console.log('- - - - State object very useful for debugging - - - -'); 270 | console.log(state); 271 | 272 | return ( 273 | <> 274 | 281 | 287 | 292 | 297 | 303 | 307 | 314 | 315 | 316 | ); 317 | }; 318 | 319 | // Chat UI Components 320 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 321 | return ( 322 |
323 |
324 |

Open Channels

325 | 326 |
327 | { 328 | channels.map(channel => { 329 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 330 | return ( 331 |
332 |
{ handleJoinChannel(channel.url) }}> 334 | {channel.name} 335 |
336 | {userIsOperator && 337 |
338 | 341 | 344 |
345 | } 346 |
347 | ); 348 | }) 349 | } 350 |
351 | ); 352 | } 353 | 354 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, isImageSizeError, channelRef }) => { 355 | if (currentlyJoinedChannel) { 356 | return
357 | {currentlyJoinedChannel.name} 358 | {isImageSizeError &&
There was an error sending the file.
} 359 |
360 | 361 |
362 |
{children}
363 |
; 364 | } 365 | return
; 366 | } 367 | 368 | const ChannelHeader = ({ children }) => { 369 | return
{children}
; 370 | } 371 | 372 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 373 | return messages.map(message => { 374 | return ( 375 |
376 | 381 |
382 | ); 383 | }) 384 | } 385 | 386 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 387 | if (!message.sender) return null; if (message.url) { 388 | const isImageMessage = message.type.includes("image"); 389 | return ( 390 |
391 |
{timestampToTime(message.createdAt)}
392 |
{message.sender.nickname}{': '}
393 | {isImageMessage ? : 394 | 395 | {message.name} 396 | } 397 |
398 | ); 399 | } 400 | 401 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 402 | return ( 403 |
404 |
{timestampToTime(message.createdAt)}
405 |
{message.sender.nickname}{':'}
406 |
{message.message}
407 | 408 | {messageSentByCurrentUser && <> 409 | 412 | 415 | } 416 |
417 | ); 418 | } 419 | 420 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 421 | return ( 422 |
423 | handleEnterPress(event, sendMessage))} 428 | /> 429 |
430 | 431 | 432 | { }} 439 | /> 440 |
441 |
442 | ); 443 | } 444 | 445 | const ChannelDetails = ({ 446 | currentlyUpdatingChannel, 447 | toggleChannelDetails, 448 | handleUpdateChannel, 449 | onChannelNamenIputChange 450 | }) => { 451 | if (currentlyUpdatingChannel) { 452 | return
453 |
454 |

Update Channel Details

455 |
Channel name
456 | 457 | 458 | 459 |
460 |
; 461 | } 462 | return null; 463 | } 464 | 465 | const ChannelCreate = ({ 466 | showChannelCreate, 467 | toggleShowCreateChannel, 468 | handleCreateChannel, 469 | onChannelNamenIputChange 470 | }) => { 471 | if (showChannelCreate) { 472 | return
473 |
474 |
475 |

Create Channel

476 |
477 |
Name
478 | handleEnterPress(event, handleCreateChannel)} /> 479 |
480 | 481 | 482 |
483 |
484 |
; 485 | } 486 | return null; 487 | } 488 | 489 | const CreateUserForm = ({ 490 | setupUser, 491 | settingUpUser, 492 | userNameInputValue, 493 | userIdInputValue, 494 | onUserNameInputChange, 495 | onUserIdInputChange 496 | }) => { 497 | if (settingUpUser) { 498 | return
499 |
handleEnterPress(event, setupUser)}> 500 |
User ID
501 | 506 |
User Nickname
507 | 511 |
512 | 516 |
517 |
518 |
519 | } else { 520 | return null; 521 | } 522 | } 523 | 524 | // Helpful functions that call Sendbird 525 | const loadChannels = async () => { 526 | try { 527 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 528 | const channels = await openChannelQuery.next(); 529 | return [channels, null]; 530 | } catch (error) { 531 | return [null, error]; 532 | } 533 | } 534 | 535 | const loadMessages = async (channel) => { 536 | try { 537 | 538 | //list all messages 539 | const messageListParams = {}; 540 | messageListParams.nextResultSize = 20; 541 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 542 | return [messages, null]; 543 | } catch (error) { 544 | return [null, error] 545 | } 546 | } 547 | 548 | const createChannel = async (channelName) => { 549 | try { 550 | const openChannelParams = {}; 551 | openChannelParams.name = channelName; 552 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 553 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 554 | return [openChannel, null]; 555 | } catch (error) { 556 | return [null, error]; 557 | } 558 | } 559 | 560 | const deleteChannel = async (channelUrl) => { 561 | try { 562 | const channel = await sb.openChannel.getChannel(channelUrl); 563 | await channel.delete(); 564 | return [channel, null]; 565 | } catch (error) { 566 | return [null, error]; 567 | } 568 | } 569 | 570 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 571 | try { 572 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 573 | const openChannelParams = {}; 574 | openChannelParams.name = channelNameInputValue; 575 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 576 | const updatedChannel = await channel.updateChannel(openChannelParams); 577 | return [updatedChannel, null]; 578 | } catch (error) { 579 | return [null, error]; 580 | } 581 | } 582 | 583 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 584 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 585 | } 586 | 587 | export default OpenChannelSendAndReceiveVariousTypesOfFiles; 588 | -------------------------------------------------------------------------------- /src/samples/OpenChannelThumbnails.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { ConnectionHandler } from '@sendbird/chat'; 3 | 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import SendbirdChat from '@sendbird/chat'; 7 | import { 8 | OpenChannelModule, 9 | OpenChannelHandler, 10 | } from '@sendbird/chat/openChannel'; 11 | 12 | import { SENDBIRD_INFO } from '../constants/constants'; 13 | import { timestampToTime, handleEnterPress } from '../utils/messageUtils'; 14 | 15 | let sb; 16 | 17 | const OpenChannelThumbnails = (props) => { 18 | 19 | const [state, updateState] = useState({ 20 | currentlyJoinedChannel: null, 21 | currentlyUpdatingChannel: null, 22 | messages: [], 23 | channels: [], 24 | showChannelCreate: false, 25 | messageInputValue: "", 26 | userNameInputValue: "", 27 | userIdInputValue: "", 28 | channelNameInputValue: "", 29 | settingUpUser: true, 30 | file: null, 31 | messageToUpdate: null, 32 | loading: false, 33 | error: false 34 | }); 35 | 36 | //need to access state in message reeived callback 37 | const stateRef = useRef(); 38 | stateRef.current = state; 39 | 40 | const channelRef = useRef(); 41 | 42 | const scrollToBottom = (item, smooth) => { 43 | item?.scrollTo({ 44 | top: item.scrollHeight, 45 | behavior: smooth 46 | }) 47 | } 48 | 49 | useEffect(() => { 50 | scrollToBottom(channelRef.current) 51 | }, [state.currentlyJoinedChannel]) 52 | 53 | useEffect(() => { 54 | scrollToBottom(channelRef.current, 'smooth') 55 | }, [state.messages]) 56 | 57 | const onError = (error) => { 58 | updateState({ ...state, error: error.message }); 59 | console.log(error); 60 | } 61 | 62 | const handleJoinChannel = async (channelUrl) => { 63 | if (state.currentlyJoinedChannel?.url === channelUrl) { 64 | return null; 65 | } 66 | const { channels } = state; 67 | updateState({ ...state, loading: true }); 68 | const channelToJoin = channels.find((channel) => channel.url === channelUrl); 69 | await channelToJoin.enter(); 70 | const [messages, error] = await loadMessages(channelToJoin); 71 | if (error) { 72 | return onError(error); 73 | } 74 | 75 | // setup connection event handlers 76 | const connectionHandler = new ConnectionHandler(); 77 | 78 | connectionHandler.onReconnectSucceeded = async () => { 79 | const [messages, error] = await loadMessages(channelToJoin); 80 | 81 | updateState({ ...stateRef.current, messages: messages }); 82 | } 83 | 84 | sb.addConnectionHandler(uuid(), connectionHandler); 85 | 86 | //listen for incoming messages 87 | const channelHandler = new OpenChannelHandler(); 88 | channelHandler.onMessageUpdated = (channel, message) => { 89 | const messageIndex = stateRef.current.messages.findIndex((item => item.messageId == message.messageId)); 90 | const updatedMessages = [...stateRef.current.messages]; 91 | updatedMessages[messageIndex] = message; 92 | updateState({ ...stateRef.current, messages: updatedMessages }); 93 | } 94 | 95 | channelHandler.onMessageReceived = (channel, message) => { 96 | const updatedMessages = [...stateRef.current.messages, message]; 97 | updateState({ ...stateRef.current, messages: updatedMessages }); 98 | }; 99 | 100 | channelHandler.onMessageDeleted = (channel, message) => { 101 | const updatedMessages = stateRef.current.messages.filter((messageObject) => { 102 | return messageObject.messageId !== message; 103 | }); 104 | updateState({ ...stateRef.current, messages: updatedMessages }); 105 | } 106 | sb.openChannel.addOpenChannelHandler(uuid(), channelHandler); 107 | updateState({ ...state, currentlyJoinedChannel: channelToJoin, messages: messages, loading: false }) 108 | } 109 | 110 | const handleLeaveChannel = async () => { 111 | const { currentlyJoinedChannel } = state; 112 | await currentlyJoinedChannel.exit(); 113 | updateState({ ...state, currentlyJoinedChannel: null }) 114 | } 115 | 116 | const handleCreateChannel = async () => { 117 | const { channelNameInputValue } = state; 118 | const [openChannel, error] = await createChannel(channelNameInputValue); 119 | if (error) { 120 | return onError(error); 121 | } 122 | const updatedChannels = [openChannel, ...state.channels]; 123 | updateState({ ...state, channels: updatedChannels, showChannelCreate: false }); 124 | } 125 | 126 | const handleDeleteChannel = async (channelUrl) => { 127 | const [channel, error] = await deleteChannel(channelUrl); 128 | if (error) { 129 | return onError(error); 130 | } 131 | const updatedChannels = state.channels.filter((channel) => { 132 | return channel.url !== channelUrl; 133 | }); 134 | updateState({ ...state, channels: updatedChannels }); 135 | } 136 | 137 | const handleUpdateChannel = async () => { 138 | const { currentlyUpdatingChannel, channelNameInputValue, channels } = state; 139 | const [updatedChannel, error] = await updateChannel(currentlyUpdatingChannel, channelNameInputValue); 140 | if (error) { 141 | return onError(error); 142 | } 143 | const indexToReplace = channels.findIndex((channel) => channel.url === currentlyUpdatingChannel.channelUrl); 144 | const updatedChannels = [...channels]; 145 | updatedChannels[indexToReplace] = updatedChannel; 146 | updateState({ ...state, channels: updatedChannels, currentlyUpdatingChannel: null }); 147 | } 148 | 149 | const toggleChannelDetails = (channel) => { 150 | if (channel) { 151 | updateState({ ...state, currentlyUpdatingChannel: channel }); 152 | } else { 153 | updateState({ ...state, currentlyUpdatingChannel: null }); 154 | } 155 | } 156 | 157 | const toggleShowCreateChannel = () => { 158 | updateState({ ...state, showChannelCreate: !state.showChannelCreate }); 159 | } 160 | 161 | const onChannelNamenIputChange = (e) => { 162 | const channelNameInputValue = e.currentTarget.value; 163 | updateState({ ...state, channelNameInputValue }); 164 | } 165 | 166 | const onUserNameInputChange = (e) => { 167 | const userNameInputValue = e.currentTarget.value; 168 | updateState({ ...state, userNameInputValue }); 169 | } 170 | 171 | const onUserIdInputChange = (e) => { 172 | const userIdInputValue = e.currentTarget.value; 173 | updateState({ ...state, userIdInputValue }); 174 | } 175 | 176 | const onMessageInputChange = (e) => { 177 | const messageInputValue = e.currentTarget.value; 178 | updateState({ ...state, messageInputValue }); 179 | } 180 | 181 | const sendMessage = async () => { 182 | const { messageToUpdate, currentlyJoinedChannel, messages } = state; 183 | 184 | if (messageToUpdate) { 185 | const userMessageUpdateParams = {}; 186 | userMessageUpdateParams.message = state.messageInputValue; 187 | const updatedMessage = await currentlyJoinedChannel.updateUserMessage(messageToUpdate.messageId, userMessageUpdateParams) 188 | const messageIndex = messages.findIndex((item => item.messageId == messageToUpdate.messageId)); 189 | messages[messageIndex] = updatedMessage; 190 | updateState({ ...state, messages: messages, messageInputValue: "", messageToUpdate: null }); 191 | } else { 192 | const userMessageParams = {}; 193 | userMessageParams.message = state.messageInputValue; 194 | currentlyJoinedChannel.sendUserMessage(userMessageParams).onSucceeded((message) => { 195 | const updatedMessages = [...messages, message]; 196 | updateState({ ...state, messages: updatedMessages, messageInputValue: "" }); 197 | }).onFailed((error) => { 198 | console.log(error) 199 | console.log("failed") 200 | }); 201 | } 202 | } 203 | 204 | const onFileInputChange = async (e) => { 205 | if (e.currentTarget.files && e.currentTarget.files.length > 0) { 206 | const { currentlyJoinedChannel, messages } = state; 207 | const fileMessageParams = {}; 208 | fileMessageParams.file = e.currentTarget.files[0]; 209 | fileMessageParams.thumbnailSizes = [{ maxWidth: 100, maxHeight: 100 }, { maxWidth: 200, maxHeight: 200 }]; 210 | currentlyJoinedChannel.sendFileMessage(fileMessageParams).onSucceeded((message) => { 211 | const updatedMessages = [...messages, message]; 212 | updateState({ ...state, messages: updatedMessages, messageInputValue: "", file: null }); 213 | }).onFailed((error) => { 214 | console.log(error) 215 | console.log("failed") 216 | }); 217 | } 218 | } 219 | 220 | const handleDeleteMessage = async (messageToDelete) => { 221 | const { currentlyJoinedChannel } = state; 222 | await deleteMessage(currentlyJoinedChannel, messageToDelete); // Delete 223 | } 224 | 225 | const updateMessage = async (message) => { 226 | updateState({ ...state, messageToUpdate: message, messageInputValue: message.message }); 227 | } 228 | 229 | const setupUser = async () => { 230 | const { userNameInputValue, userIdInputValue } = state; 231 | const sendbirdChat = await SendbirdChat.init({ 232 | appId: SENDBIRD_INFO.appId, 233 | localCacheEnabled: true, 234 | modules: [new OpenChannelModule()] 235 | }); 236 | 237 | try { 238 | await sendbirdChat.connect(userIdInputValue); 239 | } catch (e) { 240 | console.log("error", e) 241 | } 242 | await sendbirdChat.setChannelInvitationPreference(true); 243 | 244 | const userUpdateParams = {}; 245 | userUpdateParams.nickname = userNameInputValue; 246 | userUpdateParams.userId = userIdInputValue; 247 | await sendbirdChat.updateCurrentUserInfo(userUpdateParams); 248 | 249 | sb = sendbirdChat; 250 | updateState({ ...state, loading: true }); 251 | const [channels, error] = await loadChannels(); 252 | if (error) { 253 | return onError(error); 254 | } 255 | updateState({ ...state, channels: channels, loading: false, settingUpUser: false }); 256 | } 257 | 258 | if (state.loading) { 259 | return
Loading...
260 | } 261 | 262 | if (state.error) { 263 | return
{state.error} check console for more information.
264 | } 265 | 266 | console.log('- - - - State object very useful for debugging - - - -'); 267 | console.log(state); 268 | 269 | return ( 270 | <> 271 | 278 | 284 | 289 | 294 | 299 | 303 | 310 | 311 | 312 | ); 313 | }; 314 | 315 | // Chat UI Components 316 | const ChannelList = ({ channels, handleJoinChannel, toggleShowCreateChannel, handleDeleteChannel, toggleChannelDetails }) => { 317 | return ( 318 |
319 |
320 |

Open Channels

321 | 322 |
323 | {channels.map(channel => { 324 | const userIsOperator = channel.operators.some((operator) => operator.userId === sb.currentUser.userId) 325 | return ( 326 |
327 |
{ handleJoinChannel(channel.url) }}> 329 | {channel.name} 330 |
331 | {userIsOperator && 332 |
333 | 336 | 339 |
340 | } 341 |
342 | ); 343 | })} 344 |
345 | ); 346 | } 347 | 348 | const Channel = ({ currentlyJoinedChannel, handleLeaveChannel, children, channelRef }) => { 349 | if (currentlyJoinedChannel) { 350 | return
351 | {currentlyJoinedChannel.name} 352 |
353 | 354 |
355 |
{children}
356 |
; 357 | } 358 | return
; 359 | } 360 | 361 | const ChannelHeader = ({ children }) => { 362 | return
{children}
; 363 | } 364 | 365 | const MessagesList = ({ messages, handleDeleteMessage, updateMessage }) => { 366 | return messages.map(message => { 367 | return ( 368 |
369 | 374 |
375 | ); 376 | }) 377 | } 378 | 379 | const Message = ({ message, updateMessage, handleDeleteMessage }) => { 380 | if (!message.sender) return null; if (message.url) { 381 | return ( 382 | ); 383 | } 384 | 385 | const messageSentByCurrentUser = message.sender.userId === sb.currentUser.userId; 386 | return ( 387 |
388 |
{timestampToTime(message.createdAt)}
389 |
{message.sender.nickname}{':'}
390 |
{message.message}
391 | 392 | {messageSentByCurrentUser && <> 393 | 396 | 399 | } 400 |
401 | ); 402 | } 403 | 404 | const MessageInput = ({ value, onChange, sendMessage, onFileInputChange }) => { 405 | return ( 406 |
407 | handleEnterPress(event, sendMessage))} 412 | /> 413 |
414 | 415 | 416 | { }} 423 | /> 424 |
425 |
426 | ); 427 | } 428 | 429 | const ChannelDetails = ({ 430 | currentlyUpdatingChannel, 431 | toggleChannelDetails, 432 | handleUpdateChannel, 433 | onChannelNamenIputChange 434 | }) => { 435 | if (currentlyUpdatingChannel) { 436 | return
437 |
438 |

Update Channel Details

439 |
Channel name
440 | 441 | 442 | 443 |
444 |
; 445 | } 446 | return null; 447 | } 448 | 449 | const ChannelCreate = ({ 450 | showChannelCreate, 451 | toggleShowCreateChannel, 452 | handleCreateChannel, 453 | onChannelNamenIputChange 454 | }) => { 455 | if (showChannelCreate) { 456 | return
457 |
458 |
459 |

Create Channel

460 |
461 |
Name
462 | handleEnterPress(event, handleCreateChannel)} /> 463 |
464 | 465 | 466 |
467 |
468 |
; 469 | } 470 | return null; 471 | } 472 | 473 | const CreateUserForm = ({ 474 | setupUser, 475 | settingUpUser, 476 | userNameInputValue, 477 | userIdInputValue, 478 | onUserNameInputChange, 479 | onUserIdInputChange 480 | }) => { 481 | if (settingUpUser) { 482 | return
483 |
handleEnterPress(event, setupUser)}> 484 |
User ID
485 | 489 |
User Nickname
490 | 494 |
495 | 499 |
500 |
501 |
502 | } else { 503 | return null; 504 | } 505 | } 506 | 507 | const Thumbnails = ({ createdAt, sender, thumbnails, message }) => { 508 | const thumbnail = thumbnails[0]; 509 | 510 | return ( 511 |
512 |
{timestampToTime(createdAt)}
513 |
{sender.nickname}{' '}
514 | {thumbnail ? thumbnail : message} 515 |
516 | ); 517 | }; 518 | 519 | 520 | // Helpful functions that call Sendbird 521 | const loadChannels = async () => { 522 | try { 523 | const openChannelQuery = sb.openChannel.createOpenChannelListQuery({ limit: 30 }); 524 | const channels = await openChannelQuery.next(); 525 | return [channels, null]; 526 | } catch (error) { 527 | return [null, error]; 528 | } 529 | } 530 | 531 | const loadMessages = async (channel) => { 532 | try { 533 | 534 | //list all messages 535 | const messageListParams = {}; 536 | messageListParams.nextResultSize = 20; 537 | const messages = await channel.getMessagesByTimestamp(0, messageListParams); 538 | return [messages, null]; 539 | } catch (error) { 540 | return [null, error] 541 | } 542 | } 543 | 544 | const createChannel = async (channelName) => { 545 | try { 546 | const openChannelParams = {}; 547 | openChannelParams.name = channelName; 548 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 549 | const openChannel = await sb.openChannel.createChannel(openChannelParams); 550 | return [openChannel, null]; 551 | } catch (error) { 552 | return [null, error]; 553 | } 554 | } 555 | 556 | const deleteChannel = async (channelUrl) => { 557 | try { 558 | const channel = await sb.openChannel.getChannel(channelUrl); 559 | await channel.delete(); 560 | return [channel, null]; 561 | } catch (error) { 562 | return [null, error]; 563 | } 564 | } 565 | 566 | const updateChannel = async (currentlyUpdatingChannel, channelNameInputValue) => { 567 | try { 568 | const channel = await sb.openChannel.getChannel(currentlyUpdatingChannel.url); 569 | const openChannelParams = {}; 570 | openChannelParams.name = channelNameInputValue; 571 | openChannelParams.operatorUserIds = [sb.currentUser.userId]; 572 | const updatedChannel = await channel.updateChannel(openChannelParams); 573 | return [updatedChannel, null]; 574 | } catch (error) { 575 | return [null, error]; 576 | } 577 | } 578 | 579 | const deleteMessage = async (currentlyJoinedChannel, messageToDelete) => { 580 | await currentlyJoinedChannel.deleteMessage(messageToDelete); 581 | } 582 | 583 | export default OpenChannelThumbnails; 584 | -------------------------------------------------------------------------------- /src/utils/messageUtils.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export const protectFromXSS = (text) => { 4 | return text 5 | .replace(/\&/g, '&') 6 | .replace(/\/g, '>') 8 | .replace(/\"/g, '"') 9 | .replace(/\'/g, '''); 10 | }; 11 | 12 | export const timestampToTime = (timestamp) => { 13 | const now = new Date().getTime(); 14 | const nowDate = moment.unix(now.toString().length === 13 ? now / 1000 : now).format('MM/DD'); 15 | let date = moment.unix(timestamp.toString().length === 13 ? timestamp / 1000 : timestamp).format('MM/DD'); 16 | if (date === 'Invalid date') { 17 | date = ''; 18 | } 19 | return nowDate === date 20 | ? moment.unix(timestamp.toString().length === 13 ? timestamp / 1000 : timestamp).format('HH:mm') 21 | : date; 22 | }; 23 | 24 | export const handleEnterPress = (event, callback) => { 25 | if (event.key === 'Enter') { 26 | callback() 27 | } 28 | } --------------------------------------------------------------------------------