├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── assets └── logo.svg ├── common ├── package-lock.json ├── package.json ├── src │ ├── enum │ │ ├── HostDisconnectAction.ts │ │ ├── MessageType.ts │ │ ├── ParticipantRole.ts │ │ └── Reactions.ts │ ├── helper │ │ └── MediaSourceToTypeMap.ts │ ├── index.ts │ └── interfaces │ │ ├── Input.ts │ │ ├── RoomSettings.ts │ │ ├── Summaries.ts │ │ └── WebRTC.ts └── tsconfig.json ├── frontend ├── .gitignore ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── assets │ │ └── logo │ │ │ ├── bitlinklogo.ai │ │ │ └── logo.svg │ ├── components │ │ ├── App.css │ │ ├── App.tsx │ │ ├── Chat │ │ │ ├── ChatContainer.css │ │ │ ├── ChatContainer.tsx │ │ │ ├── ChatInput.css │ │ │ ├── ChatInput.tsx │ │ │ ├── Messages │ │ │ │ ├── MessageComponent.css │ │ │ │ ├── MessageComponent.tsx │ │ │ │ ├── MessageContent.css │ │ │ │ ├── MessageContent.tsx │ │ │ │ ├── MessagesContainer.css │ │ │ │ ├── MessagesContainer.tsx │ │ │ │ ├── ReactionsDisplayer.tsx │ │ │ │ ├── SystemMessage.css │ │ │ │ └── SystemMessage.tsx │ │ │ └── ParticipantList │ │ │ │ ├── ChatParticipant.css │ │ │ │ ├── ChatParticipant.tsx │ │ │ │ ├── ChatRoomList.css │ │ │ │ ├── ChatRoomList.tsx │ │ │ │ ├── SearchBar.css │ │ │ │ ├── SearchBar.tsx │ │ │ │ └── WaitingRoom │ │ │ │ ├── WaitingRoomList.css │ │ │ │ ├── WaitingRoomList.tsx │ │ │ │ ├── WaitingRoomListParticipant.css │ │ │ │ └── WaitingRoomListParticipant.tsx │ │ ├── Footer │ │ │ ├── Footer.css │ │ │ └── Footer.tsx │ │ ├── Header │ │ │ ├── Header.css │ │ │ ├── Header.tsx │ │ │ ├── RoomId.css │ │ │ └── RoomId.tsx │ │ ├── LegalText.tsx │ │ ├── Modals │ │ │ ├── CreateDialog.tsx │ │ │ ├── Dialog.css │ │ │ ├── JoinDialog.tsx │ │ │ ├── JoinOrCreate.css │ │ │ ├── JoinOrCreate.tsx │ │ │ ├── Joining.tsx │ │ │ ├── LeaveDialog.css │ │ │ ├── LeaveDialog.tsx │ │ │ ├── Modal.css │ │ │ ├── Modal.tsx │ │ │ ├── Settings │ │ │ │ ├── SettingsModal.css │ │ │ │ ├── SettingsModal.tsx │ │ │ │ ├── SettingsPanel.css │ │ │ │ ├── SettingsPanel.tsx │ │ │ │ ├── SettingsPanelItem.css │ │ │ │ ├── SettingsPanelItem.tsx │ │ │ │ ├── SettingsViewer.css │ │ │ │ ├── SettingsViewer.tsx │ │ │ │ └── SettingsViews │ │ │ │ │ ├── MySettings.tsx │ │ │ │ │ ├── Participants.tsx │ │ │ │ │ ├── Report.tsx │ │ │ │ │ ├── RoomSettingsSettings.tsx │ │ │ │ │ ├── VideoEffects.css │ │ │ │ │ └── VideoEffects.tsx │ │ │ └── WaitingRoom.tsx │ │ ├── NotificationViewer.css │ │ ├── NotificationViewer.tsx │ │ ├── Tiles │ │ │ ├── TileContainer │ │ │ │ ├── AudioFiller.tsx │ │ │ │ ├── ControlBar │ │ │ │ │ ├── ControlBar.css │ │ │ │ │ ├── ControlBar.tsx │ │ │ │ │ ├── ScreenshareSlash.css │ │ │ │ │ └── ScreenshareSlash.tsx │ │ │ │ ├── Layouts │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ ├── PinnedParticipant.tsx │ │ │ │ │ ├── PinnedScreen.css │ │ │ │ │ └── PinnedScreen.tsx │ │ │ │ ├── PreviewBox.css │ │ │ │ ├── PreviewBox.tsx │ │ │ │ ├── TileContainer.css │ │ │ │ ├── TileContainer.tsx │ │ │ │ └── VolumeSlider │ │ │ │ │ ├── VolumeSlider.css │ │ │ │ │ └── VolumeSlider.tsx │ │ │ └── TileTypes │ │ │ │ ├── AudioTile.css │ │ │ │ ├── AudioTile.tsx │ │ │ │ ├── ScreenTile.css │ │ │ │ ├── ScreenTile.tsx │ │ │ │ ├── TilePlaceholder.css │ │ │ │ ├── TilePlaceholder.tsx │ │ │ │ ├── Util │ │ │ │ ├── AutoPlayAudio.tsx │ │ │ │ ├── TileMenuItem.css │ │ │ │ ├── TileMenuItem.tsx │ │ │ │ ├── TileWrapper.css │ │ │ │ └── TileWrapper.tsx │ │ │ │ ├── VideoTile.css │ │ │ │ └── VideoTile.tsx │ │ └── Util │ │ │ ├── Logo.css │ │ │ ├── Logo.tsx │ │ │ ├── ParticipantList.css │ │ │ ├── ParticipantList.tsx │ │ │ ├── Spinner.css │ │ │ └── Spinner.tsx │ ├── controllers │ │ ├── IO.ts │ │ └── handlers │ │ │ ├── handleAddedToGroup.ts │ │ │ ├── handleDeleteMessage.ts │ │ │ ├── handleEditMessage.ts │ │ │ ├── handleGroupUpdateName.ts │ │ │ ├── handleJoinRoom.ts │ │ │ ├── handleKick.ts │ │ │ ├── handleMediaStateUpdate.ts │ │ │ ├── handleNewConsumer.ts │ │ │ ├── handleNewGroupParticipant.ts │ │ │ ├── handleNewMessage.ts │ │ │ ├── handleNewParticipant.ts │ │ │ ├── handleParticipantLeft.ts │ │ │ ├── handleParticipantNameChange.ts │ │ │ ├── handleParticipantUpdateRole.ts │ │ │ ├── handleRoomClosuere.ts │ │ │ ├── handleUpdatedRoomSettings.ts │ │ │ ├── handleWaitingRoomAccept.ts │ │ │ ├── handleWaitingRoomNewParticipant.ts │ │ │ ├── handleWaitingRoomRejection.ts │ │ │ └── index.ts │ ├── enum │ │ ├── IO_Errors.ts │ │ ├── NotificationType.ts │ │ ├── SettingsPanels.ts │ │ └── TileDisplayMode.ts │ ├── hooks │ │ └── useLayoutCalculation.tsx │ ├── index.tsx │ ├── interfaces │ │ ├── Message │ │ │ ├── DirectMessage.ts │ │ │ ├── GroupMessage.ts │ │ │ ├── Message.ts │ │ │ ├── SystemMessage.ts │ │ │ └── index.ts │ │ ├── MessageGroup.ts │ │ ├── Messages.ts │ │ ├── UINotification.ts │ │ └── handleEvent.ts │ ├── models │ │ └── Participant.ts │ ├── react-app-env.d.ts │ ├── services │ │ ├── ChatStoreService.ts │ │ ├── HardwareService.ts │ │ ├── MyInfoService.ts │ │ ├── NotificationService.ts │ │ ├── ParticipantService.ts │ │ ├── RoomService.ts │ │ ├── StreamEffectService.ts │ │ └── UIStoreService.ts │ ├── stores │ │ ├── ChatStore.ts │ │ ├── MyInfoStore.ts │ │ ├── NotificationStore.ts │ │ ├── ParticipantsStore.ts │ │ ├── RoomStore.ts │ │ ├── StreamEffectStore.ts │ │ └── UIStore.ts │ └── util │ │ ├── CameraStreamEffectsRunner.ts │ │ ├── ResetStores.ts │ │ ├── TimerWorker.ts │ │ ├── debug.ts │ │ ├── layout │ │ ├── LayoutFinder.ts │ │ └── LayoutSizeCalculation.ts │ │ └── msToTime.ts └── tsconfig.json ├── lerna.json ├── package-lock.json ├── package.json └── server ├── .gitignore ├── app.ts ├── bin └── www ├── config.ts ├── package-lock.json ├── package.json ├── src ├── handlers │ ├── ioHandlers │ │ └── handleConnection.ts │ ├── participantHandlers │ │ ├── handleConnectTransport.ts │ │ ├── handleCreateProducer.ts │ │ ├── handleCreateTransport.ts │ │ ├── handleDeleteMessage.ts │ │ ├── handleDisconnectParticipant.ts │ │ ├── handleEditMessage.ts │ │ ├── handleEndRoom.ts │ │ ├── handleGetRoomSettings.ts │ │ ├── handleKickParticipant.ts │ │ ├── handleLeaveParticipant.ts │ │ ├── handleProducerAction.ts │ │ ├── handleSendMessage.ts │ │ ├── handleTransferHost.ts │ │ ├── handleTransportsReady.ts │ │ ├── handleUpdateName.ts │ │ ├── handleUpdateRoomSettings.ts │ │ ├── handleWaitingRoomDecision.ts │ │ └── index.ts │ └── socketHandlers │ │ ├── handleCreateRoom.ts │ │ ├── handleDisconnectSocket.ts │ │ ├── handleGetRTPCapabilities.ts │ │ └── handleJoinRoom.ts ├── helpers │ └── debug.ts ├── interfaces │ ├── MediasoupPeer.ts │ ├── Message │ │ ├── DirectMessage.ts │ │ ├── GroupMessage.ts │ │ ├── Message.ts │ │ ├── SystemMessage.ts │ │ └── index.ts │ ├── MessageGroup.ts │ ├── Participant.ts │ ├── Room.ts │ └── handleEvent.ts ├── services │ ├── MediasoupPeerService.ts │ ├── MessageGroupService.ts │ ├── MessageService.ts │ ├── ParticipantService.ts │ ├── RoomService.ts │ ├── SocketService.ts │ ├── WebRTCRoomService.ts │ └── WorkerService.ts ├── stores │ ├── RoomStore.ts │ ├── SocketStore.ts │ └── WorkerStore.ts ├── types │ ├── EventListener.d.ts │ └── express.d.ts └── validation │ ├── MessageSummaryValidation.ts │ ├── handleProducerAction.ts │ ├── handleUpdateRoomSettings.ts │ └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": false, 4 | "tabWidth": 4, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *Archived as of 4/11/21* Not actively maintained. 2 | 3 | 4 | ![BitLink Logo](assets/logo.svg) 5 | 6 | BitLink is an open source video conference chat program. 7 | 8 | [https://bitlink.live](https://bitlink.live) 9 | 10 | ## Features 11 | 12 | ### No Download Needed 13 | 14 | BitLink can operate completely in the browser on both mobile and desktop devices. 15 | 16 | ### Robust Chat System 17 | 18 | BitLink has a robust integrated chat system. Participants can send messages individually or to the entire room. Participants can edit and delete messages. When a new participant joins, the entire chat history will be synced with their device. 19 | 20 | ### Background Replacement / Blur 21 | 22 | BitLink has built in background replacement (virtual background) support and blur background functionality. Powered by Tensorflow's BodyPix library, we are able to achieve great accuracy while still achieving high performance. Many laptops can apply background replacement without fan spin. 23 | 24 | ### Waiting Room 25 | 26 | BitLink has a waiting room feature. Once the host enables the waiting room, new members will be entered into a waiting room where the host can either accept them into the room or reject them from entering. 27 | 28 | ### Screen Sharing 29 | 30 | Users can share their screen with other users in the room. 31 | 32 | ## Installation 33 | 34 | ```shell script 35 | git clone https://github.com/oss-videochat/bitlink.git` 36 | cd bitlink 37 | npm install 38 | lerna bootstrap 39 | lerna link 40 | lerna run build # may hang, see note below 41 | cd server 42 | MEDIASOUP_LISTEN_IP=; npm run start 43 | ``` 44 | 45 | `lerna run build` should work, but if it doesn't, manually build with 46 | 47 | ```shell script 48 | cd common && npm run build 49 | cd ../frontend && npm run build 50 | cd ../server && npm run build 51 | ``` 52 | 53 | # Usage 54 | 55 | ```shell script 56 | cd server 57 | MEDIASOUP_LISTEN_IP=; npm run start 58 | ``` 59 | 60 | # FireFox Development Issues 61 | 62 | Firefox and Chrome both don't allow WebRTC connections to `127.0.0.1`/`localhost` via UDP. Chrome, however, does allows connecting to `127.0.0.1`/`localhost` via TCP. As such, Chrome should work without issue in development. Firefox on the other hand will likely error with something to the effect of "`ICE failed, add a STUN server and see about:webrtc for more details`". 63 | 64 | To work around this you must use an IP address not in the range of 127.0.0.1 - 127.255.255.255 and pass it to the `MEDIASOUP_LISTEN_IP` environment variable. 65 | 66 | We suggest either using your internal IP address, e.g `MEDIASOUP_LISTEN_IP=192.168.1.197` or aliasing another address to your localhost. On macOS and Linux this can be accomplished by running: 67 | 68 | ```shell script 69 | sudo ifconfig lo0 alias 172.0.0.1 70 | ``` 71 | and then passing the chosen IP to Mediasoup, e.g. `MEDIASOUP_LISTEN_IP=172.0.0.1`. 72 | 73 | **Note**: To reverse and remove the alias run `sudo ifconfig lo0 -alias 172.0.0.1`. 74 | 75 | Regardless of the chosen method, you can still access the site at `localhost`/`127.0.0.1`. 76 | 77 | ## Contributing 78 | 79 | Thank you for helping BitLink grow! Please submit a PR request. 80 | -------------------------------------------------------------------------------- /common/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bitlink/common", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "typescript": { 8 | "version": "3.9.7", 9 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", 10 | "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bitlink/common", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./build/index.js", 6 | "scripts": { 7 | "start": "tsc -w", 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "typescript": "^3.9.7" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/enum/HostDisconnectAction.ts: -------------------------------------------------------------------------------- 1 | export enum HostDisconnectAction { 2 | CLOSE_ROOM, 3 | TRANSFER_HOST, 4 | } 5 | -------------------------------------------------------------------------------- /common/src/enum/MessageType.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | GROUP, 3 | DIRECT, 4 | SYSTEM, 5 | } 6 | -------------------------------------------------------------------------------- /common/src/enum/ParticipantRole.ts: -------------------------------------------------------------------------------- 1 | export enum ParticipantRole { 2 | HOST, // one host per room 3 | MANAGER, // can do everything a host can do except create/remove other managers 4 | MEMBER, 5 | } 6 | -------------------------------------------------------------------------------- /common/src/enum/Reactions.ts: -------------------------------------------------------------------------------- 1 | export enum Reactions { 2 | ThumbsUp, 3 | ThumbsDown, 4 | Laugh, 5 | Confused, 6 | Celebrate, 7 | OneHundred, 8 | QuestionMark, 9 | Clap, 10 | } 11 | -------------------------------------------------------------------------------- /common/src/helper/MediaSourceToTypeMap.ts: -------------------------------------------------------------------------------- 1 | export const MediaSourceToTypeMap = { 2 | camera: "video", 3 | microphone: "audio", 4 | screen: "video", 5 | }; 6 | -------------------------------------------------------------------------------- /common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./enum/ParticipantRole"; 2 | export * from "./enum/Reactions"; 3 | export * from "./enum/MessageType"; 4 | export * from "./enum/HostDisconnectAction"; 5 | 6 | export * from "./interfaces/WebRTC"; 7 | export * from "./interfaces/Summaries"; 8 | export * from "./interfaces/Input"; 9 | export * from "./interfaces/RoomSettings"; 10 | 11 | export * from "./helper/MediaSourceToTypeMap"; 12 | -------------------------------------------------------------------------------- /common/src/interfaces/Input.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from ".."; 2 | 3 | export interface MessageInput { 4 | content: string; 5 | type: MessageType; 6 | } 7 | 8 | export interface DirectMessageInput extends MessageInput { 9 | to: string; 10 | } 11 | 12 | export interface GroupMessageInput extends MessageInput { 13 | group: string; 14 | } 15 | -------------------------------------------------------------------------------- /common/src/interfaces/RoomSettings.ts: -------------------------------------------------------------------------------- 1 | import { HostDisconnectAction } from ".."; 2 | 3 | export interface RoomSettings { 4 | name: string; 5 | waitingRoom: boolean; 6 | hostDisconnectAction: HostDisconnectAction; 7 | } 8 | -------------------------------------------------------------------------------- /common/src/interfaces/Summaries.ts: -------------------------------------------------------------------------------- 1 | import { MediaState, MessageType, ParticipantRole, Reactions } from ".."; 2 | 3 | export interface RoomSummary { 4 | id: string; 5 | idHash: string; 6 | name: string; 7 | myId: string; 8 | participants: ParticipantSummary[]; 9 | messages: MessageSummary[]; 10 | } 11 | 12 | export interface MessageGroupSummary { 13 | id: string; 14 | name: string; 15 | members: string[]; 16 | } 17 | 18 | export interface ParticipantSummary { 19 | id: string; 20 | name: string; 21 | role: ParticipantRole; 22 | isAlive: boolean; 23 | mediaState: MediaState; 24 | } 25 | 26 | export interface MessageSummary { 27 | id: string; 28 | content: string; 29 | created: number; 30 | type: MessageType; 31 | } 32 | 33 | export interface SystemMessageSummary extends MessageSummary { 34 | permission: ParticipantRole; 35 | type: MessageType.SYSTEM; 36 | } 37 | 38 | export interface GroupMessageSummary extends MessageSummary { 39 | type: MessageType.GROUP; 40 | group: string; 41 | from: string; 42 | } 43 | 44 | export interface DirectMessageSummary extends MessageSummary { 45 | type: MessageType.DIRECT; 46 | to: string; 47 | from: string; 48 | } 49 | 50 | export interface ReactionSummary { 51 | type: Reactions; 52 | participant: string; 53 | } 54 | -------------------------------------------------------------------------------- /common/src/interfaces/WebRTC.ts: -------------------------------------------------------------------------------- 1 | export interface MediaState { 2 | camera: boolean; 3 | microphone: boolean; 4 | screen: boolean; 5 | } 6 | 7 | export type MediaSource = keyof MediaState; 8 | export type MediaAction = "pause" | "resume" | "close"; 9 | export type MediaType = "audio" | "video"; 10 | export type TransportType = "webrtc" | "plain"; 11 | export type TransportJob = "sending" | "receiving"; 12 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "outDir": "build", 7 | "downlevelIteration": true, 8 | "declaration": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /build/ 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bitlink/frontend", 3 | "version": "0.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "@bitlink/common": "^0.0.0", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.31", 8 | "@fortawesome/free-solid-svg-icons": "^5.15.0", 9 | "@fortawesome/react-fontawesome": "^0.1.9", 10 | "@tensorflow-models/body-pix": "^2.0.5", 11 | "@tensorflow/tfjs": "^2.4.0", 12 | "@tensorflow/tfjs-backend-webgl": "^2.4.0", 13 | "@testing-library/jest-dom": "^4.2.4", 14 | "@testing-library/react": "^9.3.2", 15 | "@testing-library/user-event": "^7.1.2", 16 | "@types/node": "^12.12.62", 17 | "@types/react": "^16.9.50", 18 | "@types/react-dom": "^16.9.0", 19 | "@types/socket.io-client": "^1.4.34", 20 | "debug": "^4.2.0", 21 | "logrocket": "^1.0.13", 22 | "mediasoup-client": "^3.6.16", 23 | "mobx": "^5.15.7", 24 | "mobx-react": "^6.3.1", 25 | "react": "^16.13.1", 26 | "react-dom": "^16.13.1", 27 | "react-scripts": "^3.4.3", 28 | "socket.io-client": "^2.3.1", 29 | "stackblur-canvas": "^2.4.0", 30 | "typescript": "~3.7.2" 31 | }, 32 | "proxy": "http://localhost:3001", 33 | "scripts": { 34 | "start": "CI=true react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test", 37 | "eject": "react-scripts eject" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/assets/logo/bitlinklogo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oss-videochat/bitlink/1791442e6784ca4d69f78e716a1a66f359e9dc1f/frontend/src/assets/logo/bitlinklogo.ai -------------------------------------------------------------------------------- /frontend/src/components/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | font-size: 15px; 13 | color: #151948; 14 | background: #f9f9f9; 15 | font-family: 'Lato', sans-serif; 16 | overflow: hidden; 17 | } 18 | 19 | #root, .app { 20 | height: 100%; 21 | background: #f9f9f9; 22 | } 23 | 24 | .app { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | 30 | input, textarea { 31 | display: block; 32 | font-family: inherit; 33 | padding: 0; 34 | margin: 0; 35 | outline: none; 36 | border: none; 37 | font-size: 15px; 38 | color: #2b2b2b; 39 | min-width: 0; 40 | } 41 | 42 | .main-container { 43 | display: flex; 44 | flex: 1; 45 | overflow: hidden; 46 | } 47 | 48 | .video-participant-container { 49 | flex: 1; 50 | height: 100%; 51 | display: flex; 52 | } 53 | 54 | .video-container { 55 | flex: 1; 56 | 57 | } 58 | 59 | .legal-text { 60 | display: block; 61 | margin-top: 20px; 62 | color: #a1a1a1; 63 | font-size: 11px; 64 | } 65 | 66 | .legal-text a { 67 | color: #6b71ff; 68 | } 69 | 70 | .legal-text a:visited { 71 | color: #ac78a9; 72 | } 73 | 74 | input[hidden] { 75 | display: none; 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | import { TileContainer } from "./Tiles/TileContainer/TileContainer"; 4 | import Header from "./Header/Header"; 5 | import "./App.css"; 6 | import ChatContainer from "./Chat/ChatContainer"; 7 | import Modal from "./Modals/Modal"; 8 | import UIStore from "../stores/UIStore"; 9 | import NotificationViewer from "./NotificationViewer"; 10 | import msToTime from "../util/msToTime"; 11 | import Footer from "./Footer/Footer"; 12 | import NotificationService from "../services/NotificationService"; 13 | import { NotificationType } from "../enum/NotificationType"; 14 | 15 | const App: React.FunctionComponent = () => { 16 | const appRef = useRef(null); 17 | 18 | const url = new URL(window.location.href); 19 | const parts = url.pathname.split("/").slice(1); 20 | const verb: string = parts[0]; 21 | const data: string = parts[1]; 22 | 23 | if (verb === "join") { 24 | if (data) { 25 | UIStore.store.preFillJoinValue = data; 26 | } 27 | UIStore.store.modalStore.join = true; 28 | } 29 | 30 | if (verb === "create") { 31 | UIStore.store.modalStore.create = true; 32 | } 33 | 34 | if (!verb) { 35 | UIStore.store.modalStore.joinOrCreate = true; 36 | } 37 | document.title = UIStore.store.title; 38 | 39 | useEffect(() => { 40 | setInterval(() => { 41 | if (UIStore.store.joinedDate) { 42 | const time = Date.now() - UIStore.store.joinedDate.getTime(); 43 | document.title = `${UIStore.store.title} | ${msToTime(time)}`; 44 | } else { 45 | document.title = UIStore.store.title; 46 | } 47 | }, 250); 48 | }, []); 49 | 50 | function toggleFullscreen() { 51 | if (!appRef.current) { 52 | return; 53 | } 54 | if (document.fullscreenElement) { 55 | document.exitFullscreen(); 56 | return; 57 | } 58 | appRef.current?.requestFullscreen().catch((err: Error) => { 59 | NotificationService.add( 60 | NotificationService.createUINotification( 61 | "Could not enable fullscreen: " + err.toString(), 62 | NotificationType.Error 63 | ) 64 | ); 65 | }); 66 | } 67 | 68 | return ( 69 |
70 | 71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 |
79 | ); 80 | }; 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ChatContainer.css: -------------------------------------------------------------------------------- 1 | .chat-container { 2 | height: 100%; 3 | width: 0; 4 | transition: width 100ms ease-in-out; 5 | border-top: 1px solid #bbbbbb; 6 | position: relative; 7 | } 8 | 9 | .chat-container--content { 10 | display: flex; 11 | position: absolute; 12 | right: 0; 13 | } 14 | 15 | .chat-container.open { 16 | width: 450px; 17 | border-right: 1px solid #e1e1e1; 18 | } 19 | 20 | .chat-container--content { 21 | height: 100%; 22 | width: 450px; 23 | } 24 | 25 | .message-container { 26 | flex: 1; 27 | } 28 | 29 | @media only screen and (max-width: 600px) { 30 | .chat-container--content { 31 | width: 100%; 32 | } 33 | 34 | .chat-container.open { 35 | width: 100%; 36 | border: none; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ChatContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import ChatRoomList from "./ParticipantList/ChatRoomList"; 4 | import MessagesContainer from "./Messages/MessagesContainer"; 5 | import "./ChatContainer.css"; 6 | import UIStore from "../../stores/UIStore"; 7 | import { MessageType } from "@bitlink/common"; 8 | import { reaction } from "mobx"; 9 | import RoomStore from "../../stores/RoomStore"; 10 | 11 | export interface SelectedRoom { 12 | type: MessageType; 13 | id: string; 14 | } 15 | 16 | const ChatContainer: React.FunctionComponent = () => { 17 | const [selectedRoom, setSelectedRoom] = useState({ 18 | type: MessageType.GROUP, 19 | id: "", 20 | }); 21 | 22 | useEffect(() => 23 | reaction( 24 | () => RoomStore.groups.length, 25 | () => { 26 | // this is pretty bad 27 | if (selectedRoom.id === "") { 28 | setSelectedRoom({ type: MessageType.GROUP, id: RoomStore.groups[0].id }); 29 | } 30 | } 31 | ) 32 | ); 33 | 34 | return useObserver(() => ( 35 |
36 |
37 | 38 | 39 |
40 |
41 | )); 42 | }; 43 | 44 | export default ChatContainer; 45 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ChatInput.css: -------------------------------------------------------------------------------- 1 | .chat-input { 2 | border-top: solid 1px #d8d8d8; 3 | padding: 10px; 4 | position: relative; 5 | } 6 | 7 | .chat-input__input { 8 | border: 1px solid #d8d8d8; 9 | width: 100%; 10 | padding: 7px 7px 7px 10px; 11 | border-radius: 5px; 12 | resize: none; 13 | } 14 | 15 | .mention-container { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | transform: translateY(-100%); 20 | width: 100%; 21 | padding: 10px; 22 | max-width: 100%; 23 | } 24 | 25 | .mention-wrapper { 26 | border: 1px solid #d8d8d8; 27 | width: 100%; 28 | max-width: 100%; 29 | border-radius: 4px; 30 | background: white; 31 | } 32 | 33 | .mention-container__mention::before { 34 | content: '@'; 35 | } 36 | 37 | .mention-container__mention { 38 | padding: 3px 8px; 39 | margin: 2px 0; 40 | max-width: 100%; 41 | display: block; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/MessageComponent.css: -------------------------------------------------------------------------------- 1 | .message { 2 | width: 100%; 3 | position: relative; 4 | padding: 2px 10px; 5 | } 6 | 7 | .message:hover { 8 | background: #dfdfdf; 9 | } 10 | 11 | /* 12 | .message .message--content-container:after { 13 | content: " "; 14 | display: block; 15 | width: 0; 16 | height: 0; 17 | border-style: solid; 18 | border-width: 10px 11px 10px 0; 19 | position: absolute; 20 | right: calc(100% - 1px); 21 | top: 50%; 22 | transform: translateY(-50%); 23 | border-color: transparent #94ffc3 transparent transparent; 24 | } 25 | 26 | .message.from-me .message--content-container:after { 27 | right: unset; 28 | left: calc(100% - 1px); 29 | border-width: 10px 0 11px 10px; 30 | border-color: transparent transparent transparent #a1c6f0; 31 | } 32 | */ 33 | 34 | .message--meta { 35 | display: flex; 36 | align-items: center; 37 | } 38 | 39 | .message--name { 40 | font-weight: 700; 41 | white-space: nowrap; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | display: block; 45 | flex: 1; 46 | font-size: 16px; 47 | } 48 | 49 | .message--date { 50 | margin-left: 10px; 51 | font-size: 12px; 52 | color: #525252; 53 | } 54 | 55 | .message--content-container { 56 | /* background: #94ffc3;*/ 57 | border-radius: 15px; 58 | position: relative; 59 | } 60 | 61 | .message.from-me .message--content-container { 62 | /* background: #a1c6f0;*/ 63 | } 64 | 65 | .message--content { 66 | white-space: pre-wrap; 67 | width: 100%; 68 | display: block; 69 | word-break: break-word; 70 | line-height: 18px; 71 | } 72 | 73 | .message--options-container { 74 | display: none; 75 | position: absolute; 76 | top: 0; 77 | right: 0; 78 | margin-right: 10px; 79 | transform: translateY(-50%); 80 | z-index: 100; 81 | border: 1px solid #bbbbbb; 82 | background: #f9f9f9; 83 | border-radius: 7px; 84 | font-size: 14px; 85 | } 86 | 87 | .message--option { 88 | padding: 8px 12px; 89 | } 90 | 91 | .message--options-container .message--option:first-child { 92 | border-top-left-radius: 7px; 93 | border-bottom-left-radius: 7px; 94 | } 95 | 96 | .message--options-container .message--option:last-child { 97 | border-top-right-radius: 7px; 98 | border-bottom-right-radius: 7px; 99 | } 100 | 101 | .message--option:hover { 102 | background: #eeeeee; 103 | } 104 | 105 | .message:hover .message--options-container { 106 | display: flex; 107 | } 108 | 109 | .message--content--edit-input { 110 | border: 1px solid #d8d8d8; 111 | width: 100%; 112 | padding: 7px 7px 7px 10px; 113 | border-radius: 5px; 114 | resize: none; 115 | } 116 | 117 | .message--content-edit-cancel { 118 | font-size: 12px; 119 | color: #423fc0; 120 | cursor: pointer; 121 | user-select: none; 122 | -webkit-user-select: none; 123 | } 124 | 125 | .message--content-edit-cancel:hover { 126 | color: #4c49ff; 127 | } 128 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/MessageContent.css: -------------------------------------------------------------------------------- 1 | .participant-mention { 2 | display: inline-block; 3 | padding: 2px 6px; 4 | color: rgb(73, 114, 255); 5 | background: rgba(134, 151, 255, 0.5); 6 | border-radius: 5px; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/MessageContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./MessageContent.css"; 3 | import ParticipantService from "../../../services/ParticipantService"; 4 | 5 | interface IMessageContentProps { 6 | content: string; 7 | } 8 | 9 | type matcherArray = [ 10 | RegExp, 11 | ( 12 | before: string | undefined, 13 | match: string[], 14 | after: string | undefined 15 | ) => React.ReactElement | React.ReactElement[] | void 16 | ][]; 17 | 18 | const MessageContent: React.FunctionComponent = ({ content }) => { 19 | let id = 0; 20 | 21 | const matchers: matcherArray = [ 22 | [ 23 | /https?:\/\/[^\s]+/g, 24 | (_, match) => { 25 | return ( 26 | 27 | {match[0]} 28 | 29 | ); 30 | }, 31 | ], 32 | [ 33 | /@[^\s]+/g, 34 | (_, match) => { 35 | const participants = ParticipantService.getByMentionString(match[0]); 36 | if (participants[0]) { 37 | return ( 38 | 39 | @{participants[0].mentionString} 40 | 41 | ); 42 | } 43 | return {match[0]}; 44 | }, 45 | ], 46 | ]; 47 | 48 | function parse(text: string, matcherArray: matcherArray) { 49 | if (text.length === 0) { 50 | return; 51 | } 52 | if (matcherArray.length === 0) { 53 | return [{text}]; 54 | } 55 | 56 | const arr: React.ReactElement[] = []; 57 | 58 | function addValue(val: React.ReactElement | React.ReactElement[] | undefined | void) { 59 | // this just allows us to return anything from an array of elements to a single element to no elements 60 | if (val) { 61 | if (Array.isArray(val)) { 62 | arr.push(...val); 63 | } else { 64 | arr.push(val); 65 | } 66 | } 67 | } 68 | 69 | const [regExp, callback] = matcherArray[0]; 70 | const matches = Array.from(text.matchAll(regExp)); 71 | const splits = text.split(regExp); 72 | splits.forEach((split: string, index: number) => { 73 | addValue(parse(split, matcherArray.slice(1))); // this text obv didn't match the first one so check the next one 74 | if (matches[index]) { 75 | addValue(callback(split, matches[index], splits[index + 1])); // run the callback passing the before string, match array, and the after string 76 | } 77 | }); 78 | return arr; 79 | } 80 | 81 | return ( 82 | 83 | {parse(content, matchers)!} 84 | 85 | ); 86 | }; 87 | export default MessageContent; 88 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/MessagesContainer.css: -------------------------------------------------------------------------------- 1 | .message-container { 2 | display: flex; 3 | flex-direction: column; 4 | min-width: 0; 5 | } 6 | 7 | .message-list-wrapper { 8 | flex: 1; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | .message-list { 14 | max-height: 100%; 15 | width: 100%; 16 | overflow: auto; 17 | } 18 | 19 | .message-container--top-bar { 20 | display: none; 21 | height: 40px; 22 | background: #fffefe; 23 | border-bottom: 1px solid #d8d8d8; 24 | align-items: center; 25 | position: relative; 26 | } 27 | 28 | .message-container--participant-name { 29 | display: block; 30 | text-align: center; 31 | flex: 1; 32 | margin-right: 30px; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | } 36 | 37 | .message-container--back-button { 38 | height: 25px; 39 | width: 25px; 40 | border-radius: 50%; 41 | text-align: center; 42 | line-height: 25px; 43 | margin: 0 10px; 44 | } 45 | 46 | 47 | @media screen and (max-width: 600px) { 48 | .message-list { 49 | position: absolute; 50 | bottom: 0; 51 | } 52 | 53 | .message-container--top-bar { 54 | display: flex; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/ReactionsDisplayer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { Reactions } from "@bitlink/common"; 4 | import { Reaction } from "../../../interfaces/Messages"; 5 | 6 | interface ReactionArrayObj { 7 | [key: string]: Array; 8 | } 9 | 10 | @observer 11 | export class ReactionsDisplayer extends React.Component { 12 | private reactionsArrays: ReactionArrayObj = {}; 13 | 14 | constructor(props: any) { 15 | super(props); 16 | Object.values(Reactions).forEach((value) => { 17 | this.reactionsArrays[value] = []; 18 | }); 19 | this.props.reactions.forEach((reaction: Reaction) => { 20 | this.reactionsArrays[reaction.type].push(reaction); 21 | }); 22 | } 23 | 24 | render() { 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/SystemMessage.css: -------------------------------------------------------------------------------- 1 | .message.system .message--content-container { 2 | text-align: center; 3 | font-size: 13px; 4 | color: gray; 5 | font-style: italic; 6 | } 7 | 8 | .message.system:hover { 9 | background: unset; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/Messages/SystemMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./SystemMessage.css"; 3 | import { Message } from "../../../interfaces/Message"; 4 | 5 | interface ISystemMessageProps { 6 | message: Message; 7 | } 8 | 9 | const SystemMessage: React.FunctionComponent = ({ message }) => ( 10 |
11 |
12 | {new Date(message.created).toLocaleString()} 13 | 14 | {message.content} 15 | 16 |
17 |
18 | ); 19 | export default SystemMessage; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/ChatParticipant.css: -------------------------------------------------------------------------------- 1 | .chat-participant { 2 | height: 50px; 3 | padding: 10px; 4 | padding-top: 5px; 5 | font-weight: 500; 6 | color: #5c8dff; 7 | font-size: 15px; 8 | user-select: none; 9 | -webkit-user-select: none; 10 | cursor: pointer; 11 | border-bottom: #e4e4e4 1px solid; 12 | } 13 | 14 | .chat-participant.selected { 15 | background: #e1e1e1; 16 | } 17 | 18 | .chat-participant:hover { 19 | background: #e1e1e1; 20 | } 21 | 22 | .chat-participant:hover .chat-participant--name { 23 | color: #556bd6; 24 | } 25 | 26 | .chat-participant--content { 27 | font-size: 15px; 28 | color: #999999; 29 | width: 100%; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | overflow: hidden; 33 | display: block; 34 | } 35 | 36 | .chat-participant-name-container { 37 | display: flex; 38 | } 39 | 40 | .chat-participant--name { 41 | flex: 1; 42 | white-space: nowrap; 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | } 46 | 47 | .participant--icon { 48 | margin: 0 5px; 49 | color: #ff5f66; 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/ChatParticipant.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import "./ChatParticipant.css"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faMicrophoneSlash, faVideoSlash } from "@fortawesome/free-solid-svg-icons"; 6 | import Participant from "../../../models/Participant"; 7 | import { MessageType } from "@bitlink/common"; 8 | import { SelectedRoom } from "../ChatContainer"; 9 | import { MessageGroup } from "../../../interfaces/MessageGroup"; 10 | import { Message } from "../../../interfaces/Message"; 11 | 12 | interface IChatParticipantProps { 13 | onChosen: (info: SelectedRoom) => void; 14 | selected: boolean; 15 | type: MessageType; 16 | item: Participant | MessageGroup; 17 | lastMessage?: Message; 18 | } 19 | 20 | const ChatParticipant: React.FunctionComponent = ({ 21 | onChosen, 22 | selected, 23 | type, 24 | item, 25 | lastMessage, 26 | }) => { 27 | return useObserver(() => { 28 | let name; 29 | let id: string; 30 | let displayMediaState; 31 | let hasAudio; 32 | let hasVideo; 33 | 34 | if (type === MessageType.DIRECT) { 35 | const participant = item as Participant; 36 | name = participant.info.name; 37 | id = participant.info.id; 38 | displayMediaState = participant.info.isAlive; 39 | hasAudio = participant.hasAudio; 40 | hasVideo = participant.hasVideo; 41 | } else { 42 | const group = item as MessageGroup; 43 | name = group.name; 44 | id = group.id; 45 | displayMediaState = false; 46 | hasAudio = false; 47 | hasVideo = false; 48 | } 49 | 50 | return ( 51 |
onChosen({ type, id })} 53 | className={"chat-participant " + (selected ? "selected" : "")} 54 | > 55 |
56 | 57 | {name} 58 | 59 | {displayMediaState ? ( 60 |
61 | 62 | {hasAudio ? null : } 63 | 64 | 65 | {hasVideo ? null : } 66 | 67 |
68 | ) : null} 69 |
70 | {lastMessage && ( 71 | 72 | {lastMessage.content} 73 | 74 | )} 75 |
76 | ); 77 | }); 78 | }; 79 | export default ChatParticipant; 80 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/ChatRoomList.css: -------------------------------------------------------------------------------- 1 | .chat-participant-wrapper { 2 | border-right: 1px solid #e4e4e4; 3 | height: 100%; 4 | width: 0; 5 | min-width: 0; 6 | max-width: 0; 7 | transition: width 200ms ease-in-out, min-width 200ms ease-in-out, max-width 200ms ease-in-out; 8 | overflow: hidden; 9 | } 10 | 11 | .chat-participant-wrapper.open { 12 | min-width: 200px; 13 | width: 25%; 14 | max-width: 300px; 15 | } 16 | 17 | .chat-participant-wrapper--content { 18 | width: 100%; 19 | overflow: auto; 20 | max-height: 100%; 21 | } 22 | 23 | .chat-participant-list { 24 | overflow: auto; 25 | } 26 | 27 | @media only screen and (max-width: 600px) { 28 | .chat-participant-wrapper { 29 | border: none; 30 | } 31 | 32 | .chat-participant-wrapper.open { 33 | min-width: 100%; 34 | width: 100%; 35 | max-width: 100%; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/SearchBar.css: -------------------------------------------------------------------------------- 1 | .search-wrapper { 2 | width: 100%; 3 | padding: 10px; 4 | border-bottom: #e4e4e4 1px solid; 5 | } 6 | 7 | .search-container { 8 | display: flex; 9 | background: white; 10 | border: 1px solid #d8d8d8; 11 | border-radius: 25px; 12 | padding: 5px 10px; 13 | } 14 | 15 | .search--icon { 16 | display: block; 17 | color: #2b2b2b; 18 | margin-right: 5px; 19 | } 20 | 21 | .search--input { 22 | flex: 1; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, useState } from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faSearch } from "@fortawesome/free-solid-svg-icons"; 4 | import "./SearchBar.css"; 5 | 6 | interface ISearchBarProps { 7 | onChange: (input: string) => void; 8 | } 9 | 10 | const SearchBar: React.FunctionComponent = ({ onChange }) => { 11 | const [value, setValue] = useState(""); 12 | 13 | function handleKeyUp(event: ChangeEvent) { 14 | setValue(event.target.value); 15 | onChange(event.target.value); 16 | } 17 | 18 | return ( 19 |
20 |
21 | 22 | 23 | 24 | 31 |
32 |
33 | ); 34 | }; 35 | export default SearchBar; 36 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/WaitingRoom/WaitingRoomList.css: -------------------------------------------------------------------------------- 1 | .waiting-room-list { 2 | border-bottom: 2px solid #bbbbbb;; 3 | } 4 | 5 | .waiting-room--title { 6 | font-weight: 600; 7 | display: block; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/WaitingRoom/WaitingRoomList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import "./WaitingRoomList.css"; 4 | import ParticipantsStore from "../../../../stores/ParticipantsStore"; 5 | import WaitingRoomListParticipant from "./WaitingRoomListParticipant"; 6 | 7 | const WaitingRoomList: React.FunctionComponent = () => 8 | useObserver(() => ( 9 |
10 | Waiting Room 11 | {ParticipantsStore.waitingRoom.map((patientParticipant) => ( 12 | 16 | ))} 17 |
18 | )); 19 | export default WaitingRoomList; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/WaitingRoom/WaitingRoomListParticipant.css: -------------------------------------------------------------------------------- 1 | .waiting-room-participant { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | font-size: 15px; 6 | height: 50px; 7 | user-select: none; 8 | -webkit-user-select: none; 9 | cursor: pointer; 10 | border-bottom: #e4e4e4 2px solid; 11 | padding: 10px 0 10px 10px; 12 | } 13 | 14 | 15 | .waiting-room-participant--name { 16 | flex: 1; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | white-space: nowrap; 20 | } 21 | 22 | .waiting-room-participant--name:hover { 23 | overflow: visible; 24 | background: #f9f9f9; 25 | position: absolute; 26 | left: 0; 27 | } 28 | 29 | .waiting-room-participant--decision-container { 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .waiting-room-participant--decision-button { 35 | background: #4c5eec; 36 | color: #fbfbfb; 37 | margin: 2px 5px; 38 | font-size: 10px; 39 | cursor: pointer; 40 | user-select: none; 41 | -webkit-user-select: none; 42 | border-radius: 15px; 43 | padding: 2px 10px; 44 | } 45 | 46 | .waiting-room-participant--decision-button:hover { 47 | background: #6375ff; 48 | } 49 | 50 | .waiting-room-participant--decision-button.reject { 51 | background: #ff5f4a; 52 | } 53 | 54 | .waiting-room-participant--decision-button.reject:hover { 55 | background: #ff8a72; 56 | } 57 | 58 | .waiting-room-participant--decision-button option { 59 | background: unset; 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/Chat/ParticipantList/WaitingRoom/WaitingRoomListParticipant.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./WaitingRoomListParticipant.css"; 3 | import IO from "../../../../controllers/IO"; 4 | import Participant from "../../../../models/Participant"; 5 | import { useObserver } from "mobx-react"; 6 | 7 | interface IWaitingRoomListParticipantProps { 8 | participant: Participant; 9 | } 10 | 11 | const WaitingRoomListParticipant: React.FunctionComponent = ({ 12 | participant, 13 | }) => 14 | useObserver(() => ( 15 |
16 | 17 | {participant.info.name} 18 | 19 |
20 | IO.waitingRoomDecision(participant.info.id, true)} 22 | type={"button"} 23 | className={"waiting-room-participant--decision-button"} 24 | value={"Accept"} 25 | /> 26 | IO.waitingRoomDecision(participant.info.id, false)} 28 | type={"button"} 29 | className={"waiting-room-participant--decision-button reject"} 30 | value={"Reject"} 31 | /> 32 |
33 |
34 | )); 35 | export default WaitingRoomListParticipant; 36 | -------------------------------------------------------------------------------- /frontend/src/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: none; 3 | width: 100%; 4 | height: 65px; 5 | background: #4c5eec; 6 | } 7 | 8 | .footer ul { 9 | padding: 0; 10 | list-style: none; 11 | display: flex; 12 | color: #fbfbfb; 13 | font-size: 13px; 14 | text-transform: uppercase; 15 | justify-content: space-between; 16 | margin: 0; 17 | height: 100%; 18 | } 19 | 20 | .footer ul li { 21 | display: flex; 22 | flex-direction: column; 23 | text-align: center; 24 | width: 100%; 25 | padding: 10px; 26 | } 27 | 28 | .footer ul li.selected { 29 | background: #3842a3; 30 | } 31 | 32 | .footer .footer--icon { 33 | font-size: 20px; 34 | } 35 | 36 | .footer .footer--text { 37 | font-size: 10px; 38 | } 39 | 40 | @media only screen and (max-width: 600px) { 41 | .footer { 42 | display: block; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Footer.css"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faCogs, faComments, faVideo } from "@fortawesome/free-solid-svg-icons"; 5 | import UIStore from "../../stores/UIStore"; 6 | import { useObserver } from "mobx-react"; 7 | 8 | const Footer: React.FunctionComponent = () => 9 | useObserver(() => ( 10 | 41 | )); 42 | 43 | export default Footer; 44 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: linear-gradient(to right bottom, #5958ff, #4744d5); 3 | height: 50px; 4 | width: 100%; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding: 0 10px; 9 | color: #fbfbfb; 10 | } 11 | 12 | .header--nav { 13 | display: flex; 14 | align-items: center; 15 | height: 100%; 16 | } 17 | 18 | .header ul { 19 | margin: 0; 20 | padding: 0; 21 | list-style: none; 22 | display: flex; 23 | color: #fbfbfb; 24 | font-size: 13px; 25 | text-transform: uppercase; 26 | } 27 | 28 | .header .divider { 29 | display: inline-block; 30 | height: 60%; 31 | width: 1px; 32 | background: #c1c1c1; 33 | opacity: 0.3; 34 | } 35 | 36 | .header ul li { 37 | margin: 0 10px; 38 | cursor: pointer; 39 | user-select: none; 40 | -webkit-user-select: none; 41 | font-weight: 700; 42 | /* background: #00d79a;*/ 43 | background: #3a3a3a; 44 | border-radius: 15px; 45 | padding: 5px 10px; 46 | color: #f1f1f1; 47 | white-space: nowrap; 48 | } 49 | 50 | .header ul li:hover { 51 | /* background: #00c386;*/ 52 | background: #2b2b2b; 53 | color: #f8f8f8; 54 | } 55 | 56 | .header--room-info { 57 | display: flex; 58 | align-items: center; 59 | overflow: hidden; 60 | } 61 | 62 | .room-info--name { 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | } 67 | 68 | header ul li.leave-button { 69 | background: #e74848; 70 | } 71 | 72 | header ul li.leave-button:hover { 73 | background: #a33636; 74 | } 75 | 76 | @media only screen and (max-width: 600px) { 77 | .header { 78 | height: 60px; 79 | } 80 | 81 | .header--nav { 82 | display: none; 83 | } 84 | 85 | .header--room-info { 86 | text-align: center; 87 | max-width: 100%; 88 | width: 100%; 89 | overflow: hidden; 90 | white-space: nowrap; 91 | text-overflow: ellipsis; 92 | } 93 | 94 | .room-info--name { 95 | display: block; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import "./Header.css"; 4 | import RoomStore from "../../stores/RoomStore"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faCogs, faComments, faExpand, faUsers } from "@fortawesome/free-solid-svg-icons"; 7 | import UIStore from "../../stores/UIStore"; 8 | import RoomId from "./RoomId"; 9 | import MyInfo from "../../stores/MyInfoStore"; 10 | import UIStoreService from "../../services/UIStoreService"; 11 | 12 | interface IHeaderProps { 13 | toggleFullscreen: () => void; 14 | } 15 | 16 | const Header: React.FunctionComponent = ({ toggleFullscreen }) => { 17 | return useObserver(() => { 18 | return ( 19 |
20 |
21 | {RoomStore.info ? ( 22 | 23 | 24 | {RoomStore.info.name} 25 | 26 | 27 | 28 | ) : null} 29 |
30 | 58 |
59 | ); 60 | }); 61 | }; 62 | export default Header; 63 | -------------------------------------------------------------------------------- /frontend/src/components/Header/RoomId.css: -------------------------------------------------------------------------------- 1 | .room-info--id-wrapper { 2 | display: inline-flex; 3 | margin-left: 10px; 4 | padding: 6px 10px; 5 | background: #2a3365; 6 | border-radius: 15px; 7 | cursor: pointer; 8 | user-select: none; 9 | -webkit-user-select: none; 10 | } 11 | 12 | .room-info--id-wrapper:hover { 13 | background: #232449; 14 | } 15 | 16 | .room-info--id { 17 | display: inline-block; 18 | margin-right: 10px; 19 | } 20 | 21 | .room-info--share-icon { 22 | vertical-align: top; 23 | font-size: 12px; 24 | } 25 | 26 | @media only screen and (max-width: 600px) { 27 | .room-info--id-wrapper { 28 | display: block; 29 | margin-top: 2px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/LegalText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LegalText: React.FunctionComponent = () => { 4 | return ( 5 | 6 | By using our service you agree to our{" "} 7 | Privacy Policy 8 | 9 | ); 10 | }; 11 | 12 | export default LegalText; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Dialog.css: -------------------------------------------------------------------------------- 1 | .dialog-modal { 2 | background: #f9f9f9; 3 | color: #353535; 4 | padding: 25px; 5 | border-radius: 10px; 6 | width: 500px; 7 | max-width: 100%; 8 | margin: 30px; 9 | overflow: auto; 10 | max-height: 100%; 11 | } 12 | 13 | @media screen and (max-width: 600px) { 14 | .dialog-modal { 15 | margin: 10px; 16 | } 17 | } 18 | 19 | .modal--title { 20 | font-size: 30px; 21 | font-weight: 700; 22 | text-align: center; 23 | } 24 | 25 | .modal--input { 26 | border: 1px solid #d8d8d8; 27 | margin: 10px 0; 28 | width: 100%; 29 | border-radius: 6px; 30 | padding: 5px 10px; 31 | } 32 | 33 | .modal--input[type="checkbox"] { 34 | display: inline-block; 35 | width: auto; 36 | margin-right: 10px; 37 | } 38 | 39 | .modal--input.invalid { 40 | border: 1px solid #d8896f; 41 | } 42 | 43 | .modal--button-container { 44 | display: flex; 45 | justify-content: space-between; 46 | margin: 20px 0 10px; 47 | } 48 | 49 | .modal--button { 50 | font-weight: 700; 51 | background: #3a3a3a; 52 | border-radius: 15px; 53 | padding: 6px 15px; 54 | font-size: 16px; 55 | color: #f1f1f1; 56 | -webkit-appearance: none; 57 | } 58 | 59 | .modal--button:not(:disabled):hover { 60 | background: #2b2b2b; 61 | color: #f8f8f8; 62 | } 63 | 64 | .modal--button:disabled { 65 | background: #bebebe; 66 | color: #fbfbfb; 67 | } 68 | 69 | .spinner-wrapper { 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | margin: 20px; 74 | } 75 | 76 | .dialog-centered-text { 77 | display: block; 78 | text-align: center; 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/JoinOrCreate.css: -------------------------------------------------------------------------------- 1 | .join-or-create-button { 2 | width: 100%; 3 | max-width: 300px; 4 | margin: 10px auto; 5 | background: #5958ff; 6 | } 7 | 8 | .modal--button.join-or-create-button:hover { 9 | background: #4744d5; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/JoinOrCreate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Dialog.css"; 3 | import "./JoinOrCreate.css"; 4 | import UIStore from "../../stores/UIStore"; 5 | import LegalText from "../LegalText"; 6 | import { Logo } from "../Util/Logo"; 7 | 8 | const JoinOrCreate: React.FunctionComponent = () => { 9 | function handleClick(chosen: string) { 10 | UIStore.store.modalStore.joinOrCreate = false; 11 | if (chosen === "join") { 12 | UIStore.store.modalStore.join = true; 13 | } 14 | if (chosen === "create") { 15 | UIStore.store.modalStore.create = true; 16 | } 17 | } 18 | 19 | return ( 20 |
21 | 22 |

Join or Create a Room

23 | handleClick("join")} 25 | type={"button"} 26 | value={"Join Room"} 27 | className={"modal--button join-or-create-button"} 28 | /> 29 | handleClick("create")} 31 | type={"button"} 32 | value={"Create Room"} 33 | className={"modal--button join-or-create-button"} 34 | /> 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default JoinOrCreate; 41 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Joining.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Dialog.css"; 3 | import Spinner from "../Util/Spinner"; 4 | 5 | const Joining: React.FunctionComponent = () => ( 6 |
7 |

Joining Room

8 | Please wait... 9 |
10 | 11 |
12 |
13 | ); 14 | export default Joining; 15 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/LeaveDialog.css: -------------------------------------------------------------------------------- 1 | .leave-button { 2 | width: 100%; 3 | max-width: 300px; 4 | margin: 10px auto; 5 | } 6 | 7 | .leave-button-cancel { 8 | margin-top: 20px; 9 | background: #d04e53; 10 | } 11 | 12 | .modal--button.leave-button-cancel:hover { 13 | background: #e6545a; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/LeaveDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./Dialog.css"; 3 | import "./LeaveDialog.css"; 4 | import UIStore from "../../stores/UIStore"; 5 | import IO from "../../controllers/IO"; 6 | import ParticipantList from "../Util/ParticipantList"; 7 | import ParticipantService from "../../services/ParticipantService"; 8 | 9 | const LeaveDialog: React.FunctionComponent = () => { 10 | const [transferHostOpen, setTransferHostOpen] = useState(false); 11 | const isThereAnotherHost = 12 | ParticipantService.getLiving(true).filter((participant) => participant.isHost).length > 0; 13 | 14 | return ( 15 |
16 |

Leave Room

17 | {isThereAnotherHost ? ( 18 | <> 19 | 20 | Please choose to leave or end the room for all participants 21 | 22 | IO._leave()} 24 | type={"button"} 25 | value={"Leave"} 26 | className={"modal--button leave-button"} 27 | /> 28 | 29 | ) : ( 30 | <> 31 | 32 | Please choose to transfer the host to another participant or end the room 33 | for all participants 34 | 35 | setTransferHostOpen(true)} 38 | value={"Transfer Host"} 39 | className={"modal--button leave-button"} 40 | /> 41 | 42 | )} 43 | {transferHostOpen && ( 44 | { 46 | IO._leave(); 47 | UIStore.store.modalStore.leaveMenu = false; 48 | }} 49 | /> 50 | )} 51 | { 54 | IO.endRoomForAll().catch(console.error); 55 | UIStore.store.modalStore.leaveMenu = false; 56 | }} 57 | value={"End Room for All"} 58 | className={"modal--button leave-button"} 59 | /> 60 | (UIStore.store.modalStore.leaveMenu = false)} 63 | value={"Cancel"} 64 | className={"modal--button leave-button-cancel"} 65 | /> 66 |
67 | ); 68 | }; 69 | export default LeaveDialog; 70 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Modal.css: -------------------------------------------------------------------------------- 1 | .modal-wrapper { 2 | position: fixed; 3 | z-index: 50; 4 | left: 0; 5 | top: 0; 6 | background: rgba(0, 0, 0, 0.8); 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import "./Modal.css"; 4 | import CreateDialog from "./CreateDialog"; 5 | import UIStore from "../../stores/UIStore"; 6 | import JoinDialog from "./JoinDialog"; 7 | import JoinOrCreate from "./JoinOrCreate"; 8 | import Joining from "./Joining"; 9 | import WaitingRoom from "./WaitingRoom"; 10 | import { SettingsModal } from "./Settings/SettingsModal"; 11 | import LeaveDialog from "./LeaveDialog"; 12 | 13 | const Modal: React.FunctionComponent = () => { 14 | function getModalElements(): ReactNode | null { 15 | const modalStore = UIStore.store.modalStore; 16 | if (modalStore.joinOrCreate) { 17 | return ; 18 | } 19 | if (modalStore.join) { 20 | return ; 21 | } 22 | if (modalStore.joiningRoom) { 23 | return ; 24 | } 25 | if (modalStore.waitingRoom) { 26 | return ; 27 | } 28 | if (modalStore.create) { 29 | return ; 30 | } 31 | if (modalStore.settings) { 32 | return ; 33 | } 34 | if (modalStore.leaveMenu) { 35 | return ; 36 | } 37 | return null; 38 | } 39 | 40 | return useObserver(() => { 41 | const modal = getModalElements(); 42 | if (modal) { 43 | return
{modal}
; 44 | } 45 | return null; 46 | }); 47 | }; 48 | 49 | export default Modal; 50 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsModal.css: -------------------------------------------------------------------------------- 1 | .settings-modal { 2 | display: flex; 3 | padding: 0; 4 | width: 700px; 5 | height: 450px; 6 | max-height: 100vh; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import SettingsPanel from "./SettingsPanel"; 3 | import "./SettingsModal.css"; 4 | import SettingsViewer from "./SettingsViewer"; 5 | import { SettingsPanels } from "../../../enum/SettingsPanels"; 6 | import UIStore from "../../../stores/UIStore"; 7 | import * as Events from "events"; 8 | import MyInfo from "../../../stores/MyInfoStore"; 9 | import NotificationService from "../../../services/NotificationService"; 10 | import { NotificationType } from "../../../enum/NotificationType"; 11 | 12 | interface SettingsModalState { 13 | selectedPanel: SettingsPanels; 14 | saveEnabled: boolean; 15 | changesMade: boolean; 16 | } 17 | 18 | export const SettingsModal: React.FunctionComponent = () => { 19 | const [selectedPanel, setSelectedPanel] = useState( 20 | MyInfo.isHost ? SettingsPanels.RoomSettings : SettingsPanels.Participants 21 | ); 22 | const [saveEnabled, setSaveEnabled] = useState(false); 23 | const [changesMade, setChangesMade] = useState(false); 24 | const events = useRef(new Events.EventEmitter()); 25 | 26 | function handleChangesMade(isChangesMade: boolean) { 27 | setChangesMade(isChangesMade); 28 | } 29 | 30 | function handleSelect(settings: SettingsPanels) { 31 | setSelectedPanel(settings); 32 | setSaveEnabled(false); 33 | } 34 | 35 | function handleSave() { 36 | setSaveEnabled(false); 37 | events.current.emit("save", (err?: string) => { 38 | if (err) { 39 | NotificationService.add( 40 | NotificationService.createUINotification(err, NotificationType.Error) 41 | ); 42 | return; 43 | } 44 | NotificationService.add( 45 | NotificationService.createUINotification( 46 | "Settings Saved!", 47 | NotificationType.Success 48 | ) 49 | ); 50 | UIStore.store.modalStore.settings = false; 51 | }); 52 | } 53 | 54 | function handleCancel() { 55 | UIStore.store.modalStore.settings = false; 56 | } 57 | 58 | return ( 59 |
60 | 61 | 69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsPanel.css: -------------------------------------------------------------------------------- 1 | .settings-panel { 2 | flex-basis: 100px; 3 | display: flex; 4 | flex-direction: column; 5 | background: #262835; 6 | align-items: center; 7 | overflow: auto; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./SettingsPanel.css"; 3 | import MyInfo from "../../../stores/MyInfoStore"; 4 | import { SettingsPanels } from "../../../enum/SettingsPanels"; 5 | import SettingsPanelItem from "./SettingsPanelItem"; 6 | import { 7 | faDoorClosed, 8 | faExclamationCircle, 9 | faUserCog, 10 | faUsers, 11 | faVideo, 12 | } from "@fortawesome/free-solid-svg-icons"; 13 | import { useObserver } from "mobx-react"; 14 | 15 | interface ISettingsPanelProps { 16 | selected: SettingsPanels; 17 | onSelect: (type: SettingsPanels) => void; 18 | } 19 | 20 | const SettingsPanel: React.FunctionComponent = ({ onSelect, selected }) => 21 | useObserver(() => ( 22 |
23 | {MyInfo.isHost && ( 24 | 31 | )} 32 | 39 | 46 | 53 | 60 |
61 | )); 62 | export default SettingsPanel; 63 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsPanelItem.css: -------------------------------------------------------------------------------- 1 | .settings--item { 2 | color: #8d9db2; 3 | width: 100%; 4 | padding: 30px 10px; 5 | text-align: center; 6 | border-bottom: 2px solid #0c0c11; 7 | } 8 | 9 | .settings--item:hover { 10 | color: white; 11 | background: #303444; 12 | } 13 | 14 | .settings--item.selected { 15 | color: white; 16 | background: #14171f; 17 | border-right: none; 18 | } 19 | 20 | 21 | .settings--item:last-child { 22 | border: none; 23 | } 24 | 25 | .settings--item--icon { 26 | font-size: 25px; 27 | display: block; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsPanelItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./SettingsPanelItem.css"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { SettingsPanels } from "../../../enum/SettingsPanels"; 5 | import { IconProp } from "@fortawesome/fontawesome-svg-core"; 6 | 7 | interface ISettingsPanelItemProps { 8 | panel: SettingsPanels; 9 | onSelect: (panel: SettingsPanels) => void; 10 | selected: SettingsPanels; 11 | icon: IconProp; 12 | text: string; 13 | } 14 | 15 | const SettingsPanelItem: React.FunctionComponent = ({ 16 | panel, 17 | selected, 18 | onSelect, 19 | icon, 20 | text, 21 | }) => ( 22 |
onSelect(panel)} 24 | className={"settings--item " + (selected === panel ? "selected" : "")} 25 | > 26 | 27 | 28 | 29 | {text} 30 |
31 | ); 32 | export default SettingsPanelItem; 33 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsViewer.css: -------------------------------------------------------------------------------- 1 | .settings-viewer { 2 | flex: 1; 3 | padding: 10px 20px; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .settings-view { 9 | flex: 1; 10 | display: flex; 11 | flex-direction: column; 12 | overflow: auto; 13 | } 14 | 15 | .settings-view.loading { 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .settings--button-control { 22 | display: flex; 23 | justify-content: flex-end; 24 | } 25 | 26 | .settings--button-control .modal--button { 27 | margin: 0 10px; 28 | cursor: pointer; 29 | user-select: none; 30 | -webkit-user-select: none; 31 | } 32 | 33 | 34 | .settings--button-control .modal--button.cancel { 35 | background: #ff5f66; 36 | } 37 | 38 | .settings--button-control .modal--button.cancel:hover { 39 | background: #d04e53; 40 | } 41 | 42 | .settings-view h3 { 43 | margin: 10px 0; 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsViewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./SettingsViewer.css"; 3 | import { SettingsPanels } from "../../../enum/SettingsPanels"; 4 | import MySettings from "./SettingsViews/MySettings"; 5 | import { RoomSettingsSettings } from "./SettingsViews/RoomSettingsSettings"; 6 | import Participants from "./SettingsViews/Participants"; 7 | import Report from "./SettingsViews/Report"; 8 | import * as Events from "events"; 9 | import VideoEffects from "./SettingsViews/VideoEffects"; 10 | 11 | interface ISettingsViewerProps { 12 | selected: SettingsPanels; 13 | events: Events.EventEmitter; 14 | cancel: () => void; 15 | save: () => void; 16 | changesMade: boolean; 17 | handleChangesMade: (isChangesMade: boolean) => void; 18 | } 19 | 20 | export interface ISettingsPanelProps { 21 | handleChangesMade: (isChangesMade: boolean) => void; 22 | changesMade: boolean; 23 | events: Events.EventEmitter; 24 | } 25 | 26 | const SettingsViewer: React.FunctionComponent = ({ 27 | selected, 28 | events, 29 | cancel, 30 | save, 31 | changesMade, 32 | handleChangesMade, 33 | }) => { 34 | function getView(): 35 | | React.ComponentClass 36 | | React.FunctionComponent { 37 | switch (selected) { 38 | case SettingsPanels.MySettings: 39 | return MySettings; 40 | case SettingsPanels.RoomSettings: 41 | return RoomSettingsSettings; 42 | case SettingsPanels.Participants: 43 | return Participants; 44 | case SettingsPanels.CameraSettings: 45 | return VideoEffects; 46 | case SettingsPanels.Report: 47 | return Report; 48 | } 49 | } 50 | 51 | const View = getView(); 52 | 53 | return ( 54 |
55 | 56 |
57 | { 60 | events.emit("cancel"); 61 | cancel(); 62 | }} 63 | type={"button"} 64 | value={"Cancel"} 65 | className={"modal--button cancel"} 66 | /> 67 | 75 |
76 |
77 | ); 78 | }; 79 | export default SettingsViewer; 80 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsViews/Participants.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ISettingsPanelProps } from "../SettingsViewer"; 3 | import ParticipantList from "../../../Util/ParticipantList"; 4 | 5 | const Participants: React.FunctionComponent = ({ events }) => ( 6 |
7 |

Participant List

8 | 9 |
10 | ); 11 | 12 | export default Participants; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsViews/Report.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Report: React.FunctionComponent = () => ( 4 |
5 |

Report An Issue

6 | 7 | Please report all issues{" "} 8 | 13 | here 14 | {" "} 15 | or contact me directly. 16 | 17 |
18 | ); 19 | export default Report; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/Settings/SettingsViews/VideoEffects.css: -------------------------------------------------------------------------------- 1 | .video-preview-container { 2 | width: 80%; 3 | margin: 0 auto 10px; 4 | } 5 | 6 | .video-preview-wrapper { 7 | position: relative; 8 | max-height: 100%; 9 | padding-bottom: 56.25%; 10 | overflow: hidden; 11 | display: block; 12 | margin: 0 auto 10px; 13 | } 14 | 15 | .video-preview { 16 | width: 100%; 17 | object-fit: cover; 18 | height: 100%; 19 | position: absolute; 20 | top: 0; 21 | display: none; 22 | } 23 | 24 | .video-spinner { 25 | width: 100%; 26 | height: 100%; 27 | position: absolute; 28 | top: 0; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | 34 | .video-preview.ready { 35 | display: block; 36 | } 37 | 38 | .video-effects { 39 | height: 100%; 40 | width: 100%; 41 | overflow: auto; 42 | } 43 | 44 | .video-effects label input { 45 | display: inline-block; 46 | } 47 | 48 | .virtual-background-box-container { 49 | display: flex; 50 | } 51 | 52 | .virtual-background-box { 53 | display: inline-block; 54 | border: 3px solid #a8a8a8; 55 | border-radius: 9px; 56 | margin: 10px; 57 | height: 75px; 58 | width: 110px; 59 | color: rgba(15, 15, 15, 0.69); 60 | overflow: hidden; 61 | } 62 | 63 | .virtual-background-box.selected { 64 | border: 4px solid #4c5ef0; 65 | } 66 | 67 | .virtual-background-box { 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | } 72 | 73 | .virtual-background-box__image { 74 | width: 100%; 75 | height: 100%; 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/components/Modals/WaitingRoom.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Dialog.css"; 3 | 4 | const WaitingRoom: React.FunctionComponent = () => ( 5 |
6 |

Waiting Room

7 | 8 | You are in a waiting room. Please wait for the host to accept you. 9 | 10 |
11 | ); 12 | export default WaitingRoom; 13 | -------------------------------------------------------------------------------- /frontend/src/components/NotificationViewer.css: -------------------------------------------------------------------------------- 1 | .notification-list { 2 | position: absolute; 3 | bottom: 0; 4 | right: 0; 5 | z-index: 200; 6 | padding: 40px; 7 | max-width: 400px; 8 | overflow: auto; 9 | max-height: 100%; 10 | } 11 | 12 | .notification { 13 | padding: 13px 20px; 14 | margin: 10px 0; 15 | display: block; 16 | max-width: 100%; 17 | overflow: hidden; 18 | word-break: break-word; 19 | } 20 | 21 | .notification.alert { 22 | background: #7fa7ff; 23 | color: darkblue; 24 | border-radius: 5px; 25 | } 26 | 27 | 28 | .notification.success { 29 | background: #82ffb8; 30 | color: darkgreen; 31 | border-radius: 5px; 32 | } 33 | 34 | .notification.warning { 35 | background: #f5ff8d; 36 | color: #5d5d00; 37 | border-radius: 5px; 38 | } 39 | 40 | .notification.error { 41 | background: #ff8d8e; 42 | color: darkred; 43 | border-radius: 5px; 44 | } 45 | 46 | @media screen and (max-width: 600px) { 47 | .notification-list { 48 | top: 0; 49 | bottom: unset; 50 | padding: 10px; 51 | max-width: 100%; 52 | max-height: 25%; 53 | right: unset; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/NotificationViewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useObserver } from "mobx-react"; 3 | import "./NotificationViewer.css"; 4 | import NotificationStore from "../stores/NotificationStore"; 5 | import { UINotification } from "../interfaces/UINotification"; 6 | 7 | const NotificationViewer: React.FunctionComponent = () => { 8 | return useObserver(() => ( 9 |
10 | {NotificationStore.store.map((notification: UINotification, index: number) => { 11 | return ( 12 |
13 | 14 | {notification.message} 15 | 16 |
17 | ); 18 | })} 19 |
20 | )); 21 | }; 22 | export default NotificationViewer; 23 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/AudioFiller.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Participant from "../../../models/Participant"; 3 | import ParticipantService from "../../../services/ParticipantService"; 4 | import { useObserver } from "mobx-react"; 5 | import AutoPlayAudio from "../TileTypes/Util/AutoPlayAudio"; 6 | 7 | /* 8 | For the Grid layout, we embed the audio elements in the individual Tile elements, but for Pinned layouts, those 9 | Tile's aren't displayed (except for the pinned one) so the audio isn't gonna play. Instead of adding yet another audio 10 | bug, we simply include this element that goes through and adds any audio elements that we need to 11 | */ 12 | 13 | interface AudioFillerProps { 14 | exclusionList: Participant[]; 15 | } 16 | 17 | export const AudioFiller: React.FunctionComponent = ({ exclusionList }) => { 18 | if (!exclusionList) { 19 | exclusionList = []; 20 | } 21 | return useObserver(() => { 22 | const living = ParticipantService.getLiving(true); 23 | const participants = living.filter( 24 | (participant) => participant.hasAudio && !exclusionList!.includes(participant) 25 | ); 26 | 27 | return ( 28 | <> 29 | {participants.map((participant) => ( 30 | 35 | ))} 36 | 37 | ); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/ControlBar/ControlBar.css: -------------------------------------------------------------------------------- 1 | .controls-wrapper { 2 | position: absolute; 3 | bottom: 0; 4 | margin-bottom: 20px; 5 | font-size: 20px; 6 | color: #f1f1f1; 7 | opacity: 0; 8 | transition: opacity 200ms ease-in-out; 9 | transition-delay: 500ms; 10 | z-index: 2; 11 | } 12 | 13 | .controls-wrapper > span { 14 | height: 50px; 15 | width: 50px; 16 | border-radius: 50%; 17 | background: #3a3a3a; 18 | padding: 5px; 19 | margin: 0 10px; 20 | display: inline-flex; 21 | justify-content: center; 22 | align-items: center; 23 | box-shadow: 0px 0px 20px rgba(0,0,0,0.5); 24 | } 25 | 26 | 27 | .controls-wrapper span.controls-wrapper--leave-button { 28 | display: inline-flex; 29 | background: #e74848; 30 | } 31 | 32 | .controls-wrapper span.controls-wrapper--leave-button svg { 33 | transform: rotate(-135deg); 34 | } 35 | 36 | .controls-wrapper span.controls-wrapper--leave-button:hover { 37 | background: #a33636; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/ControlBar/ControlBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "./ControlBar.css"; 3 | import { when } from "mobx"; 4 | import UIStore from "../../../../stores/UIStore"; 5 | import IO from "../../../../controllers/IO"; 6 | import MyInfo from "../../../../stores/MyInfoStore"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { 9 | faDesktop, 10 | faMicrophone, 11 | faMicrophoneSlash, 12 | faPhone, 13 | faVideo, 14 | faVideoSlash, 15 | } from "@fortawesome/free-solid-svg-icons"; 16 | import ScreenShareSlash from "./ScreenshareSlash"; 17 | import { useObserver } from "mobx-react"; 18 | 19 | export const ControlBar: React.FunctionComponent = () => { 20 | const [forceDisplayControls, setForceDisplayControls] = useState(true); 21 | 22 | useEffect(() => { 23 | let timeout: NodeJS.Timeout; 24 | const disposer = when( 25 | () => !!UIStore.store.joinedDate, 26 | () => { 27 | timeout = setTimeout(() => setForceDisplayControls(false), 5000); 28 | } 29 | ); 30 | return () => { 31 | disposer(); 32 | if (timeout) { 33 | clearTimeout(timeout); 34 | } 35 | }; 36 | }, []); 37 | 38 | return useObserver(() => ( 39 |
40 | IO.toggleMedia("camera")}> 41 | {MyInfo.participant?.mediaState.camera ? ( 42 | 43 | ) : ( 44 | 45 | )} 46 | 47 | IO.toggleMedia("microphone")}> 48 | {MyInfo.participant?.mediaState.microphone ? ( 49 | 50 | ) : ( 51 | 52 | )} 53 | 54 | { 57 | IO.leave(); 58 | }} 59 | > 60 | 61 | 62 | IO.toggleMedia("screen")}> 63 | {MyInfo.participant?.mediaState.screen ? ( 64 | 65 | ) : ( 66 | 67 | )} 68 | 69 |
70 | )); 71 | }; 72 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/ControlBar/ScreenshareSlash.css: -------------------------------------------------------------------------------- 1 | .screenshare-slash { 2 | position: relative; 3 | } 4 | 5 | span.screenshare-slash__slash { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | margin: 0; 10 | padding: 0; 11 | transform: translate(-50%, -50%); 12 | background: transparent; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/ControlBar/ScreenshareSlash.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { faDesktop, faSlash } from "@fortawesome/free-solid-svg-icons"; 3 | import React from "react"; 4 | import "./ScreenshareSlash.css"; 5 | 6 | const ScreenShareSlash: React.FunctionComponent = () => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | export default ScreenShareSlash; 15 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/Layouts/PinnedParticipant.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UIStore from "../../../../stores/UIStore"; 3 | import VideoTile from "../../TileTypes/VideoTile"; 4 | import AudioTile from "../../TileTypes/AudioTile"; 5 | import { TileWrapper } from "../../TileTypes/Util/TileWrapper"; 6 | import { useObserver } from "mobx-react"; 7 | import { TileDisplayMode } from "../../../../enum/TileDisplayMode"; 8 | import { useLayoutCalculation } from "../../../../hooks/useLayoutCalculation"; 9 | import { TileMenuItem } from "../../TileTypes/Util/TileMenuItem"; 10 | import { VolumeSlider } from "../VolumeSlider/VolumeSlider"; 11 | 12 | interface PinnedParticipantProps { 13 | container: React.RefObject; 14 | } 15 | 16 | export const PinnedParticipant: React.FunctionComponent = ({ 17 | container, 18 | }) => { 19 | const styles = useLayoutCalculation(1, container); 20 | return useObserver(() => { 21 | if ( 22 | !UIStore.store.layout.participant!.hasVideo && 23 | !UIStore.store.layout.participant!.hasAudio 24 | ) { 25 | return null; 26 | } 27 | return ( 28 | 32 | (UIStore.store.layout = { 33 | mode: TileDisplayMode.GRID, 34 | participant: null, 35 | }) 36 | } 37 | > 38 | Unpin 39 | , 40 | UIStore.store.layout.participant!.hasAudio && ( 41 | 42 | ), 43 | ]} 44 | style={styles} 45 | > 46 | {UIStore.store.layout.participant!.hasVideo ? ( 47 | 48 | ) : ( 49 | 50 | )} 51 | 52 | ); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/Layouts/PinnedScreen.css: -------------------------------------------------------------------------------- 1 | .screen-hover-camera { 2 | position: absolute; 3 | bottom: 0; 4 | right: 0; 5 | margin: 10px; 6 | width: 200px; 7 | z-index: 1; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/Layouts/PinnedScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import UIStore from "../../../../stores/UIStore"; 3 | import VideoTile from "../../TileTypes/VideoTile"; 4 | import { TileWrapper } from "../../TileTypes/Util/TileWrapper"; 5 | import { useObserver } from "mobx-react"; 6 | import ScreenTile from "../../TileTypes/ScreenTile"; 7 | import "./PinnedScreen.css"; 8 | import { TileDisplayMode } from "../../../../enum/TileDisplayMode"; 9 | import { useLayoutCalculation } from "../../../../hooks/useLayoutCalculation"; 10 | import { TileMenuItem } from "../../TileTypes/Util/TileMenuItem"; 11 | import { VolumeSlider } from "../VolumeSlider/VolumeSlider"; 12 | 13 | interface PinnedScreenProps { 14 | container: React.RefObject; 15 | } 16 | 17 | export const PinnedScreen: React.FunctionComponent = ({ container }) => { 18 | const [forceHideCamera, setForceHideCamera] = useState(false); 19 | const styles = useLayoutCalculation(1, container); 20 | 21 | return useObserver(() => { 22 | if (!UIStore.store.layout.participant!.hasScreen) { 23 | return null; 24 | } 25 | 26 | const parentMenuItems = [ 27 | 29 | (UIStore.store.layout = { mode: TileDisplayMode.GRID, participant: null }) 30 | } 31 | > 32 | Unpin 33 | , 34 | ]; 35 | 36 | const childMenu = []; 37 | 38 | if (forceHideCamera) { 39 | parentMenuItems.push( 40 | setForceHideCamera(false)}>Show Camera 41 | ); 42 | } else { 43 | parentMenuItems.push( 44 | setForceHideCamera(true)}>Hide Camera 45 | ); 46 | childMenu.push( 47 | setForceHideCamera(true)}>Hide Camera 48 | ); 49 | if (UIStore.store.layout.participant!.hasAudio) { 50 | childMenu.push(); 51 | } 52 | } 53 | 54 | return ( 55 | 56 | <> 57 | 58 | {UIStore.store.layout.participant!.hasVideo && !forceHideCamera && ( 59 |
60 | 64 | 65 | 66 |
67 | )} 68 | 69 |
70 | ); 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/PreviewBox.css: -------------------------------------------------------------------------------- 1 | .preview-video-wrapper { 2 | height: 0; 3 | width: 100%; 4 | padding-bottom: 56.25%; 5 | overflow: hidden; 6 | position: relative; 7 | } 8 | 9 | .preview-video video { 10 | height: 100%; 11 | width: 100%; 12 | object-fit: cover; 13 | position: absolute; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/PreviewBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import "./PreviewBox.css"; 3 | import MyInfo from "../../../stores/MyInfoStore"; 4 | import HardwareService from "../../../services/HardwareService"; 5 | import { reaction } from "mobx"; 6 | import StreamEffectStore from "../../../stores/StreamEffectStore"; 7 | import NotificationService from "../../../services/NotificationService"; 8 | import { NotificationType } from "../../../enum/NotificationType"; 9 | 10 | export const PreviewBox: React.FunctionComponent = () => { 11 | const previewRef = useRef(null); 12 | 13 | async function updateMedia() { 14 | if (!MyInfo.participant?.mediaState.camera || !previewRef.current) { 15 | return; 16 | } 17 | const stream = await HardwareService.getStream("camera"); 18 | const srcObject = previewRef.current.srcObject as MediaStream | undefined; 19 | if (srcObject?.getVideoTracks()[0].id !== stream.getVideoTracks()[0].id) { 20 | previewRef.current!.srcObject = stream; 21 | } 22 | } 23 | 24 | useEffect(() => { 25 | if (!previewRef.current) { 26 | return; 27 | } 28 | 29 | function play() { 30 | previewRef.current!.play().catch((e) => { 31 | NotificationService.add( 32 | NotificationService.createUINotification( 33 | `Some error occurred. This shouldn't happen: "${e.toString()}"`, 34 | NotificationType.Error 35 | ) 36 | ); 37 | }); 38 | } 39 | 40 | previewRef.current.addEventListener("canplay", play); 41 | previewRef.current.removeEventListener("canplay", play); 42 | }, [previewRef]); 43 | 44 | useEffect(() => { 45 | updateMedia().catch((e) => { 46 | NotificationService.add( 47 | NotificationService.createUINotification( 48 | `Some error occurred. This shouldn't happen: "${e.toString()}"`, 49 | NotificationType.Error 50 | ) 51 | ); 52 | }); 53 | 54 | return reaction( 55 | () => ({ 56 | video: MyInfo.preferredInputs.video, 57 | effectRunner: StreamEffectStore.cameraStreamEffectRunner, 58 | }), 59 | updateMedia 60 | ); 61 | }, []); 62 | 63 | return ( 64 |
65 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/TileContainer.css: -------------------------------------------------------------------------------- 1 | .video-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | background: black; 6 | position: relative; 7 | overflow: hidden; 8 | } 9 | 10 | .screen-sharing-warning { 11 | position: absolute; 12 | top: 0; 13 | left: 50%; 14 | transform: translateX(-50%); 15 | background: #d04e53; 16 | color: #eeeeee; 17 | font-weight: 600; 18 | padding: 2px 10px; 19 | border-bottom-right-radius: 7px; 20 | border-bottom-left-radius: 7px; 21 | font-size: 13px; 22 | z-index: 1; 23 | } 24 | 25 | .preview-video { 26 | position: absolute; 27 | top: 0; 28 | right: 0; 29 | margin: 20px; 30 | margin-bottom: 0; 31 | width: 175px; 32 | padding-bottom: 20px; 33 | z-index: 1; 34 | } 35 | 36 | @media only screen and (max-width: 600px) { 37 | .preview-video { 38 | width: 150px; 39 | bottom: 80px; 40 | } 41 | } 42 | 43 | 44 | .videos-list-wrapper { 45 | max-height: 100%; 46 | max-width: 100%; 47 | display: flex; 48 | flex-wrap: wrap; 49 | justify-content: center; 50 | width: 100%; 51 | } 52 | 53 | 54 | .video-container:hover .controls-wrapper, .controls-wrapper.force-display { 55 | opacity: 1; 56 | transition-delay: 0ms; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/VolumeSlider/VolumeSlider.css: -------------------------------------------------------------------------------- 1 | .volume-slider { 2 | display: flex; 3 | align-items: center; 4 | color: white; 5 | } 6 | .volume-slider-wrapper { 7 | flex: 1; 8 | background: gray; 9 | height: 10px; 10 | position: relative; 11 | margin-left: 10px; 12 | width: 100px; 13 | border-radius: 7px; 14 | } 15 | 16 | .volume-slider--fill { 17 | background: #2a3365; 18 | height: 100%; 19 | display: inline-block; 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | border-radius: 7px; 24 | } 25 | 26 | .volume-slider--button { 27 | background: #f3f3f3; 28 | border: 1px solid #2a3365; 29 | height: 16px; 30 | width: 16px; 31 | position: absolute; 32 | top: 50%; 33 | border-radius: 50%; 34 | transform: translate(25%, -50%); 35 | right: 0; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileContainer/VolumeSlider/VolumeSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | MouseEvent as ReactMouseEvent, 3 | useCallback, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import "./VolumeSlider.css"; 9 | import Participant from "../../../../models/Participant"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 11 | import { faVolumeDown } from "@fortawesome/free-solid-svg-icons"; 12 | import { useObserver } from "mobx-react"; 13 | import { TileMenuItem } from "../../TileTypes/Util/TileMenuItem"; 14 | 15 | interface VolumeSliderProps { 16 | participant: Participant; 17 | } 18 | 19 | export const VolumeSlider: React.FunctionComponent = ({ participant }) => { 20 | const slider = useRef(null); 21 | const [listenToMouse, setListenToMouse] = useState(false); 22 | 23 | const changeVolume = useCallback( 24 | (e: ReactMouseEvent | MouseEvent) => { 25 | if (!slider.current) { 26 | return; 27 | } 28 | const boundingClientRect = slider.current.getBoundingClientRect(); 29 | participant.volume = Math.min( 30 | Math.max( 31 | Math.round( 32 | ((e.clientX - boundingClientRect.left) / boundingClientRect.width) * 100 33 | ) / 100, 34 | 0 35 | ), 36 | 1 37 | ); 38 | }, 39 | [slider, participant] 40 | ); 41 | 42 | function handleClick(e: ReactMouseEvent) { 43 | e.nativeEvent.stopImmediatePropagation(); 44 | changeVolume(e); 45 | } 46 | 47 | function handleMouseDown() { 48 | setListenToMouse(true); 49 | } 50 | 51 | function handleMouseUp() { 52 | setListenToMouse(false); 53 | } 54 | 55 | useEffect(() => { 56 | function move(e: MouseEvent) { 57 | if (!listenToMouse) { 58 | return; 59 | } 60 | changeVolume(e); 61 | } 62 | 63 | function up(e: MouseEvent) { 64 | e.stopImmediatePropagation(); 65 | setListenToMouse(false); 66 | } 67 | 68 | document.addEventListener("mouseup", up); 69 | document.addEventListener("mousemove", move); 70 | 71 | return () => { 72 | document.removeEventListener("mousemove", move); 73 | document.removeEventListener("mouseup", up); 74 | }; 75 | }, [changeVolume, listenToMouse]); 76 | 77 | return useObserver(() => ( 78 | 79 | 80 | 81 | 88 | 92 | 93 | 94 | 95 | 96 | 97 | )); 98 | }; 99 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileTypes/AudioTile.css: -------------------------------------------------------------------------------- 1 | .audio-wrapper { 2 | overflow: hidden; 3 | padding-bottom: 0; 4 | position: absolute; 5 | height: 100%; 6 | width: 100%; 7 | background: #2b2b2b; 8 | } 9 | 10 | .audio-participant--name { 11 | font-size: 20px; 12 | color: white; 13 | overflow: hidden; 14 | text-align: center; 15 | text-overflow: ellipsis; 16 | position: absolute; 17 | top: 50%; 18 | transform: translate(-50%, -50%); 19 | left: 50%; 20 | } 21 | 22 | .audio-participant--spacer { 23 | height: 0; 24 | padding-top: 56.25%; 25 | width: 100%; 26 | position: relative; 27 | background: #2b2b2b; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileTypes/AudioTile.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./AudioTile.css"; 3 | import { useObserver } from "mobx-react"; 4 | import { ITileProps } from "../TileContainer/TileContainer"; 5 | 6 | const AudioTile: React.FunctionComponent = ({ participant }) => 7 | useObserver(() => ( 8 |
9 | {participant.info.name} 10 |
11 | )); 12 | export default AudioTile; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileTypes/ScreenTile.css: -------------------------------------------------------------------------------- 1 | .screen-participant--video { 2 | width: 100%; 3 | object-fit: contain; 4 | height: 100%; 5 | position: absolute; 6 | top: 0; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/Tiles/TileTypes/ScreenTile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import "./VideoTile.css"; 3 | import "./ScreenTile.css"; 4 | import { ITileProps } from "../TileContainer/TileContainer"; 5 | import { useObserver } from "mobx-react"; 6 | 7 | const ScreenTile: React.FunctionComponent = ({ participant }) => { 8 | const videoRef = useRef(null); 9 | 10 | useEffect(() => { 11 | if (!videoRef.current) { 12 | return; 13 | } 14 | const element = videoRef.current; 15 | 16 | function canplay() { 17 | element.play().catch((e) => console.error(e.toString())); 18 | } 19 | 20 | element.addEventListener("canplay", canplay); 21 | 22 | videoRef.current.srcObject = new MediaStream([participant.consumers.screen!.track]); 23 | 24 | return () => element.removeEventListener("canplay", canplay); 25 | }, [videoRef, participant]); 26 | 27 | return useObserver(() => ( 28 | <> 29 |