When adding live video to your applications on Stream, we recommend checking out our newly released Video API!
Using Stream Video, developers can build live video calling and conferencing, voice calling, audio rooms, and livestreaming from a single unified API, complete with our fully customizable UI Kits across all major frontend platforms.
To learn more, check out our Video homepage or dive directly into the code using one of our SDKs.
With the recent launch of Stream Chat, the team here has been working with vendors to add real-time chat and messaging to various platforms. We’ve had several requests for voice and video chat, and after looking around at the market, it was a clear choice to partner up with Voxeet (recently acquired by Dolby). The proof is in the pudding, so we built out a fully functional proof of concept for a live video-conferencing application using Stream Chat and Voxeet as a web application with React.
The reason for our decision to use Voxeet over other competitors such as Twilio’s Programmable Video platform was primarily due to the following:
- Voxeet is an industry leader in the video space and provides SDKs for popular languages – in either your choice of JavaScript, iOS (Swift), or Android (Java)
- Predictable and generous pricing model that flat out makes sense – it’s based on the number of voice and video minutes
- Their documentation is written for developers with an emphasis on how to integrate – including nice example tutorials in the beginning
- They provide a React integration that works flawlessly with the Stream Chat React components making it easy for developers to access the underlying Voxeet API and connect it with Stream Chat
Furthermore, our initial tests showed that Voxeet had substantially less latency and offered far more clarity when it came to real-time video (and their video conferencing API) compared to Twilio. With better documentation, we knew ahead of time that we would be able to execute without running into any hiccups.
For the rest of this post, we will outline how we went about building a competitor similar to Zoom (the video communication tool) in less than a week. We\'ll be using Voxeet for live video (via WebRTC as well as Stream Chat for real-time chat/messaging capabilities.
Note: The full code for the frontend UI is available on GitHub. If you would like to run the demo using a backend API, please see this GitHub repo.
Prerequisites
To fully follow along with this tutorial, you’ll want to make sure that you have a decent understanding of the following:
- JavaScript/Node.js
- React
- Redux/Redux Saga
You will also want to ensure that you have the following installed on your machine:
- Homebrew (Latest)
- Node.js (v12.10.0 or above)
- Yarn (Latest)
- Create React App (CRA)
And, you’ll need to have created free accounts with the following services:
- Heroku (for hosting the API – free tier)
- Stream (for real-time chat functionality – free 14-day trial)
- Voxeet (for video conferencing – free tier)
Note: We’ll be using the latest version of Chrome on macOS to test throughout this tutorial.
Getting Started
The Build
The build-out of this application is rather straightforward. It requires two primary elements – the frontend UI and the backend API. If you want to take it a step further, you can host the frontend CRA application on Netlify and the API on Heroku in just a few minutes.
Step 1: Frontend UI
First, make sure you have an account set up with both Stream and Voxeet, and you have all the keys and tokens for each in hand, you will need the following:
- Stream Key
- Stream Secret
- Voxeet Key
- Voxeet Secret
Next, run npx create-react-app stream-voxeet
in your terminal. Once complete, add the Stream API key and the Voxeet keys from the associated dashboards to a .env
file within your project\'s root directory for safe-keeping.
Note: With create-react-app, you must prepend all environment variables with REACT_APP.
You will need the following environment variables in your .env
file:
- REACT_APP_STREAM_KEY
- REACT_APP_VOX_KEY
- REACT_APP_VOX_SECRET
Head back to your terminal and install the dependencies we need for the project:
yarn add animated react-router-dom @voxeet/voxeet-web-sdk @voxeet/react-components stream-chat stream-chat-react redux react-redux@5.1.1 redux-saga redux-thunk reselect shortid styled-components tinycolor2
Note: In our initial testing we realized that the Voxeet components currently throw an error with the latest version of react-redux. Make sure you enter react-redux@5.1.1 when you install the dependencies above.
Finally, create a jsconfig.json
file in the root of your application. Copy and paste the following:
{ "compilerOptions": { "baseUrl": "src" }, "include": ["src"] }
Note: The jsconfig.json file will allow aliased imports based on your directory structure without having to use absolute paths to your modules and components. E.g. import Button from components/Button.
Now that our project is all set up let’s jump into our code editor and start building.
Step 2: Setup
First up, we’ll get our Redux store, middleware, and styled-components theme set up, as well as removing the defaults provided by the CRA boilerplate.
Next, navigate to the src
directory in your project and remove the index.css
, app.css
, and logo.svg
files.
You’ll want to remove the imports for these assets in the App.js
and the index.js
files, as well as strip all boilerplate markup out of App.js
– leaving only the parent div
.
Note: You can also remove the className prop as we will replace all of the CSS with styled-components. Doing so will provide a theme file where you can easily tweak aspects of the design of the app once it is fully built.
Then, create the following directories inside your /src
directory:
- components
- containers
- data
- styles
- screens
Styles
We’ll start by creating a couple of files inside of the styles
directory. First, create styles/colors.js
and styles/breakpoints.js
respectively, and paste the following:
Colors:
export default { trueblack: '#000000', black: '#0A0B09', gray: '#111210', slate: '#232328', red: '#DC4C40', purple: '#6E7FFE', white: '#ffffff', };
Breakpoints:
export default { xs: 600, sm: 900, md: 1200, lg: 1800, xl: 2200 };
Next, we will import these variables in a moment to help populate our styled-components theme. Then, we’ll create a colorUtils.js
file and paste the following code inside:
import tinycolor from "tinycolor2"; // // Adapted from Material UI v0.x // https://github.com/mui-org/material-ui/tree/v0.x // /** * Returns a number whose value is limited to the given range. * * @param {number} value The value to be clamped * @param {number} min The lower boundary of the output range * @param {number} max The upper boundary of the output range * @returns {number} A number in the range [min, max] */ function clamp(value, min, max) { if (value < min) { return min; } if (value > max) { return max; } return value; } /** * Converts a color object with type and values to a string. * * @param {object} color - Decomposed color * @param {string} color.type - One of, 'rgb', 'rgba', 'hsl', 'hsla' * @param {array} color.values - [n,n,n] or [n,n,n,n] * @returns {string} A CSS color string */ export function convertColorToString(color) { const { type, values } = color; if (type.indexOf("rgb") > -1) { // Only convert the first 3 values to int (i.e. not alpha) for (let i = 0; i < 3; i++) { values[i] = parseInt(values[i], 10); } } let colorString; if (type.indexOf("hsl") > -1) { colorString = `${color.type}(${values[0]}, ${values[1]}%, ${values[2]}%`; } else { colorString = `${color.type}(${values[0]}, ${values[1]}, ${values[2]}`; } if (values.length === 4) { colorString += `, ${color.values[3]})`; } else { colorString += ")"; } return colorString; } export function convertHexToRGBAObj(color) { if (color.length === 4) { let extendedColor = "#"; for (let i = 1; i < color.length; i++) { extendedColor += color.charAt(i) + color.charAt(i); } color = extendedColor; } return { r: parseInt(color.substr(1, 2), 16), g: parseInt(color.substr(3, 2), 16), b: parseInt(color.substr(5, 2), 16), a: 1 }; } /** * Converts a color from CSS hex format to CSS rgb format. * * @param {string} color - Hex color, i.e. #nnn or #nnnnnn * @returns {string} A CSS rgb color string */ export function convertHexToRGB(color) { if (color.length === 4) { let extendedColor = "#"; for (let i = 1; i < color.length; i++) { extendedColor += color.charAt(i) + color.charAt(i); } color = extendedColor; } const values = { r: parseInt(color.substr(1, 2), 16), g: parseInt(color.substr(3, 2), 16), b: parseInt(color.substr(5, 2), 16) }; return `rgb(${values.r}, ${values.g}, ${values.b})`; } /** * Returns an object with the type and values of a color. * * Note: Does not support rgb % values and color names. * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @returns {{type: string, values: number[]}} A MUI color object */ export function decomposeColor(color) { if (color.charAt(0) === "#") { return decomposeColor(convertHexToRGB(color)); } const marker = color.indexOf("("); const type = color.substring(0, marker); let values = color.substring(marker + 1, color.length - 1).split(","); values = values.map(value => parseFloat(value)); return { type, values }; } /** * Calculates the contrast ratio between two colors. * * Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef * * @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @returns {number} A contrast ratio value in the range 0 - 21 with 2 digit precision. */ export function getContrastRatio(foreground, background) { const lumA = getLuminance(foreground); const lumB = getLuminance(background); const contrastRatio = (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05); return Number(contrastRatio.toFixed(2)); // Truncate at two digits } /** * The relative brightness of any point in a color space, * normalized to 0 for darkest black and 1 for lightest white. * * Formula: https://www.w3.org/WAI/GL/wiki/Relative_luminance * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @returns {number} The relative brightness of the color in the range 0 - 1 */ export function getLuminance(color) { color = decomposeColor(color); if (color.type.indexOf("rgb") > -1) { const rgb = color.values.map(val => { val /= 255; // normalized return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); }); return Number( (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3) ); // Truncate at 3 digits } else if (color.type.indexOf("hsl") > -1) { return color.values[2] / 100; } } /** * Darken or lighten a colour, depending on its luminance. * Light colors are darkened, dark colors are lightened. * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {number} coefficient=0.15 - multiplier in the range 0 - 1 * @returns {string} A CSS color string. Hex input values are returned as rgb */ export function emphasize(color, coefficient = 0.15) { return getLuminance(color) > 0.5 ? darken(color, coefficient) : lighten(color, coefficient); } /** * Set the absolute transparency of a color. * Any existing alpha values are overwritten. * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {number} value - value to set the alpha channel to in the range 0 -1 * @returns {string} A CSS color string. Hex input values are returned as rgb */ export function fade(color, value) { color = decomposeColor(color); value = clamp(value, 0, 1); if (color.type === "rgb" || color.type === "hsl") { color.type += "a"; } color.values[3] = value; return convertColorToString(color); } /** * Darkens a color. * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {number} coefficient - multiplier in the range 0 - 1 * @returns {string} A CSS color string. Hex input values are returned as rgb */ export function darken(color, coefficient) { color = decomposeColor(color); coefficient = clamp(coefficient, 0, 1); if (color.type.indexOf("hsl") > -1) { color.values[2] *= 1 - coefficient; } else if (color.type.indexOf("rgb") > -1) { for (let i = 0; i < 3; i++) { color.values[i] *= 1 - coefficient; } } return convertColorToString(color); } /** * Lightens a color. * * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {number} coefficient - multiplier in the range 0 - 1 * @returns {string} A CSS color string. Hex input values are returned as rgb */ export function lighten(color, coefficient) { color = decomposeColor(color); coefficient = clamp(coefficient, 0, 1); if (color.type.indexOf("hsl") > -1) { color.values[2] += (100 - color.values[2]) * coefficient; } else if (color.type.indexOf("rgb") > -1) { for (let i = 0; i < 3; i++) { color.values[i] += (255 - color.values[i]) * coefficient; } } return convertColorToString(color); } export function isValidHex(hex) { const lh = String(hex).charAt(0) === "#" ? 1 : 0; return ( hex.length !== 4 + lh && hex.length < 7 + lh && tinycolor(hex).isValid() ); }
Note: This file is adapted from v0.x of Material UI and offers handy utilities for handling colors – such as converting hex to RGB/RGBA and quickly altering the opacity, hue, and saturation of the color just by passing in a hex value. We often add this to our theme definition for easy access inside styled-components.
Now, we can bring this all together in styles/theme.js
– this theme will be available inside all of our styled-components and accessible through the withTheme
HOC.
import breakpoints from './breakpoints'; import colors from './colors'; import * as colorUtils from './colorUtils'; export default { breakpoints, borderRadius: 8, color: { background: colors.black, error: colors.red, text: colors.white, undersheet: colorUtils.fade(colors.black, 0.5), placeholder: colors.gray, border: colorUtils.fade(colors.white, 0.16), gradient: 'linear-gradient(120deg, #8148FC 0%, #55AAFF 100%)', ...colors, }, colorUtils, easing: { accelerate: [0.4, 0.0, 1, 1], deccelerate: [0.0, 0.0, 0.2, 1], standard: [0.4, 0.0, 0.2, 1], css: (easing) => `cubic-bezier(${easing.join(',')})`, }, z: { snackbar: 101, modal: 100, }, };
Last, we’ll create styles/global.js
. This file is effectively a drop-in replacement for index.css
that we removed earlier. Please note that we’ll also import two additional files at a later time to override styles for both stream-chat-react and voxeet.
import { createGlobalStyle } from 'styled-components'; export default createGlobalStyle` * { outline: none; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } html { height: 100%; width: 100%; } body { background-color: ${({ theme }) => theme.color.background}; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; min-height: 100vh; color: ${({ theme }) => theme.color.text}; display: flex; align-items: stretch; flex-direction: column; } #root { display: flex; align-items: stretch; flex-direction: column; flex: 1; } h1, h2, h3, h4, h5, h6, h7, h8 { margin: 0; } p { margin: 0; } `;
We’ll get this all tied together in our App.js
shortly, but first, we’ll set up our Redux store.
Redux
Inside of your data directory, add a file called createReducer.js
with the following code inside of it:
import { combineReducers } from 'redux'; import { reducer as voxeet } from '@voxeet/react-components'; export default () => combineReducers({ voxeet, });
The following code will hook the Voxeet reducer up to our Redux store and enable the Conference
components to work correctly. We will also create a rootSaga.js
file within the data directory; however, for now, we will leave it blank and come back to it later.
import { all, fork } from 'redux-saga/effects'; export default function*() { // We will fork all of our sagas // from this file later on. }
Next, let\'s create the Redux store itself in data/createStore.js
to tie it all together – we will add the thunk middleware which is required under the hood in the Voxeet components, as well as the redux-saga middleware which we will use a bit later on when we add simple authentication to the app as well as some other fun stuff.
import { compose, createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import thunkMiddleware from 'redux-thunk'; import createReducer from './createReducer'; import sagas from './rootSaga'; let store; export default () => { const reducer = createReducer(); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const middleware = []; const sagaMiddleware = createSagaMiddleware(); middleware.push(sagaMiddleware); middleware.push(thunkMiddleware); store = createStore(reducer, composeEnhancers(applyMiddleware(...middleware))); sagaMiddleware.run(sagas); return store; };
Now that our theme and our Redux store are ready to go, we’ll wrap our app in ThemeProvider
, react-redux’s Provider
and include our global styles and initial route definitions. To do this, open App.js
and make sure it looks like the following snippet:
import React from 'react'; // Router // import { Router, Switch, Route } from 'react-router-dom'; import history from 'utils/history'; // Styles // import { ThemeProvider } from 'styled-components'; import theme from 'styles/theme'; import GlobalStyles from 'styles/global'; import '@voxeet/react-components/dist/voxeet-react-components.css'; // Screens // import Conference from 'screens/Conference'; // Redux // import { Provider } from 'react-redux'; import createStore from 'data/createStore'; const store = createStore(); function App() { return ( <ThemeProvider theme={theme}> <Provider store={store}> <Router history={history}> <> <Switch> <Route path='/:conferenceAlias' component={Conference} /> </Switch> <GlobalStyles /> </> </Router> </Provider> </ThemeProvider> ); } export default App;
We’re now ready to start including Voxeet’s components and later, Stream Chat too. However, you should now have two missing imports from the file above. One is the Conference screen which we will create in just a second. The other is utils/history.js
.
By importing history
in this way, we can pass it to the Router
in the file above and it will operate identically to react-router v4’s BrowserRouter
. We can also import it in our sagas (and anywhere in our code for that matter) to navigate programmatically without using <Link />
or in areas where the history
prop is inaccessible.
Here is the code for the history util:
import { createBrowserHistory } from 'history'; export default createBrowserHistory();
Note: We will still want to use the component in our JSX code – but this allows us, for example, to redirect a user when they log in or log out, from within the authentication saga itself.
Step 3: Initializing Voxeet
We’ll start by creating the Conference screen where the video calls themselves will take place. Create Conference/Conference.js
inside of your screens
directory, open it in your editor and paste in the following:
import React, { Component } from 'react'; import styled from 'styled-components'; import { ConferenceRoom } from '@voxeet/react-components'; class Conference extends Component { handleOnConnect = () => { console.log('Participant connected'); }; handleOnLeave = () => { console.log('Participant disconnected'); this.props.history.push('/'); }; get settings() { return { consumerKey: process.env.REACT_APP_VOX_KEY, consumerSecret: process.env.REACT_APP_VOX_SECRET, constraints: { audio: true, video: true, }, videoRatio: { width: 1920, height: 1080, }, videoCodec: 'H264', }; } render() { const { match } = this.props; return ( <ConferenceRoom isWidget={false} autoJoin kickOnHangUp handleOnLeave={this.handleOnLeave} handleOnConnect={this.handleOnConnect} {...this.settings} conferenceAlias={match.params.conferenceAlias} /> ); } } export default Conference;
You should now be able to go to http://localhost:3000/test
to see a video call screen that is fully operational. The Voxeet components are fantastic. Out of the box, Voxeet provides full functionality to all of the features available by their SDK and APIs.
We are using the URL param match.params.conferenceAlias
as defined in App.js
to scope each conference to the URL – which also enables users to share the URL to invite other participants, much like Google Hangouts or Zoom. At this point, if you were to push your site up to Netlify, you can share the URL with a friend, and you will be able to conduct a full-featured video call! Note that you can use any random string after the /
in the URL to set the conference id – but more on that later.
Right now we only have control over the styling of the Voxeet UI through overriding the default CSS and users will have randomly generated usernames and ids. So, let’s fix that.
We can achieve much higher customization of the UI by passing in some additional props to the ConferenceRoom
component, namely attendeesChat
and actionsButtons
. Both of these props should be passed either a function that returns a component, or the component itself. We can start by passing NOOP functions to these props as follows:
class Conference extends Component { ... render() { const { match } = this.props; return ( <ConferenceRoom attendeesChat={() => null} actionsButtons={() => null} isWidget={false} autoJoin kickOnHangUp handleOnLeave={this.handleOnLeave} handleOnConnect={this.handleOnConnect} {...this.settings} conferenceAlias={match.params.conferenceAlias} /> ); } } ...
The components passed to these props will replace the chat drawer and the bottom actions bar, respectively. The ConferenceRoom
will pass down the necessary state from Redux as props, allowing us to use custom components that will alter the state of the call. Furthermore, it will aid us in providing a custom chat UI, which we\'ll use Stream Chat for later in the tutorial!
With the above changes, you should now see the main wrapper of the Conference screen will be full-height and full-width in the browser, and the actions bar along the bottom will be gone. We can now start to build our custom call UI.
Inside of the screens/Conference
directory, create a components
directory and inside create a new file called ActionsButtons.js
.
For now, we can leave this file empty as we will need some other components first that we can then utilize within the ActionButtons bar itself.
Back in src/components
let’s create a new directory called Icons and an Icon.js
file inside with the following code:
import React, { cloneElement } from 'react'; import PropTypes from 'prop-types'; import { withTheme } from 'styled-components'; const Icon = ({ children, className, color, onClick, theme, size, viewBox, style }) => { return ( <svg className={className} width={size} height={size} viewBox={viewBox} style={style} onClick={onClick}> {cloneElement(children, { fill: theme.color[color] })} </svg> ); }; Icon.propTypes = { className: PropTypes.string.isRequired, color: PropTypes.string.isRequired, size: PropTypes.number.isRequired, style: PropTypes.object, theme: PropTypes.object.isRequired, viewBox: PropTypes.string.isRequired, }; Icon.defaultProps = { color: 'text', size: 24, viewBox: '0 0 24 24', }; export default withTheme(Icon);
This file creates a re-usable Icon
component that we can use to generate a set of consistent SVG icons. We can then use these icons just the same as any other react component. The Icon
component also offers additional functionality – such as the ability to control the icon height/width via the size
prop (while maintaining consistency if we set the viewBox
correctly), as well as the ability to change the color with the color
prop.
Note: Thanks to the withTheme HOC, we can pass color names that map directly to our styled-components theme – i.e., purple, red, error, placeholder, text, etc. meaning we can add more color options by adding values to styles/colors.js.
For the sake of speed and ease, we used Material Icons and converting them for use inside of our new Icon component is a breeze.
Now that we have icons to use, we can create our ActionButton
component that we will use inside of the ActionsButtons.js
file we created earlier. Create a new ActionButton.js
file inside of screens/Conference/components
and paste in the following code:
import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; // Components // const Root = styled.div` height: ${({ size }) => size}px; width: ${({ size }) => size}px; border-radius: 50%; position: relative; display: flex; justify-content: center; align-items: center; background-color: ${({ theme }) => theme.color.slate}; cursor: pointer; user-select: none; transition: 200ms ease-out; & + & { margin-left: 16px; } &:hover { background-color: ${({ theme }) => theme.colorUtils.lighten(theme.color.slate, 0.1)}; } `; const Badge = styled.div` position: absolute; width: 8px; height: 8px; border-radius: 50%; background-color: ${({ theme }) => theme.color.red}; bottom: -16px; left: 50%; transform: translateX(-50%); `; const ActionButton = ({ color, enabled, icon: Icon, onClick, showBadge, size }) => ( <Root enabled={enabled} size={size} onClick={onClick}> <Icon color={color} size={size / 2} /> {showBadge ? <Badge /> : null} </Root> ); ActionButton.propTypes = { color: PropTypes.string, enabled: PropTypes.bool, icon: PropTypes.func.isRequired, onClick: PropTypes.func, showBadge: PropTypes.bool, size: PropTypes.number.isRequired, }; ActionButton.defaultProps = { color: 'white', enabled: false, onClick: () => {}, size: 64, }; export default ActionButton;
And finally, we’re ready to create our custom actions bar!
Back in screens/Conference/components/ActionsButtons.js
we can now add the following:
import React from 'react'; import styled from 'styled-components'; // Components // import { ChatIcon, CloseIcon, HangUpIcon, MicOffIcon, MicOnIcon, VideoOffIcon, VideoOnIcon, PeopleIcon, SettingsIcon, ShareScreenIcon, ShareScreenOffIcon, } from 'components/Icons'; import ActionButton from './ActionButton'; const Root = styled.div` width: 100%; display: flex; justify-content: space-between; align-items: center; height: 96px; padding: 16px 48px; margin-right: ${({ sidebarOpen }) => (sidebarOpen ? 376 : 0)}px; transition: margin-right 250ms; `; const MainControls = styled.div` display: flex; justify-content: center; align-items: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); `; const Actions = styled.div` display: flex; align-items: center; & > * + * { margin-left: 16px; } `; const ActionsButtons = ({ attendeesChatOpened, attendeesListOpened, attendeesSettingsOpened, isMuted, isScreenshare, leave, toggleAttendeesChat, toggleAttendeesList, toggleAttendeesSettings, toggleMicrophone, toggleScreenShare, toggleVideo, videoEnabled, unreadCount = 0, ...props }) => { const sidebarOpen = attendeesChatOpened || attendeesListOpened; return ( <Root sidebarOpen={sidebarOpen}> <Actions> <ActionButton icon={attendeesSettingsOpened ? CloseIcon : SettingsIcon} onClick={toggleAttendeesSettings} size={40} /> <ActionButton icon={isScreenshare ? ShareScreenOffIcon : ShareScreenIcon} onClick={toggleScreenShare} size={40} /> </Actions> <MainControls> <ActionButton icon={isMuted ? MicOffIcon : MicOnIcon} onClick={toggleMicrophone} size={40} /> <ActionButton color='red' icon={HangUpIcon} onClick={leave} /> <ActionButton enabled={videoEnabled} icon={videoEnabled ? VideoOnIcon : VideoOffIcon} onClick={toggleVideo} size={40} /> </MainControls> <Actions> <ActionButton icon={attendeesListOpened ? CloseIcon : PeopleIcon} onClick={toggleAttendeesList} size={40} /> <ActionButton showBadge={unreadCount > 0} icon={attendeesChatOpened ? CloseIcon : ChatIcon} onClick={toggleAttendeesChat} size={40} /> </Actions> </Root> ); }; export default ActionsButtons;
Currently, we are setting the unreadCount
prop to fall back to 0
as we are not defining it anywhere just yet. We will pull this data off of our Redux store later once we have Stream Chat set up.
All other props are provided by the Voxeet ConferenceRoom
component and are used internally by the default ActionsButtons
UI to change the state of buttons conditionally – so we will do the same. This state depends on parameters such as whether or not the user\'s mic or camera are disabled, if the chat drawer is open, etc.
Note that we have left some of the default Voxeet views in the UI such as settings (for controlling the input/output devices and the AttendeesList
for viewing all the active participants).
These views are great out of the box, and small amounts of CSS can get them looking as we would want without building them out by hand. Currently, there is not a prop available to pass a custom settings view; however, should you wish to build the attendees list by hand, as we have with the ActionsButtons
, you can pass a component in the same way to the attendeesList
prop of the ConferenceRoom
.
Now all that’s left to do is import our shiny new ActionsButtons
into the ConferenceRoom
as below:
... import ActionsButtons from './ActionsButtons'; class Conference extends Component { ... render() { const { match } = this.props; return ( <ConferenceRoom attendeesChat={() => null} actionsButtons={ActionsButtons} isWidget={false} autoJoin kickOnHangUp handleOnLeave={this.handleOnLeave} handleOnConnect={this.handleOnConnect} {...this.settings} conferenceAlias={match.params.conferenceAlias} /> ); } } ...
Now in your browser, you should see our custom call UI along the bottom of the screen! You can hang up the call, disable your camera or microphone, share your screen with the other participants, change your I/O settings, view the list of participants and, last but not least, toggle the chat drawer. This will be empty for now, but we’ll come back to this shortly.
We are also going to want to override some of Voxeets CSS to have the Conference screen match the rest of our application. For the sake of brevity, below is the complete CSS file from our finished version - feel free to tweak any of the values you see fit.
Go to your src/styles
folder and create a new directory called css
– then create a new file inside called voxeet.js
with the following styled-components CSS code:
import { css } from "styled-components"; export default css` /* * Main Wrapper */ .vxt-conference-attendees { background: ${({ theme }) => theme.color.background} !important; } @media screen and (max-width: 767px) { .vxt-conference-attendees .sidebar-container { margin-bottom: 80px !important; } } /* * Attendees Settings */ .attendees-settings { background: #000000 !important; height: calc(100% - 96px) !important; & .attendees-settings-header { border-color: ${({ theme }) => theme.color.gray} !important; } & h1 { color: #ffffff !important; } & .settings { height: inherit !important; background: #000000 !important; & .loadbar { padding: 0px !important; & li { background: ${({ theme }) => theme.color.slate} !important; & > .ins { background: ${({ theme }) => theme.color.purple} !important; } } } & .content .form-group select { border-color: ${({ theme }) => theme.color.gray} !important; color: #ffffff !important; padding: 4px; } & .content p { color: white !important; opacity: 0.56 !important; } } } /* * AttendeesList */ .attendees-list { background: #000000 !important; height: calc(100% - 96px) !important; & .attendees-list-header, & ul { border-color: ${({ theme }) => theme.color.gray} !important; } & .attendees-list-header h1, & .title-section, & .participant-details .participant-username { color: #ffffff !important; } & .title-section { opacity: 0.56; } } /* * View Switcher */ .SidebarList { background: ${({ theme }) => theme.color.gray} !important; } .vxt-conference-attendees, .vxt-conference-attendees .SidebarSpeaker .active-speaker .video-frame { background: ${({ theme }) => theme.color.background} !important; & p { color: ${({ theme }) => theme.color.black}; } } /* * Notification Snackbar */ .vxt-conference-attendees .onboardingmessage, .vxt-conference-attendees .onboardingmessage-fadeout { width: inherit !important; top: inherit !important; left: 50% !important; right: inherit !important; bottom: 120px !important; padding: 20px 16px !important; background: ${({ theme }) => theme.color.gradient} !important; border-radius: ${({ theme }) => theme.borderRadius}px; transform: translateX(-50%) !important; .chat-open & { transform: translateX(calc(-50% - (376px / 2))) !important; } } /* * Bottom Bar */ .vxt-bottom-bar { background-color: transparent !important; width: auto !important; left: 0 !important; right: 0 !important; transition: right 250ms; .chat-open & { @media (min-width: ${({ theme: { breakpoints } }) => breakpoints.sm}px) { right: 376px !important; } } } .vxt-conference-attendees .SidebarSpeaker .active-speaker .video-frame .fullscreen-screenshare { height: 30px !important; } /* * Attendee */ /* Video Wrapper */ .vxt-widget-fullscreen-on .vxt-conference-attendees .SidebarTiles .tile-item .tile-video, .vxt-conference-attendees { background-color: ${({ theme }) => theme.color.background} !important; } /* Name Bar */ .vxt-conference-attendees .participant-bar { background-color: #232328 !important; border: 0 !important; } /* Waiting for attendees placeholder */ .vxt-conference-attendees .conference-empty p { opacity: 1 !important; background-image: ${({ theme }) => theme.color.gradient} !important; } /* * Chat */ .vxt-widget-fullscreen-on .vxt-conference-attendees .sidebar-container.attendees-list-opened { margin-right: 376px !important; } /* * Error Banner */ .onboardingmessagewithaction-error { background: ${({ theme }) => theme.color.red} !important; } `;
Note: By creating our CSS overrides in this way, we can write our CSS as we usually would in a standard .css file, but with access to our theme.
All that’s left to do now is to jump into styles/global.js
and add the following two lines to import our overrides into the global CSS file:
... import voxeet from "./css/voxeet"; export default createGlobalStyle` ... ${voxeet} `;
Step 4: Setting up the Backend API & Authentication
Prior to setting up our Chat components, we will make things much easier for ourselves by first setting up the backend API and authentication for the app.
The backend for this project is simple, and Voxeet’s react components handle all of the Voxeet related backend functionality for you. All that we will need to do is pass in user data, which means we only need to generate a token for Stream Chat.
You can download the boilerplate here and follow along with the following section to get the backend and authentication flow working in your app. Alternatively, you can hit the Heroku button to immediately deploy the complete API code to Heroku or click here to view the finished API repo.
Note: You can click the Heroku Deploy button below to launch a Stream Chat trial using our prebuilt boilerplate API. Please take caution when using this, as the API does not enforce auth when hitting the /v1/token endpoint. If you would like to lock this down, we secure adding additional security measures to prevent authorized access.
Note: You can safely skip this next part and jump to building the authentication flow if you go the auto-deploy with Heroku route.
Let’s get to coding the backend!
Fortunately, a lot of the work has already been completed through the provided boilerplate API. We need to make a few small changes to get it working how we want.
Outside of your frontend repository, run the following command in your terminal from whichever directory you want to store the API code in.
git clone https://github.com/nparsons08/stream-chat-boilerplate-api stream-voxeet-api
Then, run cd stream-voxeet-api && yarn
and open the stream-chat-voxeet
directory in your editor.
We’ll start by renaming env.example
to .env
and inserting our Stream key and secret that we saved earlier into the correct variables. Once you\'re done, it should look something like this:
NODE_ENV=production PORT=8080 STREAM_API_KEY=your_stream_key STREAM_API_SECRET=your_stream_secret
Now, let’s hop back into the terminal and run yarn dev
to spin up the development server. The repo uses nodemon to automatically refresh when you make a change whilst keeping the server alive.
Finally, we need to open src/controllers/v1/token/token.action.js
and do a quick “find all” so that we can change all references to data.email
to data.username
. Your token.action.js
file should now look like the following snippet:
import dotenv from 'dotenv'; import md5 from 'md5'; import { StreamChat } from 'stream-chat'; dotenv.config(); exports.token = async (req, res) => { try { const data = req.body; let apiKey; let apiSecret; if (process.env.STREAM_URL) { [apiKey, apiSecret] = process.env.STREAM_URL.substr(8) .split('@')[0] .split(':'); } else { apiKey = process.env.STREAM_API_KEY; apiSecret = process.env.STREAM_API_SECRET; } const client = new StreamChat(apiKey, apiSecret); const user = Object.assign({}, data, { id: md5(data.username), role: 'admin', image: `https://robohash.org/${data.username}`, }); const token = client.createToken(user.id); await client.updateUsers([user]); res.status(200).json({ user, token, apiKey }); } catch (error) { console.log(error); res.status(500).json({ error: error.message }); } };
Note: By changing data.email to data.username we are changing the default behavior of authenticating via an email address, to using a simple username that we will build a login form for in just a second.
Optionally, you will see on line 28 the repo uses robohash to generate an avatar for the user. In our final version, we used ui-avatars.com as the images it generates are better suited to our design, but you can drop in any avatar generation URL here that you see fit.
And that’s it for the backend!
You can test that this is all working properly with Postman (or your REST client of choice is) by leaving the server running, and firing a POST request to http://localhost:8080/v1/token
where the body is the following object:
{ "username": "testuser" }
If successful, you will see the following in the response body:
{ "user": { "username": "testuser", "id": "your_user_id_here", "role": "admin", "name": "testuser", "image": "https://ui-avatars.com/api/?name=testuser&size=192&background=000000&color=6E7FFE&length=1" }, "token": "your_jwt_here", "apiKey": "your_api_key_here" }
Awesome! We can now start to build our login form and authenticate users inside our app.
Leave your API server running for now, and head back to your frontend code in your editor.
First up, let’s add a new env variable (in .env
) named REACT_APP_API_ENDPOINT
and set it’s value to http://localhost:8080/v1
and then make sure we have rebooted the frontend by closing the dev server and running yarn start
once again.
We’ll also need this small wrapper utility around Axios to make a request to our backend, save it in your project file in src/utils/fetch.js
import axios from 'axios'; const fetch = (method, path, data, params, headers, cancelToken) => { if (!method) throw new Error('Method is a required field.'); if (!path) throw new Error('Path is a required field.'); const options = { cancelToken, method: method.toUpperCase(), baseURL: `${process.env.REACT_APP_API_ENDPOINT}v1`, url: path, data: data || {}, params: params || {}, headers: { 'Content-Type': 'application/json', ...headers, }, }; return axios(options); }; const cancelToken = () => axios.CancelToken.source(); export default fetch; export { cancelToken };
Building the Authentication Flow
Now that we’re all set up to generate tokens for our users, we’ll kick off the auth flow by creating a new directory inside of our screens
directory called Login
with a Login.js
file inside of that with the following code:
import React, { Component } from 'react'; import styled from 'styled-components'; import shortid from 'shortid'; // Assets // import BackgroundImg from 'assets/bg.jpg'; import StreamLogo from 'assets/stream.svg'; // Forms // import LoginForm from 'forms/LoginForm'; // Components // import Logo from 'components/Logo'; import Text from 'components/Text'; const Root = styled.div` display: flex; flex: 1; background-image: url(${BackgroundImg}); background-size: cover; `; const Overlay = styled.div` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: ${({ theme }) => theme.colorUtils.fade(theme.color.background, 0.88)}; `; const Container = styled.div` max-width: 1280px; width: 100%; padding: 0px 24px; margin: 0 auto; z-index: 1; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; align-self: center; `; const Title = styled(Text)` margin-top: 16px; text-transform: capitalize; `; const Header = styled.div` display: flex; max-width: 1280px; width: 100%; margin: 0 auto; padding: 80px 24px 40px 24px; align-items: center; position: absolute; top: 0; left: 0; right: 0; `; const Stream = styled.img` max-height: 96px; margin-left: 40px; `; class Login extends Component { handleLogin = (val) => { console.log('login', val); }; renderForm = () => { return ( <Container> <Title size={40} weight='600'> Welcome. </Title> <Text size={16}>Enter a username to get started.</Text> <LoginForm conferenceAlias={this.conferenceAlias} onSubmit={this.handleLogin} /> </Container> ); }; render() { return ( <Root> <Overlay /> <Header> <Logo color='white' size={56} /> <Stream src={StreamLogo} /> </Header> {this.renderForm()} </Root> ); } } export default Login;
In the Login.js
file, we are importing a few components that we don’t have in our project just yet. The biggest of which is the LoginForm
component which we will come back to in a second but first, here is the code for the Text,
Input and
Button` components we will need to build the form UI.
Input:
import React from 'react'; // eslint-disable-line no-unused-vars import styled from 'styled-components'; const Root = styled.input` border: 0; border-radius: 8px; font-size: 16px; padding: 20px; color: white; margin: 16px 0px; background-color: ${({ theme }) => theme.colorUtils.fade(theme.color.white, 0.08)}; &::placeholder { color: #ffffff; } &:focus { background-color: ${({ theme }) => theme.colorUtils.fade(theme.color.white, 0.16)}; } &:hover { background-color: ${({ theme }) => theme.colorUtils.fade(theme.color.white, 0.16)}; } `; const Input = React.forwardRef(({ field, ...props }, ref) => <Root ref={ref} {...field} {...props} />); export default Input;
Button:
import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; // Components // import Text from 'components/Text'; const Root = styled.button` border: 0; background-color: ${({ theme }) => theme.color.purple}; background-image: ${({ theme }) => theme.color.gradient}; border-radius: 8px; box-shadow: 0px 4px 24px ${({ theme }) => theme.colorUtils.fade(theme.color.purple, 0.4)}; display: flex; justify-content: center; align-items: center; padding: 16px; cursor: pointer; `; const Button = ({ className, label, onClick, type }) => ( <Root className={className} type={type} onClick={onClick}> <Text size={16}>{label}</Text> </Root> ); Button.propTypes = { label: PropTypes.string.isRequired, type: PropTypes.oneOf(['button', 'submit']).isRequired, }; Button.defaultProps = { type: 'button', }; export default Button;
Text:
import React, { forwardRef } from 'react'; // eslint-disable-line no-unused-vars import PropTypes from 'prop-types'; import styled from 'styled-components'; import Animated from 'animated/lib/targets/react-dom'; const AnimatedText = Animated.createAnimatedComponent('p'); const Text = styled( forwardRef(({ color, faded, fontFamily, lineHeight, paragraph, size, weight, ...props }, ref) => ( <AnimatedText ref={ref} {...props} /> )), )` color: ${({ color, theme }) => theme.color[color]}; font-weight: ${({ weight }) => weight}; font-family: ${({ fontFamily }) => fontFamily}, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue'; font-size: ${({ size }) => size}px; opacity: ${({ faded }) => (faded ? 0.5 : 1)}; line-height: ${({ lineHeight, size, paragraph }) => lineHeight || size + 10}px; `; Text.propTypes = { color: PropTypes.string, weight: PropTypes.oneOf(['100', '200', '300', '400', '500', '600', '700', '800', '900']), size: PropTypes.number, faded: PropTypes.any, }; Text.defaultProps = { color: 'text', faded: false, fontFamily: 'Helvetica Neue', weight: '400', size: 17, }; export default Text;
Now we have all the pieces we need to build our LoginForm
component using Formik and Yup which we installed at the start of the tutorial.
Formik is an awesome library that allows us to easily build complex forms with React, and Yup provides a nice and powerful way to validate our input values. We won’t be utilizing the full potential of Formik in this tutorial, but should you decide to build upon this project after your done with the tutorial, this will set you up with a strong foundation for building any kind of form quickly and painlessly.
First, we’ll create a new directory inside of src
called forms
and inside of there create a file at this path forms/LoginForm/LoginForm.js
.
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { Field, Formik } from 'formik'; import Button from 'components/Button'; import Input from 'components/Input'; import validationSchema from './validationSchema'; const Root = styled.form` margin-top: 24px; flex: 1; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; & button { margin-top: 16px; } `; class LoginForm extends Component { get initialValues() { return { username: '', }; } renderForm = (form) => { const { conferenceAlias } = this.props; return ( <Root onSubmit={form.handleSubmit}> <Field name='username' component={Input} placeholder='Username' /> <Button label={conferenceAlias ? 'Join Call' : 'Start a Call'} type='submit' /> </Root> ); }; render() { const { onSubmit } = this.props; return ( <Formik children={this.renderForm} initialValues={this.initialValues} onSubmit={onSubmit} validationSchema={validationSchema} /> ); } } LoginForm.propTypes = { conferenceAlias: PropTypes.string, onSubmit: PropTypes.func.isRequired, }; export default LoginForm;
We start off by importing Formik
and Field
from formik
. The <Formik />
component will be the root element in our render function and we pass in our onSubmit
handler from props as well as some initial values as defined in the getter function on line 24 and finally, our this.renderForm
function as the child of the component.
Note: The initial values ensure that our inputs are always ‘controlled’. If we don’t do this, the value will be undefined and react will throw an error to say we have switched the input from a non-controlled to a controlled state.
The <Field />
component provided by Formik encapsulates all the functionality needed to pass the onChange
handler down to our custom input component, along with error messages (if there are any). It also ensures that the value is updated in the correct key of the forms value store using the name prop, which we will set to name=”username”
to match the key in the initial values.
We are also importing validationSchema.js
– this is where Yup comes in. The syntax is pretty easy to grasp and not only are the docs easy to follow but Yup also provides useful helpers for validating emails, phone numbers, etc as well as the option to pass custom regex validators and easily set the error messages that will be passed into the input component if the conditions are not met.
Create the validation file in your LoginForm
directory, next to the form component and paste in the following code:
import * as Yup from 'yup'; export default Yup.object().shape({ username: Yup.string('Must be a valid string.').required('This field is required.'), });
Now, back inside of LoginForm.js
you’ll see that we are passing our Yup schema into Formik as the validationSchema
prop. At this point, our form will be fully operational, but isn’t yet rendered to the DOM anywhere and doesn’t send it’s value to the backend just yet either.
Let’s quickly jump into App.js
and update the file to render our login route.
... // Screens // import Conference from 'screens/Conference'; import Login from 'screens/Login/Login.js'; // <-- add this line ... function App() { return ( <ThemeProvider theme={theme}> <Provider store={store}> <Router history={history}> <> <Switch> <Route path='/:conferenceAlias' component={Conference} /> <Route path='/' component={Login} /> // <-- and this line </Switch> <GlobalStyles /> </> </Router> </Provider> </ThemeProvider> ); } ...
Now, when you navigate to http://localhost:3000/ you should see our login page rendered to the screen, complete with the login form. You can also fill out the form, click the button below and open your console to see the output from the form.
We now need a way to send our form data to the backend, retrieve our token, and authenticate the user before sending them to the Conference Screen. Time for some Redux magic!
We’ll start out by creating our action creators and action types. You can find the relative snippets below, which will save inside of data/auth
in our project.
data/auth/types.js:
export const LOGIN = { REQUEST: 'Login/REQUEST', SUCCESS: 'Login/SUCCESS', ERROR: 'Login/ERROR', }; export const LOGOUT = { REQUEST: 'Logout/REQUEST', SUCCESS: 'Logout/SUCCESS', ERROR: 'Logout/ERROR', };
data/auth/actions.js:
import { LOGIN } from './types'; export const loginRequest = (data, conferenceAlias) => ({ type: LOGIN.REQUEST, conferenceAlias, data, });
Note: You’ll notice we are passing conferenceAlias as the second argument to the loginRequest action. This will be used later on when a user receives a link to join a call so that we can route them to the correct place once they have logged in.
Now, inside of auth/sagas/index.js
add the following code:
import { all, takeEvery } from 'redux-saga/effects'; // Sagas // import loginRequest from './loginRequest'; // Types // import { LOGIN } from '../types'; export default function*() { yield takeEvery(LOGIN.REQUEST, loginRequest); }
Generators, used heavily in redux-saga, are a new feature of JS as of ES6 and work very similarly to async/await. They are initialized using the function * ()
syntax and have a special keyword yield
that works very much like await
by “pausing” the function and waiting for the expression to resolve before continuing.
redux-saga
also provides some helpers like takeEvery
(used in the above snippet) which will take an action type as the first argument and a saga as the second. Every time the action is fired, the saga will run.
So the next logical step is to build our loginRequest
saga. Inside of auth/sagas
create a new file, loginRequest.js
and paste in the following code.
import { all, call, put } from 'redux-saga/effects'; import shortid from 'shortid'; // Utils // import history from 'utils/history'; import fetch from 'utils/fetch'; // Types // import { LOGIN } from '../types'; export default function*({ conferenceAlias, data: { username } }) { try { username = username.toLowerCase().replace(/\s/g, '_'); const { data: { token, user }, } = yield call(fetch, 'post', '/token', { username, }); localStorage.setItem('user', JSON.stringify(user)); localStorage.setItem('streamToken', token); if (!conferenceAlias) { conferenceAlias = shortid(); } yield all([ put({ type: LOGIN.SUCCESS, user, token, }), call(history.push, `/${conferenceAlias}`), ]); } catch (error) { yield put({ type: LOGIN.ERROR, error, }); } }
Let’s quickly break down what’s going on here.
- We import the
all
,call
andput
helpers from redux-saga as well asshortid
for generating the conference alias if we aren’t provided one by our login action. - We then import
history
andfetch
from our utils directory. - We also import our
LOGIN
action type so we can fire a success action or error action later in the saga. - Next, we define our saga and destructure its first and only argument, which will always be the action creator we fired (
loginRequest
), meaning we can pull the values off of the action and use them in our saga, in this case, the optional conference alias and the form data. - We then open a try/catch block and do some simple formatting on the username, to make sure it is all lowercase and contains no spaces (we’re replacing them with ‘_’ using regex) before running our fetch call.
- We use the
call
helper and pass in fetch as the first argument. All following arguments will be passed through to fetch itself. - Axios (and therefore our fetch util) returns the response body from the server as the
data
key, so we also destructure that to pull out the token and user object from the response. - Next, we store these values in localStorage so we can persist the user across sessions (but more on this later when we get to our reducer) and then check if a
conferenceAlias
was present in the action data. If it wasn’t, we generate one usingshortid
. - And finally, we use the
all
helper to run two saga helpers in parallel. The first isput
, which will fire our success action for the reducer to consume and the second iscall
in which we passhistory.push
to navigate the user to the conference screen. - We also use
put
in the catch block to fire theLOGIN.ERROR
action and pass the message to the reducer.
Now that our login saga is ready to rock, we need to make sure we are actually running the saga so that it can listen for the LOGIN.REQUEST
action. We will do this by using another saga helper (fork
) in the root saga that we created when we initialized our Redux store.
Open data/rootSaga.js
in your editor and amend it to look like the following snippet:
import { all, fork } from 'redux-saga/effects'; // Sagas // import auth from 'data/auth/sagas'; export default function*() { yield all([ fork(auth), ]); }
Next, create a reducer.js
file inside of data/auth
and add the following code to it:
import { LOGIN } from './types'; const init = { user: JSON.parse(localStorage.getItem('user')), streamToken: localStorage.getItem('streamToken'), loading: false, error: false, }; export default (state = init, action) => { switch (action.type) { case LOGIN.REQUEST: return { ...state, loading: true, }; case LOGIN.SUCCESS: return { ...state, loading: false, user: action.user, streamToken: action.token, }; default: return state; } };
The reducer will handle the LOGIN.SUCCESS
action and save the users token and profile data to the Redux store. We’re passing in some initial state with the defaults set to pull the values from localStorage
– if they aren’t present they will just return null and therefore leave the app in an unauthenticated state ready for login.
We can now use this data in our components to create a wrapper around the Conference screen route to protect it from unauthenticated users. In your src/containers
directory, create a new file called AuthedRoute.js
and past in the following snippet:
import React from 'react'; import { connect } from 'react-redux'; import { Redirect, Route } from 'react-router-dom'; // Screens // import LoadingScreen from 'screens/LoadingScreen'; // Redux // import { createStructuredSelector } from 'reselect'; import { makeSelectIsAuthed } from 'data/auth/selectors'; const AuthedRoute = ({ component: Component, isAuthed, loading, queueSnackbar, ...rest }) => { return ( <Route {...rest} render={(props) => { if (loading) { return <LoadingScreen />; } if (!isAuthed) { return ( <Redirect to={{ pathname: '/', state: { conferenceAlias: props.match.params.conferenceAlias }, }} /> ); } return <Component {...props} />; }} /> ); }; const mapStateToProps = createStructuredSelector({ isAuthed: makeSelectIsAuthed(), }); export default connect(mapStateToProps)(AuthedRoute);
The above component handles protecting our route from unauthenticated users as well as showing a loading screen if the auth request is in progress. It will also pass the conferenceAlias
parameter to location.state
so that if a user tries to access a conference without logging in, it will store the id so we can redirect them by checking for the value in the location state and passing it through to the action creator that we set up previously.
For the LoadingScreen add the following file to screens/LoadingScreen.js
:
import React from 'react'; import styled from 'styled-components'; // Components // import Text from 'components/Text'; const Root = styled.div` display: flex; justify-content: center; align-items: center; flex-direction: column; flex: 1; & > ${Text} { opacity: .08; margin-top: 16px; text-transform: uppercase; } `; const LoadingScreen = () => ( <Root> <Text size={16} weight='500'> Loading... </Text> </Root> ); export default LoadingScreen;
We are also using reselect
here to pull values out of the Redux store. reselect
memoizes the values from our store to reduce accidental or unnecessary re-renders of our components if the values haven’t changed. But, we haven’t defined these selectors yet. Back in data/auth
create a new file called selectors.js
and add the following:
import { createSelector } from 'reselect'; const getAuth = (state) => state.auth; export const makeSelectCurrentUser = () => createSelector( getAuth, ({ user }) => user, ); export const makeSelectStreamToken = () => createSelector( getAuth, ({ streamToken }) => streamToken, ); export const makeSelectIsAuthed = () => createSelector( makeSelectCurrentUser(), (user) => !!user, );
Now to implement our AuthedRoute
, jump back into your App.js
file, import it and update the route definition for the conference screen to use AuthedRoute
instead of React Routers <Route />
.
... // Containers // import AuthedRoute from 'containers/AuthedRoute'; ... function App() { return ( <ThemeProvider theme={theme}> <Provider store={store}> <Router history={history}> <> <Switch> <AuthedRoute path='/:conferenceAlias' component={Conference} /> // <-- HERE <Route path='/' component={Login} /> </Switch> <GlobalStyles /> <Snackbar /> </> </Router> </Provider> </ThemeProvider> ); } ...
Finally, let’s go back into our screens/Login/Login.js
file and make the following changes:
... import { connect } from 'react-redux'; ... // Redux // import { createStructuredSelector } from 'reselect'; import { makeSelectIsAuthed, makeSelectCurrentUser } from 'data/auth/selectors'; import { loginRequest } from 'data/auth/actions'; ... // Components // import Button from 'Components/Button'; ... const Avatar = styled.div` width: 160px; height: 160px; border-radius: 50%; background-image: ${({ src }) => `url(${src})`}; background-size: cover; margin-bottom: 16px; `; const CallBtn = styled(Button)` margin-top: 32px; `; class Login extends Component { get conferenceAlias() { const { location } = this.props; return location.state ? location.state.conferenceAlias : null; } get userName() { const { user } = this.props; return user.name.replace(/_/g, ' '); } startCall = () => { const { history } = this.props; const conferenceAlias = shortid(); history.push(`/${conferenceAlias}`); }; handleLogin = (val) => { const { loginRequest } = this.props; loginRequest(val, this.conferenceAlias); }; renderForm = () => { return ( <Container> <Title size={40} weight='600'> Welcome. </Title> <Text size={16}>Enter a username to get started.</Text> <LoginForm conferenceAlias={this.conferenceAlias} onSubmit={this.handleLogin} /> </Container> ); }; renderWelcome = () => { const { user } = this.props; return ( <Container> <Avatar src={user.image} /> <Title size={32} weight='600'> Welcome back, {this.userName}. </Title> <CallBtn label='Start Video Call' onClick={this.startCall} /> </Container> ); }; render() { const { isAuthed } = this.props; return ( <Root> <Overlay /> <Header> <Logo color='white' size={56} /> <Stream src={StreamLogo} /> </Header> {isAuthed ? this.renderWelcome() : this.renderForm()} </Root> ); } } const mapStateToProps = createStructuredSelector({ isAuthed: makeSelectIsAuthed(), user: makeSelectCurrentUser(), }); export default connect( mapStateToProps, { loginRequest }, )(Login);
We added some getter functions for formatting the user\'s name and retrieving the conferenceAlias
from the location state. We also connected our component to the Redux store so that we can access our store as well as mapping our loginRequest
function to props so we can fire it in the handleLogin
method.
Lastly, we added a new method to our class, renderWelcome
which renders a slightly different UI that shows the uses avatar and a “Start Video Call” button to launch a call. We conditionally call either that or renderLogin
in our render function depending on whether the user is authenticated or not.
Now that our backend is in place and our Login screen is fully operational, we can finish up our app by building our chat implementation with Stream! 🎉
Step 5: Stream Chat
If you have clicked the chat button in our call UI we built earlier, you will have no doubt noticed that opening the drawer will move the videos over to the left, but the drawer itself will not be visible. So now is as good a time as any to start integrating Stream Chat and building the custom drawer that we can pass into our ConferenceRoom
.
First up, you will need to add the following Portal
util, built using Reacts internal Portal implementation. If you haven’t used Portals before, they essentially allow you to define a component anywhere in the tree but have it rendered to the DOM anywhere outside of its current parent hierarchy. We will use the Portal to render a chat drawer inside of the body – above the rest of the app.
import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; // Utils // function getContainer(container, defaultContainer) { container = typeof container === 'function' ? container() : container; return ReactDOM.findDOMNode(container) || defaultContainer; } /** * Portals provide a first-class way to render children into a DOM node * that exists outside the DOM hierarchy of the parent component. */ class Portal extends React.Component { componentDidMount() { this.setMountNode(this.props.container); // Only rerender if needed if (!this.props.disable) { this.forceUpdate(this.props.onRendered); } } componentDidUpdate(prevProps) { if (prevProps.container !== this.props.container || prevProps.disable !== this.props.disable) { this.setMountNode(this.props.container); // Only rerender if needed if (!this.props.disable) { this.forceUpdate(this.props.onRendered); } } } componentWillUnmount() { this.mountNode = null; } setMountNode(container) { if (this.props.disable) { this.mountNode = ReactDOM.findDOMNode(this).parentElement; return; } this.mountNode = getContainer(container, document.body); } getMountNode = () => { return this.mountNode; }; render() { const { children, disable, unmount } = this.props; if (unmount) { return null; } if (disable) { return children; } return this.mountNode ? ReactDOM.createPortal(children, this.mountNode) : null; } } Portal.propTypes = { children: PropTypes.node.isRequired, container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), disable: PropTypes.bool, onRendered: PropTypes.func, unmount: PropTypes.bool, }; Portal.defaultProps = { disable: false, }; export default Portal;
We will also need a couple more action creators to handle toggling our drawer from outside of our ActionsButtons
component and setting the unread count in Redux. Create a new directory called chat
inside of your src/data
directory and place the following three files inside that contain our Action Types, Action Creators and Selectors for pulling the values out of Redux.
export const SET_UNREAD_COUNT = 'SET_UNREAD_COUNT'; // The below type is a replica of the one used internally by voxeet so we can mimic a click of the Chat ActionButton. export const TOGGLE_ATTENDEES_CHAT = 'TOGGLE_ATTENDEES_CHAT';
import { TOGGLE_ATTENDEES_CHAT, SET_UNREAD_COUNT } from './types'; export const toggleAttendeesChat = () => ({ type: TOGGLE_ATTENDEES_CHAT, }); export const setUnreadCount = (count) => ({ type: SET_UNREAD_COUNT, count, });
import { createSelector } from 'reselect'; const getChat = (state) => state.chat; export const makeSelectUnreadCount = () => createSelector( getChat, (chat) => chat.unread, );
Last, we will need to create our chat
reducer and import it into our root reducer also. Below is the code for the reducer, followed by a snippet showing the root reducer.
import { SET_UNREAD_COUNT } from './types'; const init = { unread: 0, }; export default (state = init, action) => { switch (action.type) { case SET_UNREAD_COUNT: return { ...state, unread: action.count }; default: return state; } };
import { combineReducers } from 'redux'; import { reducer as voxeet } from '@voxeet/react-components'; import auth from 'data/auth/reducer'; import chat from 'data/chat/reducer'; export default () => combineReducers({ auth, chat, voxeet, });
Now, on to our chat drawer!
In our screens/Conference
directory, let\'s make a new directory called containers
and create a new directory inside of that called AttendeesChat
. Inside, we’ll create AttendeesChat.js
and place the following code inside:
import React, { Component } from 'react'; import styled from 'styled-components'; import { StreamChat } from 'stream-chat'; import { Chat, Channel, Window, MessageList, MessageInput } from 'stream-chat-react'; import { withRouter } from 'react-router-dom'; import Animated from 'animated/lib/targets/react-dom'; import { compose } from 'redux'; import { connect } from 'react-redux'; // Redux // import { toggleAttendeesChat, setUnreadCount } from 'data/chat/actions'; import { createStructuredSelector } from 'reselect'; import { makeSelectCurrentUser, makeSelectStreamToken } from 'data/auth/selectors'; // Components // import Portal from 'utils/Portal'; import ChatHeader from './ChatHeader'; const Root = styled(Animated.div)` position: fixed; top: 0; right: 0; bottom: 0; z-index: 100; max-width: 376px; width: 100%; background-color: ${({ theme }) => theme.color.trueblack}; `; class AttendeesChat extends Component { anim = new Animated.Value(0); constructor(props) { super(props); this.chatClient = new StreamChat(process.env.REACT_APP_STREAM_KEY); this.state = { channel: null, unmount: true, }; } async componentDidMount() { const { match, user, streamToken } = this.props; await this.chatClient.setUser(user, streamToken); const channel = await this.chatClient.channel('messaging', match.params.conferenceAlias, { name: 'Video Call', }); await this.setState({ channel, }); } async componentDidUpdate(prevProps, prevState) { const { attendeesChatOpened, setUnreadCount } = this.props; const { channel } = this.state; if (!prevState.channel && channel) { this.init(); } if (!prevProps.attendeesChatOpened && attendeesChatOpened) { await this.setState({ unmount: false }); setUnreadCount(0); document.body.classList.add('chat-open'); Animated.timing(this.anim, { toValue: 1, duration: 250, }).start(); } else if (prevProps.attendeesChatOpened && !attendeesChatOpened) { document.body.classList.remove('chat-open'); Animated.timing(this.anim, { toValue: 0, duration: 250, }).start(() => { this.setState({ unmount: true, }); }); } } async init() { const { channel } = this.state; await channel.watch(); channel.on('message.new', this.handleNewMessage); } handleNewMessage = async () => { const { attendeesChatOpened, setUnreadCount } = this.props; const { channel } = this.state; const unread = await channel.countUnread(); setUnreadCount(attendeesChatOpened ? 0 : unread); }; get rootStyle() { return { transform: [ { translateX: this.anim.interpolate({ inputRange: [0, 1], outputRange: ['100%', '0%'], }), }, ], }; } render() { const { toggleAttendeesChat } = this.props; const { channel, unmount } = this.state; if (!channel || unmount) { return null; } return ( <Portal> <Root style={this.rootStyle}> <Chat client={this.chatClient} theme='messaging dark'> <Channel channel={channel}> <Window hideOnThread> <ChatHeader onClose={toggleAttendeesChat} /> <MessageList /> <MessageInput /> </Window> </Channel> </Chat> </Root> </Portal> ); } } const mapStateToProps = createStructuredSelector({ user: makeSelectCurrentUser(), streamToken: makeSelectStreamToken(), }); export default compose( connect( mapStateToProps, { toggleAttendeesChat, setUnreadCount }, ), withRouter, )(AttendeesChat);
There is quite a lot going on here – so here is a breakdown.
First, in our constructor, we define our default state and also set this.chatClient
to contain our StreamChat
instance and pass it the .env
variables we set at the beginning of the tutorial. Then, in componentDidMount
we call this.chatClient.setUser
and pass in an object representing our user
which we are pulling into props using our Redux selectors.
We then initialize the chat channel using this.chatClient.channel()
and pass in the “messaging” config parameter, along with the conferenceAlias
from our URL parameter so that we can scope the chat channel to the current conference and finally, call this.setState
to store our channel in our components state for later.
... class AttendeesChat extends Component { ... async componentDidMount() { const { match, user, streamToken } = this.props; await this.chatClient.setUser(user, streamToken); const channel = await this.chatClient.channel('messaging', match.params.conferenceAlias, { name: 'Video Call', }); await this.setState({ channel, }); } ... } ...
Next, in componentDidUpdate
we run a comparison check that will run when our channel is first initialized, by comparing the previous state value to the current one and checking our channel exists. If the condition is truthy, we run this.init()
and in turn, calls await this.state.channel.watch()
which subscribes the channel to future updates.
... class AttendeesChat extends Component { ... async componentDidUpdate(prevProps, prevState) { const { attendeesChatOpened, setUnreadCount } = this.props; const { channel } = this.state; if (!prevState.channel && channel) { this.init(); } ... } async init() { const { channel } = this.state; await channel.watch(); channel.on('message.new', this.handleNewMessage); } handleNewMessage = async () => { const { attendeesChatOpened, setUnreadCount } = this.props; const { channel } = this.state; const unread = await channel.countUnread(); setUnreadCount(attendeesChatOpened ? 0 : unread); } ... } ...
We also initialize a listener here, that fires on the message.new
event. Every time a new message is sent in the channel, the callback will run and updates our unread count using our setUnreadCount
action creator.
Next, in our render function, we render the stream-chat-react
components that will power our UI and handle a lot of the state management for us.
... class AttendeesChat extends Component { ... render() { const { toggleAttendeesChat } = this.props; const { channel, unmount } = this.state; if (!channel || unmount) { return null; } return ( <Portal> <Root style={this.rootStyle}> <Chat client={this.chatClient} theme='messaging dark'> <Channel channel={channel}> <Window hideOnThread> <ChatHeader onClose={toggleAttendeesChat} /> <MessageList /> <MessageInput /> </Window> </Channel> </Chat> </Root> </Portal> ); } } ...
We wrap everything with the Chat
component and pass in this.chatClient
to the client prop. Inside of this, we render the Channel
component to which we pass our channel
from this.state
, followed by Window
which will wrap the visual aspects of our Chat UI. Then, Inside of our Window
we render the ChatHeader
, MessageList
, MessageInput
.
Back inside of screens/Conference/components
create ChatHeader.js
and add the following snippet:
import React from 'react'; import styled from 'styled-components'; // Components // import { CloseIcon } from 'components/Icons'; import Text from 'components/Text'; const Root = styled.div` position: relative; z-index: 1; padding: 8px 40px; min-height: 72px; display: flex; background-color: ${({ theme }) => theme.color.background}; box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.56); color: white !important; display: flex; flex-direction: row; align-items: center; & > ${Text} `; const Fill = styled.span` flex: 1 1 auto; `; const Btn = styled.div` cursor: pointer; `; const ChatHeader = ({ onClose }) => ( <Root> <div> <Text size={24} weight='600' color='white'> Chat </Text> </div> <Fill /> <Btn onClick={onClose}> <CloseIcon color='white' size={24} /> </Btn> </Root> ); export default ChatHeader;
And that’s everything we need for a fully operational chat UI!
But we aren’t finished just yet...
We need to include the CSS file from stream-chat-react
. The default file exported from the library is, of course, responsive. However, this presents some issues due to the fact we are rendering the Chat components inside of a 375px wide wrapper div.
Because CSS media queries react to the size of the window and not the size of the parent div, properties such as padding
and max-width
will be incorrect – using the values it would for the full viewport regardless of the container size. To fix this, we’ll strip out the media queries so that we only use the mobile-sized definitions.
@import 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,400i,700,700i'; .str-chat { box-sizing: border-box; } .str-chat *, .str-chat *::after, .str-chat *::before { box-sizing: inherit; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .clearfix { clear: both; } .messenger-chat.str-chat { height: 100vh; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; display: flex; align-items: flex-start; justify-content: flex-start; margin: 0; flex: 1 0 100%; } .messenger-chat.str-chat .str-chat__container { flex: 1; height: 100%; display: flex; flex-direction: row; } .messenger-chat.str-chat .str-chat__main-panel { width: 100%; flex: 1; height: 100%; display: flex; flex-direction: column; padding: 20px 20px 0 10px; } .str-chat { height: 100vh; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .str-chat.messaging { background-color: #f1f1f3; } .str-chat.messaging.dark { background-color: #212326; } .str-chat.team.dark { background: #212326; } .str-chat.livestream.dark { background: #1a1a1a; } .str-chat-channel-list { float: left; } .str-chat-channel { max-height: 100vh; } .str-chat-channel .str-chat__container { height: 100%; display: flex; } .str-chat-channel .str-chat__container .str-chat__main-panel { height: 100%; display: flex; flex-direction: column; flex: 1; } .str-chat-channel.messaging .str-chat__main-panel { padding: 5px 5px 0; } .str-chat-channel.team .str-chat__container { display: flex; } .str-chat-channel.commerce .str-chat__main-panel { width: 100%; } .str-chat-channel.commerce .str-chat__container { background: rgba(255, 255, 255, 0.97); } .str-chat-channel.commerce.dark .str-chat__container { background: rgba(29, 32, 36, 0.9); box-shadow: 0 10px 31px 0 rgba(0, 0, 0, 0.5); } .str-chat.dark .emoji-mart { background: #1a1a1a; border: #343434; } .str-chat.dark .emoji-mart-category-label span { background: #1f1f1f; color: #fff; } .str-chat.dark .emoji-mart-search input { background: #1f1f1f; color: #fff; } .str-chat.dark .emoji-mart-search button svg { fill: #fff; } .rfu-file-icon--small.svg-inline--fa { color: #414d54; } .rfu-file-icon--small.fa-file-excel { color: #207245; } .rfu-file-icon--small.fa-file-powerpoint { color: #cb4a32; } .rfu-file-icon--small.fa-file-word { color: #2c599d; } .rfu-file-icon--small.fa-file-pdf { color: #f82903; } [class^='rfu-'], [class*=' rfu-'] { font-family: Avenir, Arial, Helvetica, sans-serif; -webkit-box-sizing: border-box; box-sizing: border-box; } .rfu-file-previewer { border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 4px; overflow: hidden; margin: 8px 0; position: relative; } .rfu-file-previewer ol { position: relative; margin: 0; padding: 0; list-style: none; } .rfu-file-previewer ol li { position: relative; padding: 8px 16px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .rfu-file-previewer ol li:last-child { border-color: transparent; } .rfu-file-previewer__file { position: relative; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; cursor: pointer; } .rfu-file-previewer__file:hover { background: #fafafa; } .rfu-file-previewer__file a { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; margin: 0 8px; color: #414d54; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rfu-file-previewer__file svg { min-width: 25px; } .rfu-file-previewer__file--uploading { opacity: 0.4; } .rfu-file-previewer__file--failed a { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; color: #8b9297; } .rfu-file-previewer__file--failed a::after { text-decoration: none; } .rfu-file-previewer__image { min-width: 25px; display: -webkit-box; display: -ms-flexbox; display: flex; } .rfu-file-previewer__loading-indicator { position: absolute; width: 100%; height: 100%; top: 0; left: 0; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; z-index: 1000; } .rfu-file-previewer__close-button { position: relative; z-index: 10000; } .rfu-file-previewer__failed { padding: 3px 6px; margin-left: 8px; color: #fff; border-radius: 4px; background: #ff6363; font-size: 12px; } .rfu-file-previewer__retry { text-decoration: none; padding: 3px 6px; margin-left: 8px; color: #fff; border-radius: 4px; background: #63e5a4; font-size: 12px; } .rfu-file-upload-button { cursor: pointer; } .rfu-file-upload-button svg { fill: #a0b2b8; } .rfu-file-upload-button:hover svg { fill: #88979c; } .rfu-file-upload-button label { cursor: pointer; } .rfu-file-upload-button .rfu-file-input { width: 0; height: 0; opacity: 0; overflow: hidden; position: absolute; z-index: -1; } .rfu-icon-button { cursor: pointer; position: relative; padding: 4px; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } .rfu-icon-button svg { margin: 4px; position: relative; z-index: 50; fill: #a0b2b8; } .rfu-icon-button:hover svg { fill: #88979c; } .rfu-dropzone .rfu-dropzone__notifier { position: absolute; height: 100%; width: 100%; padding: 30px; z-index: 90; display: none; border-radius: 4px; } .rfu-dropzone--accept .rfu-dropzone__notifier { background: rgba(0, 212, 106, 0.83); display: block; } .rfu-dropzone--reject .rfu-dropzone__notifier { background: rgba(255, 0, 0, 0.83); display: block; } .rfu-dropzone__inner { width: 100%; height: 100%; padding: 30px; border: 1px dashed #fff; border-radius: 4px; -webkit-box-sizing: border-box; box-sizing: border-box; display: -webkit-box; display: -ms-flexbox; display: flex; text-align: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; color: #fff; font-weight: 800; font-size: 12px; } .rfu-dropzone--reject .rfu-dropzone__inner { display: none; } .rfu-image-previewer { display: -webkit-box; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin: 8px 0; } .rfu-image-previewer__image { width: 100px; height: 100px; position: relative; margin-right: 8px; margin-bottom: 8px; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } .rfu-image-previewer__image--loaded .rfu-thumbnail__overlay { background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.4)), to(rgba(0, 0, 0, 0))); background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 100%); } .rfu-image-previewer__image .rfu-thumbnail__wrapper { position: absolute; } .rfu-image-previewer__image .rfu-loading-indicator { position: absolute; z-index: 90; } .rfu-image-previewer__retry { z-index: 90; } .rfu-thumbnail__wrapper { width: 100px; height: 100px; border-radius: 4px; overflow: hidden; position: relative; } .rfu-thumbnail__overlay { position: absolute; background-color: rgba(0, 0, 0, 0.4); width: 100%; height: 100%; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; padding: 5px; } .rfu-thumbnail__image { width: inherit; height: inherit; -o-object-fit: cover; object-fit: cover; } .rfu-loading-indicator { margin: 0 auto; width: 70px; text-align: center; } .rfu-loading-indicator > div { width: 18px; height: 18px; background-color: #ccc; border-radius: 100%; display: inline-block; -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; animation: sk-bouncedelay 1.4s infinite ease-in-out both; } .rfu-loading-indicator .bounce1 { -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } .rfu-loading-indicator .bounce2 { -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } @-webkit-keyframes sk-bouncedelay { 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); } 40% { -webkit-transform: scale(1); transform: scale(1); } } @keyframes sk-bouncedelay { 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); } 40% { -webkit-transform: scale(1); transform: scale(1); } } @-webkit-keyframes spinner { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes spinner { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } .rfu-loading-indicator__spinner { width: 20px; height: 20px; border: 2px solid #eee; border-top-color: #00d46a; border-radius: 50%; -webkit-animation: spinner 0.6s linear infinite; animation: spinner 0.6s linear infinite; } .rfu-image-upload-button { cursor: pointer; } .rfu-image-upload-button svg { fill: #a0b2b8; } .rfu-image-upload-button:hover svg { fill: #88979c; } .rfu-image-upload-button label { cursor: pointer; } .rfu-image-upload-button .rfu-image-input { width: 0; height: 0; opacity: 0; overflow: hidden; position: absolute; z-index: -1; } .rfu-thumbnail-placeholder { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; width: 100px; height: 100px; border: 1px dashed #bfbfbf; border-radius: 4px; cursor: pointer; } .rfu-thumbnail-placeholder:hover { background: #f2f2f2; } .emoji-mart, .emoji-mart * { box-sizing: border-box; line-height: 1.15; } .emoji-mart { font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; font-size: 16px; display: inline-block; color: #222427; border: 1px solid #d9d9d9; border-radius: 5px; background: #fff; } .emoji-mart .emoji-mart-emoji { padding: 6px; } .emoji-mart-bar { border: 0 solid #d9d9d9; } .emoji-mart-bar:first-child { border-bottom-width: 1px; border-top-left-radius: 5px; border-top-right-radius: 5px; } .emoji-mart-bar:last-child { border-top-width: 1px; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; } .emoji-mart-anchors { display: flex; flex-direction: row; justify-content: space-between; padding: 0 6px; color: #858585; line-height: 0; } .emoji-mart-anchor { position: relative; display: block; flex: 1 1 auto; text-align: center; padding: 12px 4px; overflow: hidden; transition: color 0.1s ease-out; margin: 0; box-shadow: none; background: none; border: none; } .emoji-mart-anchor:hover, .emoji-mart-anchor-selected { color: #464646; } .emoji-mart-anchor-selected .emoji-mart-anchor-bar { bottom: 0; } .emoji-mart-anchor-bar { position: absolute; bottom: -3px; left: 0; width: 100%; height: 3px; background-color: #464646; } .emoji-mart-anchors i { display: inline-block; width: 100%; max-width: 22px; } .emoji-mart-anchors svg, .emoji-mart-anchors img { fill: #858585; height: 18px; width: 18px; } .emoji-mart-scroll { overflow-y: scroll; height: 270px; padding: 0 6px 6px 6px; will-change: transform; } .emoji-mart-search { margin-top: 6px; padding: 0 6px; position: relative; } .emoji-mart-search input { font-size: 16px; display: block; width: 100%; padding: 5px 25px 6px 10px; border-radius: 5px; border: 1px solid #d9d9d9; outline: 0; } .emoji-mart-search input, .emoji-mart-search input::-webkit-search-decoration, .emoji-mart-search input::-webkit-search-cancel-button, .emoji-mart-search input::-webkit-search-results-button, .emoji-mart-search input::-webkit-search-results-decoration { -webkit-appearance: none; } .emoji-mart-search-icon { position: absolute; top: 7px; right: 11px; z-index: 2; padding: 2px 5px 1px; border: none; background: none; } .emoji-mart-category .emoji-mart-emoji span { z-index: 1; position: relative; text-align: center; cursor: default; } .emoji-mart-category .emoji-mart-emoji:hover:before { z-index: 0; content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #f4f4f4; border-radius: 100%; } .emoji-mart-category-label { z-index: 2; position: relative; position: -webkit-sticky; position: sticky; top: 0; } .emoji-mart-category-label span { display: block; width: 100%; font-weight: 500; padding: 5px 6px; background-color: #fff; background-color: rgba(255, 255, 255, 0.95); } .emoji-mart-category-list { margin: 0; padding: 0; } .emoji-mart-category-list li { list-style: none; margin: 0; padding: 0; display: inline-block; } .emoji-mart-emoji { position: relative; display: inline-block; font-size: 0; margin: 0; padding: 0; border: none; background: none; box-shadow: none; } .emoji-mart-emoji-native { font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji'; } .emoji-mart-no-results { font-size: 14px; text-align: center; padding-top: 70px; color: #858585; } .emoji-mart-no-results-img { display: block; margin-left: auto; margin-right: auto; width: 50%; } .emoji-mart-no-results .emoji-mart-category-label { display: none; } .emoji-mart-no-results .emoji-mart-no-results-label { margin-top: 0.2em; } .emoji-mart-no-results .emoji-mart-emoji:hover:before { content: none; } .emoji-mart-preview { position: relative; height: 70px; } .emoji-mart-preview-emoji, .emoji-mart-preview-data, .emoji-mart-preview-skins { position: absolute; top: 50%; transform: translateY(-50%); } .emoji-mart-preview-emoji { left: 12px; } .emoji-mart-preview-data { left: 68px; right: 12px; word-break: break-all; } .emoji-mart-preview-skins { right: 30px; text-align: right; } .emoji-mart-preview-skins.custom { right: 10px; text-align: right; } .emoji-mart-preview-name { font-size: 14px; } .emoji-mart-preview-shortname { font-size: 12px; color: #888; } .emoji-mart-preview-shortname + .emoji-mart-preview-shortname, .emoji-mart-preview-shortname + .emoji-mart-preview-emoticon, .emoji-mart-preview-emoticon + .emoji-mart-preview-emoticon { margin-left: 0.5em; } .emoji-mart-preview-emoticon { font-size: 11px; color: #bbb; } .emoji-mart-title span { display: inline-block; vertical-align: middle; } .emoji-mart-title .emoji-mart-emoji { padding: 0; } .emoji-mart-title-label { color: #999a9c; font-size: 26px; font-weight: 300; } .emoji-mart-skin-swatches { font-size: 0; padding: 2px 0; border: 1px solid #d9d9d9; border-radius: 12px; background-color: #fff; } .emoji-mart-skin-swatches.custom { font-size: 0; border: none; background-color: #fff; } .emoji-mart-skin-swatches.opened .emoji-mart-skin-swatch { width: 16px; padding: 0 2px; } .emoji-mart-skin-swatches.opened .emoji-mart-skin-swatch.selected:after { opacity: 0.75; } .emoji-mart-skin-swatch { display: inline-block; width: 0; vertical-align: middle; transition-property: width, padding; transition-duration: 0.125s; transition-timing-function: ease-out; } .emoji-mart-skin-swatch:nth-child(1) { transition-delay: 0s; } .emoji-mart-skin-swatch:nth-child(2) { transition-delay: 0.03s; } .emoji-mart-skin-swatch:nth-child(3) { transition-delay: 0.06s; } .emoji-mart-skin-swatch:nth-child(4) { transition-delay: 0.09s; } .emoji-mart-skin-swatch:nth-child(5) { transition-delay: 0.12s; } .emoji-mart-skin-swatch:nth-child(6) { transition-delay: 0.15s; } .emoji-mart-skin-swatch.selected { position: relative; width: 16px; padding: 0 2px; } .emoji-mart-skin-swatch.selected:after { content: ''; position: absolute; top: 50%; left: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; background-color: #fff; border-radius: 100%; pointer-events: none; opacity: 0; transition: opacity 0.2s ease-out; } .emoji-mart-skin-swatch.custom { display: inline-block; width: 0; height: 38px; overflow: hidden; vertical-align: middle; transition-property: width, height; transition-duration: 0.125s; transition-timing-function: ease-out; cursor: default; } .emoji-mart-skin-swatch.custom.selected { position: relative; width: 36px; height: 38px; padding: 0 2px 0 0; } .emoji-mart-skin-swatch.custom.selected:after { content: ''; width: 0; height: 0; } .emoji-mart-skin-swatches.custom .emoji-mart-skin-swatch.custom:hover { background-color: #f4f4f4; border-radius: 10%; } .emoji-mart-skin-swatches.custom.opened .emoji-mart-skin-swatch.custom { width: 36px; height: 38px; padding: 0 2px 0 0; } .emoji-mart-skin-swatches.custom.opened .emoji-mart-skin-swatch.custom.selected:after { opacity: 0.75; } .emoji-mart-skin-text.opened { display: inline-block; vertical-align: middle; text-align: left; color: #888; font-size: 11px; padding: 5px 2px; width: 95px; height: 40px; border-radius: 10%; background-color: #fff; } .emoji-mart-skin { display: inline-block; width: 100%; padding-top: 100%; max-width: 12px; border-radius: 100%; } .emoji-mart-skin-tone-1 { background-color: #ffc93a; } .emoji-mart-skin-tone-2 { background-color: #fadcbc; } .emoji-mart-skin-tone-3 { background-color: #e0bb95; } .emoji-mart-skin-tone-4 { background-color: #bf8f68; } .emoji-mart-skin-tone-5 { background-color: #9b643d; } .emoji-mart-skin-tone-6 { background-color: #594539; } .emoji-mart-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } .str-chat__actions-box { background: #fff; background-image: linear-gradient(-180deg, rgba(255, 255, 255, 0.02) 0%, rgba(0, 0, 0, 0.02) 100%); box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.22), 0 1px 0 0 rgba(0, 0, 0, 0.08), 0 1px 8px 0 rgba(0, 0, 0, 0.05); border-radius: 7px; display: flex; flex-direction: column; z-index: 1000; position: absolute; min-width: 150px; } .str-chat__actions-box--right { right: 0; top: calc(100% + 2px); } .str-chat__actions-box--left { left: 0; top: calc(100% + 2px); } .str-chat__actions-box > button { text-align: left; width: 100%; border: none; margin: 0; padding: 10px; font-size: 12px; background: none; cursor: pointer; } .str-chat__actions-box > button:not(:last-of-type) { box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.08); } .str-chat__actions-box > button:hover { color: #006cff; } .dark.str-chat .str-chat__message-actions-box { background: #67686a; background-image: linear-gradient(-180deg, rgba(255, 255, 255, 0.02) 0%, rgba(0, 0, 0, 0.02) 100%); box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.22), 0 1px 0 0 rgba(0, 0, 0, 0.08), 0 1px 8px 0 rgba(0, 0, 0, 0.05); } .dark.str-chat .str-chat__message-actions-box button { color: #fff; } .dark.str-chat .str-chat__message-actions-box button:hover { color: #006cff; } .str-chat__message-attachment-actions-form { width: 100%; margin: 8px 0; padding: 0; display: flex; } .str-chat__message-attachment-actions-button { flex: 1; border: none; background: none; margin: 0 4px; padding: 8px 8px; border-radius: 100px; outline: none; } .str-chat__message-attachment-actions-button:focus { border: 1px solid #006cff; box-shadow: 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__message-attachment-actions-button--primary { background-color: #006cff; color: #fff; } .str-chat__message-attachment-actions-button--default { border: 2px solid rgba(0, 0, 0, 0.08); } .dark.str-chat .str-chat__message-attachment-actions-button { color: #fff; } .dark.str-chat .str-chat__message-attachment-actions-button--default { border-color: rgba(255, 255, 255, 0.1); } .str-chat__player-wrapper { position: relative; padding-top: 56.25%; } .str-chat__player-wrapper .react-player { position: absolute; top: 0; left: 0; } .str-chat__message .str-chat__player-wrapper .react-player { border-radius: 16px 16px 16px 0; overflow: hidden; } .str-chat__message--me .str-chat__player-wrapper .react-player { border-radius: 16px 16px 0 16px; overflow: hidden; } .str-chat__message-attachment { width: 100%; max-width: 375px; border-radius: 16px; margin: 8px auto 8px 0; padding: 0; } .str-chat__message--me .str-chat__message-attachment { padding-left: 0; margin: 8px 0 8px auto; } .str-chat__message-team.thread-list .str-chat__message-attachment { max-width: 200px; } .str-chat__message-attachment:hover { background: transparent; } .str-chat__message-attachment--card--no-image { height: 60px; } .str-chat__message-attachment--card--actions { height: auto; } .str-chat__message-attachment-file { width: 100%; } .str-chat__message-attachment-file--item { position: relative; height: 50px; display: flex; align-items: center; font-size: 14px; line-height: 22px; border-left: 1px solid rgba(0, 0, 0, 0.1); width: auto; padding-left: 5px; } .str-chat__message-attachment-file--item:hover { background: #f7f7f7; } .str-chat__message-attachment-file--item img, .str-chat__message-attachment-file--item svg { margin-right: 10px; } .str-chat__message-attachment-file--item-text { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .str-chat__message-attachment-file--item a { font-weight: 700; color: #000; opacity: 0.8; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; } .str-chat__message-attachment-file--item a::after { content: ''; position: absolute; top: 0; right: 0; bottom: 0; left: 0; } .str-chat__message-attachment-file--item span { line-height: 14px; font-size: 12px; font-weight: bold; text-transform: uppercase; display: block; color: #000; opacity: 0.5; } .str-chat__message-attachment--image { height: auto; max-height: 300px; max-width: 100%; border-radius: 0; } .str-chat__message-attachment--image:hover { background: transparent; } .str-chat__message-attachment--image img { height: inherit; width: auto; max-height: inherit; max-width: 100%; display: block; object-fit: cover; overflow: hidden; } .str-chat__message-attachment--image img:hover { background: transparent; } .str-chat__message-attachment--image--actions { height: 320px; } .str-chat__message-attachment--image--actions img { height: calc(320px - 40px); } .str-chat__message-attachment-card { min-height: 60px; } .str-chat__message-attachment-card__giphy-logo { height: 20px; width: auto; } .messaging.str-chat .str-chat__message-attachment.str-chat__message-attachment--image--actions .str-chat__message-attachment--img { max-height: 254px; } .livestream.str-chat .str-chat__message-attachment.str-chat__message-attachment--file { max-width: 100%; } .livestream.str-chat .str-chat__message-attachment.str-chat__message-attachment--file .str-chat__message-attachment-file--item { border-left: none; } .livestream.str-chat .str-chat__message-attachment.str-chat__message-attachment--file .str-chat__message-attachment-file--item:hover { background: rgba(255, 255, 255, 0.29); } .livestream.str-chat.dark .str-chat__message-attachment-file--item a, .livestream.str-chat.dark .str-chat__message-attachment-file--item span { color: #fff; } .livestream.str-chat.dark .str-chat__message-attachment-file--item:hover { background: transparent; } .str-chat__avatar { width: 32px; height: 32px; flex: 0 0 32px; margin-right: 14px; display: flex; align-items: center; justify-content: center; color: #fff; text-transform: uppercase; overflow: hidden; } .str-chat__avatar--circle { border-radius: 50%; } .str-chat__avatar--rounded { border-radius: 6px; } .str-chat__avatar--square { border-radius: 0; } .str-chat__avatar-image, .str-chat__avatar-fallback { display: block; width: inherit; height: inherit; object-fit: cover; text-align: center; } .str-chat__avatar-image--loaded { background-color: none; } .str-chat__avatar-fallback { background-color: #006cff; } .str-chat__message--me > .str-chat__avatar { order: 1; margin: 0; margin-left: 10px; } .str-chat__li--top .str-chat__message > .str-chat__avatar, .str-chat__li--middle .str-chat__message > .str-chat__avatar { visibility: hidden; } .str-chat__audio__wrapper { height: 80px; overflow: hidden; position: relative; border-radius: 6px; margin: 8px 0 0; display: flex; background: #f1f1f1; } .str-chat__audio__image { height: 80px; width: 80px; position: relative; z-index: 20; } .str-chat__audio__image--overlay { width: inherit; height: inherit; position: absolute; top: 0; left: 0; background: rgba(0, 0, 0, 0.4); z-index: 30; font-size: 3em; color: rgba(255, 255, 255, 0.69); display: flex; align-items: center; justify-content: center; user-select: none; } .str-chat__audio__image--button { margin: 0; padding: 0; display: flex; align-items: center; width: 40px; height: 40px; } .str-chat__audio__image--button svg { fill: rgba(255, 255, 255, 0.69); } .str-chat__audio__image img { z-index: 20; position: absolute; top: 0; left: 0; width: inherit; height: inherit; object-fit: cover; } .str-chat__audio__content { display: flex; flex-direction: column; justify-content: space-around; padding: 8px 16px; margin-left: 16px; width: 100%; } .str-chat__audio__content--title { margin: 0; padding: 0; line-height: 1; } .str-chat__audio__content--subtitle { margin: 0; padding: 0; line-height: 1; font-size: 12px; opacity: 0.49; } .str-chat__audio__content--progress { height: 6px; width: 100%; border-radius: 4px; background: rgba(0, 0, 0, 0.1); padding: 1px; margin: 2px 0; } .str-chat__audio__content--progress > div { height: 4px; border-radius: 4px; width: 0%; background: #006cff; transition: width 0.5s linear; } .str-chat.dark .str-chat__audio__wrapper { background: #1a1a1a; color: #fff; } .str-chat.dark .str-chat__audio__content--progress { background: rgba(255, 255, 255, 0.1); } .str-chat__message-attachment-card { position: relative; background: #fff; border-radius: 16px 16px 16px 0; overflow: hidden; font-size: 13px; border: 1px solid rgba(0, 0, 0, 0.08); margin: 32px 0 0 0; } .str-chat__message-attachment-card--header { width: 100%; height: 175px; } .str-chat__message-attachment-card--header img { width: inherit; height: inherit; object-fit: cover; } .str-chat__message-attachment-card--title { font-weight: 700; flex: 1; } .str-chat__message-attachment-card--flex { min-width: 0px; } .str-chat__message-attachment-card--flex, .str-chat__message-attachment-card--flex > * { overflow: hidden; text-overflow: ellipsis; } .str-chat__message-attachment-card--content { padding: 8px 16px; margin: -8px 0; display: flex; flex-direction: row; align-items: center; justify-content: space-between; } .str-chat__message-attachment-card--content > * { margin: 8px 0; } .str-chat__message-attachment-card--url { text-decoration: none; display: block; color: #000; text-transform: uppercase; opacity: 0.5; } .str-chat__message-attachment-card--url::after { content: ''; position: absolute; top: 0; right: 0; bottom: 0; left: 0; } .str-chat__message-attachment-card--giphy .str-chat__message-attachment-card--header { height: unset; } .str-chat.commerce .str-chat__message-attachment-card { max-width: 375px; width: 100%; } .str-chat__message--me .str-chat__message-attachment-card { background: rgba(0, 0, 0, 0.08); border: 1px solid transparent; border-radius: 16px 16px 0 16px; } .dark.str-chat .str-chat__message-attachment-card__giphy-logo { filter: invert(100%); } .str-chat__header { display: flex; padding: 10px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__header-thread { display: flex; padding: 10px; min-height: 70px; align-items: center; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background: #fff; box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.07); font-size: 14px; } .str-chat__header-thread__back-button { padding: 10px 10px 10px 10px; border: 0; background: none; transform: rotate(180deg); } .str-chat__header-livestream { padding: 10px 40px; min-height: 70px; display: flex; align-items: center; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background: #fff; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14); } .str-chat__header-livestream-left { font-size: 14px; flex: 1; } .str-chat__header-livestream-left--title { font-weight: 700; margin: 0; } .str-chat__header-livestream-left--members { font-weight: 400; margin: 0; } .str-chat__header-livestream-left--livelabel { position: relative; left: 5px; font-size: 13px; text-transform: uppercase; color: red; display: inline-block; animation: pulse 2s infinite; } .str-chat__header-livestream-left--livelabel::before { content: ''; position: relative; top: -2px; left: -4px; display: inline-block; width: 5px; height: 5px; border-radius: 100%; background-color: red; } @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } .str-chat__header-livestream-right { display: flex; margin: 0 -5px; } .str-chat__header-livestream-right-button-wrapper { position: relative; } .str-chat__header .str-chat__avatar { margin: 0 16px 0 0; } .str-chat__title { font-weight: 600; } .str-chat__meta { width: 100%; display: flex; flex-direction: column; justify-content: space-between; } .str-chat__info { width: 100%; display: flex; justify-content: space-between; font-size: 14px; color: rgba(0, 0, 0, 0.4); } .str-chat__square-button { border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 3px; width: 30px; height: 30px; margin: 0 5px; display: flex; align-items: center; justify-content: center; } .str-chat__square-button svg { fill: rgba(0, 0, 0, 0.8); } .str-chat__square-button:active { background-color: rgba(0, 0, 0, 0.1); } .dark.str-chat .str-chat__square-button { background: rgba(255, 255, 255, 0.07); box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.69); border-radius: 3px; } .dark.str-chat .str-chat__square-button svg { fill: rgba(255, 255, 255, 0.7); } .messaging.str-chat .str-chat__header-livestream { position: relative; z-index: 1; border-radius: 10px 10px 0 0; background: rgba(255, 255, 255, 0.9); box-shadow: none; padding-left: 20px; padding-right: 20px; box-shadow: 0 7px 9px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 rgba(0, 0, 0, 0.03); } .messaging.str-chat.dark .str-chat__header-livestream { background: rgba(46, 48, 51, 0.98); box-shadow: 0 7px 9px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 rgba(0, 0, 0, 0.03); border-radius: 10px 10px 0 0; color: #fff; } .livestream.str-chat .str-chat__header-livestream { position: relative; z-index: 1; background: rgba(255, 255, 255, 0.29); box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); } .livestream.str-chat.dark .str-chat__header-livestream { background: rgba(255, 255, 255, 0.03); box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.34); } .livestream.str-chat.dark .str-chat__header-livestream-left--title, .livestream.str-chat.dark .str-chat__header-livestream-left--members { color: #fff; } .livestream.str-chat.dark .str-chat__header-livestream-left--title { font-size: 15px; } .commerce.str-chat .str-chat__header-livestream { background: rgba(255, 255, 255, 0.81); box-shadow: 0 7px 9px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 rgba(0, 0, 0, 0.03); border-radius: 10px 10px 0 0; padding: 25px; } .commerce.str-chat .str-chat__header-livestream-left--title { font-size: 25px; margin: 0; line-height: 1; font-weight: 400; } .commerce.str-chat .str-chat__header-livestream-left--subtitle { margin: 8px 0; font-size: 15px; } .commerce.str-chat .str-chat__header-livestream-left--members { display: none; } .commerce.str-chat .str-chat__header-livestream-right-button--info { display: none; } .commerce.str-chat.dark .str-chat__header-livestream { background: rgba(44, 47, 51, 0.81); box-shadow: 0 7px 9px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 rgba(0, 0, 0, 0.03); border-radius: 10px 10px 0 0; color: #fff; } .team.str-chat.dark .str-chat__header-livestream { background: rgba(38, 40, 43, 0.9); box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.26); } .team.str-chat.dark .str-chat__header-livestream-left { color: #fff; } .team.str-chat.dark .str-chat__header-livestream-left--title { font-size: 18px; } .team.str-chat.dark .str-chat__header-livestream-left--members { font-size: 13px; } .str-chat__channel-list { flex: 1; overflow-y: auto; max-width: 300px; background: #f2f3f5; box-shadow: 1px 0 0 0 rgba(0, 0, 0, 0.07); display: flex; flex-direction: column; } .str-chat__channel-list--channels { flex: 1; } .str-chat__channel-list .channel_preview { padding: 8px 16px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__button { background: #fff; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.12), 0 1px 4px 0 rgba(0, 0, 0, 0.09); font-size: 14px; padding: 14px 70px; color: #006cff; display: flex; align-items: center; justify-content: center; width: calc(100% - 10px); margin: 5px; border: 1px solid transparent; } .str-chat__button:active, .str-chat__button:focus { outline: none; box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.12), 0 1px 4px 0 rgba(0, 0, 0, 0.09), 0 0 0 2px rgba(0, 108, 255, 0.36); border: 1px solid #006cff; } .str-chat__button > * { margin: 0 5px; } .str-chat__button--round { border-radius: 100px; } .str-chat-channel-checkbox { position: absolute; top: 0; right: 0; z-index: 100001; display: none; } .str-chat-channel-list-burger { width: 10px; height: 50px; background: #fff; border-radius: 0 4px 4px 0; padding: 3px; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.09); position: fixed; top: 10px; left: 0; z-index: 10000; justify-content: center; cursor: pointer; display: flex; } .str-chat-channel-list-burger div { width: 4px; height: 100%; border-radius: 3px; background: rgba(0, 0, 0, 0.08); } .str-chat-channel-list.messaging, .str-chat-channel-list.team { position: fixed; left: -380px; top: 0; z-index: 1001; min-height: 100vh; overflow-y: auto; box-shadow: 7px 0 9px 0 rgba(0, 0, 0, 0.03), 1px 0 0 0 rgba(0, 0, 0, 0.03); transition: left 200ms ease-in-out; } .str-chat-channel-checkbox:checked ~ .str-chat-channel-list.messaging, .str-chat-channel-checkbox:checked ~ .str-chat-channel-list.team { left: 0px; } .str-chat-channel-list .str-chat__channel-list-messenger { padding: 0; } .str-chat-channel-list .str-chat__channel-list-messenger__main { padding: 20px 10px 0 10px; height: 100%; overflow-y: auto; } .str-chat__channel-list-messenger { padding: 20px 10px 0 10px; min-width: 300px; height: 100%; } .str-chat__channel-list-team { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; display: flex; height: 100%; overflow-y: auto; } .str-chat__channel-list-team__sidebar { display: flex; flex-direction: column; width: 70px; padding: 45px 10px 10px; background: #dedfe2; } .str-chat__channel-list-team__sidebar--top { display: flex; flex-direction: column; justify-content: center; align-items: center; margin: -10px 0; } .str-chat__channel-list-team__sidebar--top > * { margin: 10px 0; } .str-chat__channel-list-team__sidebar--bottom { flex: 1; display: flex; align-items: center; justify-content: center; } .str-chat__channel-list-team__send-button, .str-chat__channel-list-team__search-button { background: none; margin: none; border: none; } .str-chat__channel-list-team__main { min-width: 230px; background: #f1f1f3; } .str-chat__channel-list-team__header { padding: 15px 15px 15px 20px; max-height: 70px; display: flex; align-items: center; background: rgba(255, 255, 255, 0.01); box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14); font-size: 14px; color: #000; letter-spacing: 0; position: relative; } .str-chat__channel-list-team__header--title { font-weight: 700; text-transform: capitalize; } .str-chat__channel-list-team__header--status { position: relative; padding: 0 0 0 10px; } .str-chat__channel-list-team__header--status::before { position: absolute; top: 5px; left: 0; content: ' '; width: 6px; height: 6px; border-radius: 10px; } .str-chat__channel-list-team__header--status.watcher_count::before { background: #28ca42; } .str-chat__channel-list-team__header--status.busy::before { background: #28ca42; } .str-chat__channel-list-team__header--status.away::before { background: #28ca42; } .str-chat__channel-list-team__header--left { width: 40px; } .str-chat__channel-list-team__header--middle { flex: 1; margin-left: 15px; } .str-chat__channel-list-team__header--button { margin: 0; padding: 15px 5px; border: 0; background: none; } .messenger-chat.str-chat .str-chat__channel-list-team { padding: 0 0 0 10px; } .messenger-chat.str-chat .str-chat__channel-list-team__header { background: none; box-shadow: none; } .messenger-chat.str-chat .str-chat__channel-list-team__main { background: none; } .dark.str-chat .str-chat__channel-list-team { background: #1d1f22; } .dark.str-chat .str-chat__channel-list-team__header--title { color: #fff; } .dark.str-chat .str-chat__channel-list-team__sidebar { background: rgba(0, 0, 0, 0.2); } .dark.str-chat .str-chat__channel-list-team__main { background: none; } .str-chat__channel-preview button { position: relative; border: none; background: none; display: flex; flex-direction: row; align-items: center; width: 100%; padding: 10px 40px 10px 10px; font-size: 13px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); outline: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .str-chat__channel-preview button:focus { background: #fff; box-shadow: inset 0 0 0 1px #006cff, inset 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__channel-preview-info { display: flex; flex-direction: column; text-align: left; padding: 0 10px; max-width: 250px; } .str-chat__channel-preview-avatar { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 18px; } .str-chat__channel-preview-title { color: #000; display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .str-chat__channel-preview-unread-count { position: absolute; right: 10px; width: 22px; height: 22px; color: #000; font-size: 12px; background: #d3d3d3; border-radius: 12px; display: flex; align-items: center; justify-content: center; align-self: center; } .str-chat__channel-preview-last-message { color: gray; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; } .str-chat__channel-preview--active { background: #006cff; color: #fff; } .str-chat__channel-preview--active .str-chat__channel-preview-title { color: #fff; } .str-chat__channel-preview--active .str-chat__channel-preview-last-message { color: rgba(255, 255, 255, 0.69); } .str-chat__channel-preview--unread { position: relative; } .str-chat__channel-preview--unread .str-chat__channel-preview-last-message { font-weight: 700; color: #000; } .str-chat__channel-preview--dot { width: 5px; height: 5px; position: absolute; border-radius: 50%; left: 2px; background-color: #f0f; } .str-chat__channel-preview-compact { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; display: flex; font-size: 14px; letter-spacing: 0; line-height: 40px; color: #000; position: relative; border: none; background: none; flex-direction: row; align-items: center; width: 100%; padding: 0 40px 0 10px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); text-align: left; outline: 0; } .str-chat__channel-preview-compact:focus { background: #fff; box-shadow: inset 0 0 0 1px #006cff, inset 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__channel-preview-compact--left { width: 22px; height: 22px; } .str-chat__channel-preview-compact--right { flex: 1; margin-left: 11px; } .str-chat__channel-preview-compact--unread { font-weight: 700; } .str-chat__channel-preview-compact--active { color: #fff; background: #004ab3; } .str-chat__channel-preview-messenger { display: flex; width: 100%; border: none; padding: 10px; align-items: center; background: rgba(255, 255, 255, 0); box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.07); text-align: left; margin: 5px 0; } .str-chat__channel-preview-messenger--active { border: none; border-radius: 9px; background: rgba(255, 255, 255, 0.9); box-shadow: none; box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.07); } .str-chat__channel-preview-messenger--last-message { font-size: 13px; line-height: 14px; opacity: 0.5; } .str-chat__channel-preview-messenger--name { font-size: 14px; line-height: 17px; font-weight: 600; margin-bottom: 2px; max-width: 250px; } .str-chat__channel-preview-messenger--name span { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .str-chat__channel-preview-messenger--unread .str-chat__channel-preview-messenger--last-message { opacity: 1; font-weight: 600; } .dark.str-chat .str-chat__channel-preview--active { background: #132d50; } .dark.str-chat .str-chat__channel-preview-title { color: #fff; } .dark.str-chat .str-chat__channel-preview button:focus { background: #132d50; box-shadow: inset 0 0 0 1px #006cff, inset 0 0 0 2px rgba(0, 108, 255, 0.36); } .dark.str-chat .str-chat__channel-preview-messenger--active { background: rgba(255, 255, 255, 0.06); box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.07); } .dark.str-chat .str-chat__channel-preview-messenger--last-message { color: rgba(255, 255, 255, 0.5); } .dark.str-chat .str-chat__channel-preview-messenger--name { color: #fff; } .str-chat__channel-search { margin: 10px; margin-bottom: 30px; display: flex; align-items: center; } .str-chat__channel-search input { flex: 1; background: rgba(0, 0, 0, 0.05); margin-right: 20px; border: 1px solid transparent; outline: none; height: 30px; border-radius: 15px; color: #000; font-size: 14px; padding: 0 10px; } .str-chat__channel-search input::placeholder { color: gray; } .str-chat__channel-search input:focus { background: #fff; border: 1px solid #006cff; box-shadow: 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__channel-search button { margin: 0; padding: 0 0 0 0; display: flex; align-items: center; justify-content: center; background: #fff; border: none; width: 40px; height: 40px; border-radius: 100%; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.12), 0 1px 4px 0 rgba(0, 0, 0, 0.09); cursor: pointer; outline: 0; } .str-chat__channel-search button:focus { background: #fff; border: 1px solid #006cff; box-shadow: 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__channel-search button svg { fill: #006cff; transform: translateX(2px); } .dark.str-chat .str-chat__channel-search input { background: rgba(255, 255, 255, 0.04); color: #fff; } .dark.str-chat .str-chat__channel-search button { background: #006cff; } .dark.str-chat .str-chat__channel-search button svg { fill: #fff; } .str-chat__down { display: flex; height: 100%; } .str-chat__down-main { flex: 1; padding: 30px; } .dark.str-chat .str-chat__down { color: rgba(255, 255, 255, 0.87); } .str-chat.messaging .str-chat__event-component__channel-event { display: flex; margin-top: 20px; } .str-chat.messaging .str-chat__event-component__channel-event__content { margin-right: 10px; color: rgba(0, 0, 0, 0.5); font-size: 15px; } .str-chat.messaging .str-chat__event-component__channel-event__date { font-size: 11px; margin-top: 4px; } .str-chat.team .str-chat__event-component__channel-event { display: flex; margin: 20px 40px; } .str-chat.team .str-chat__event-component__channel-event__content { margin-right: 10px; color: rgba(0, 0, 0, 0.5); font-size: 15px; } .str-chat.team .str-chat__event-component__channel-event__date { font-size: 11px; margin-top: 4px; } .str-chat.commerce .str-chat__event-component__channel-event, .str-chat.livestream .str-chat__event-component__channel-event { display: none; } .str-chat__date-separator { display: flex; margin: 40px; align-items: center; } .str-chat__date-separator-date { font-size: 14px; font-weight: 700; color: rgba(0, 0, 0, 0.7); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .str-chat__date-separator-line { flex: 1; background-color: rgba(0, 0, 0, 0.1); height: 1px; border: none; } .str-chat__date-separator > *:not(:last-child) { margin-right: 20px; } .commerce.str-chat .str-chat__date-separator { margin: 40px 0; } .dark.str-chat .str-chat__date-separator-line { background-color: rgba(255, 255, 255, 0.1); } .dark.str-chat .str-chat__date-separator-date { color: rgba(255, 255, 255, 0.7); } .dark.str-chat.team .str-chat__date-separator-line { background-color: rgba(0, 0, 0, 0.4); } .str-chat__edit-message-form { width: 100%; } .str-chat__edit-message-form form { position: relative; width: 100%; } .str-chat__edit-message-form textarea { padding: 7px; background: #fff; box-shadow: inset 0 0 0 1px #006cff; border: 1px solid transparent; resize: none; border-radius: 5px; width: 100%; font-size: 15px; line-height: 22px; } .str-chat__edit-message-form textarea:focus { box-shadow: inset 0 0 0 1px #006cff, 0 0 0 2px rgba(0, 108, 255, 0.36); outline: 0; } .str-chat__edit-message-form button { background: none; border: none; font-weight: 700; color: rgba(0, 0, 0, 0.4); } .str-chat__edit-message-form button[type='submit'] { color: #006cff; } .str-chat__edit-message-form .rfu-dropzone { width: 100%; } .str-chat__edit-message-form .rfu-file-upload-button, .str-chat__edit-message-form .str-chat__input-emojiselect, .str-chat__edit-message-form .str-chat__input-fileupload { position: unset; top: unset; right: unset; left: unset; } .str-chat__edit-message-form .rfu-file-upload-button svg, .str-chat__edit-message-form .str-chat__input-emojiselect svg, .str-chat__edit-message-form .str-chat__input-fileupload svg { fill: #000; opacity: 0.5; } .str-chat__edit-message-form .rfu-file-upload-button:hover svg, .str-chat__edit-message-form .str-chat__input-emojiselect:hover svg, .str-chat__edit-message-form .str-chat__input-fileupload:hover svg { opacity: 1; } .str-chat__edit-message-form-options { display: flex; } .str-chat.dark .str-chat__edit-message-form .rfu-file-upload-button svg, .str-chat.dark .str-chat__edit-message-form .str-chat__input-emojiselect svg { fill: #fff; } .str-chat.dark .str-chat__edit-message-form button { color: rgba(255, 255, 255, 0.4); } .str-chat.dark .str-chat__edit-message-form button[type='submit'] { color: #006cff; } .str-chat.dark .str-chat__edit-message-form textarea { background: rgba(255, 255, 255, 0.05); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.21); border: 2px solid transparent; border-radius: 6px; color: #fff; } .str-chat.dark .str-chat__edit-message-form textarea:focus { box-shadow: inset 0 0 0 1px #006cff; border: 2px solid rgba(0, 108, 255, 0.36); border-radius: 6px; } .str-chat__gallery { display: flex; flex-wrap: wrap; width: 100%; max-width: 304px; overflow: hidden; } .str-chat__gallery-image { width: 150px; height: 150px; background: #fff; margin-bottom: 1px; margin-right: 1px; } .str-chat__gallery-image:hover { cursor: -moz-zoom-in; cursor: -webkit-zoom-in; cursor: zoom-in; } .str-chat__gallery-image img { width: inherit; height: inherit; object-fit: cover; } .livestream.str-chat .str-chat__gallery, .messaging.str-chat .str-chat__gallery, .commerce.str-chat .str-chat__gallery, .team.str-chat .str-chat__gallery { margin: 5px 0; } .livestream.str-chat .str-chat__gallery-image, .messaging.str-chat .str-chat__gallery-image, .commerce.str-chat .str-chat__gallery-image, .team.str-chat .str-chat__gallery-image { width: 150px; height: 150px; } .livestream.str-chat .str-chat__gallery-placeholder, .messaging.str-chat .str-chat__gallery-placeholder, .commerce.str-chat .str-chat__gallery-placeholder, .team.str-chat .str-chat__gallery-placeholder { position: relative; width: 150px; height: 150px; color: #fff; display: flex; align-items: center; justify-content: center; cursor: -moz-zoom-in; cursor: -webkit-zoom-in; cursor: zoom-in; } .livestream.str-chat .str-chat__gallery-placeholder p, .messaging.str-chat .str-chat__gallery-placeholder p, .commerce.str-chat .str-chat__gallery-placeholder p, .team.str-chat .str-chat__gallery-placeholder p { position: relative; z-index: 1; } .livestream.str-chat .str-chat__gallery-placeholder:after, .messaging.str-chat .str-chat__gallery-placeholder:after, .commerce.str-chat .str-chat__gallery-placeholder:after, .team.str-chat .str-chat__gallery-placeholder:after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.69); z-index: 0; } .commerce.str-chat .str-chat__gallery { width: calc(100% - 30px); display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: 100px; } .commerce.str-chat .str-chat__gallery-image, .commerce.str-chat .str-chat__gallery-placeholder { width: 100%; height: 100%; } .commerce.str-chat .str-chat__message-commerce .str-chat__gallery { border-radius: 16px 16px 16px 2px; } .commerce.str-chat .str-chat__message-commerce--right .str-chat__gallery { border-radius: 16px 16px 2px 16px; } .str-chat__loading-channels { width: 300px; height: 100%; padding: 20px; background: #fafafa; } .str-chat__loading-channels-meta { flex: 1; } .str-chat__loading-channels-avatar, .str-chat__loading-channels-username, .str-chat__loading-channels-status { background-image: linear-gradient(-90deg, #f2f2f2 0%, #e5e5e5 100%); } .str-chat__loading-channels-username, .str-chat__loading-channels-status { border-radius: 2px; height: 14px; } .str-chat__loading-channels-avatar { width: 40px; height: 40px; border-radius: 100%; margin-right: 10px; } .str-chat__loading-channels-username { width: 40%; margin-bottom: 6px; } .str-chat__loading-channels-status { width: 80%; } .str-chat__loading-channels-item { display: flex; align-items: center; width: 100%; height: 40px; border-radius: 3px; margin-bottom: 10px; animation: pulsate 1s linear 0s infinite alternate; } .str-chat__loading-channels-item:nth-of-type(2) { animation: pulsate 1s linear 0.3334 infinite alternate; } .str-chat__loading-channels-item:last-of-type { animation: pulsate 1s linear 0.6667s infinite alternate; } @keyframes pulsate { from { opacity: 0.5; } to { opacity: 1; } } .str-chat__loading-indicator { display: flex; align-items: center; justify-content: center; animation: rotate 1s linear infinite; } @-webkit-keyframes rotate { from { -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes rotate { from { -ms-transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } to { -ms-transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); transform: rotate(360deg); } } .str-chat.messaging .str-chat__load-more-button__button { border: 0; width: 100%; height: 40px; border-radius: 9px; background: rgba(255, 255, 255, 0.9); box-shadow: none; box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.07); padding: 10px; font-size: 14px; } .str-chat.team .str-chat__load-more-button__button { border: 0; background: transparent; width: 100%; height: 40px; padding: 10px; font-size: 14px; } .str-chat__li { display: block; position: relative; } .str-chat__li--top, .str-chat__li--single { margin: 20px 0 0; } .str-chat__li--top .str-chat__message-attachment--img, .str-chat__li--top .str-chat__message-attachment-card, .str-chat__li--top .str-chat__message .str-chat__gallery, .str-chat__li--single .str-chat__message-attachment--img, .str-chat__li--single .str-chat__message-attachment-card, .str-chat__li--single .str-chat__message .str-chat__gallery { border-radius: 16px 16px 16px 2px; } .str-chat__li--top .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--top .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--top .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery, .str-chat__li--single .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--single .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--single .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery { border-radius: 16px 16px 16px 2px; } .str-chat__li--top .str-chat__message--me .str-chat__message-attachment--img, .str-chat__li--top .str-chat__message--me .str-chat__message-attachment-card, .str-chat__li--single .str-chat__message--me .str-chat__message-attachment--img, .str-chat__li--single .str-chat__message--me .str-chat__message-attachment-card { border-radius: 16px 16px 2px 16px; } .str-chat__li--top .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--top .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--single .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--single .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card { border-radius: 16px 16px 2px 16px; } .str-chat__li--top .str-chat__message--me .str-chat__gallery, .str-chat__li--single .str-chat__message--me .str-chat__gallery { border-radius: 16px 16px 2px 16px; } .str-chat__li--top .str-chat__message--me.str-chat__message--has-text .str-chat__gallery, .str-chat__li--single .str-chat__message--me.str-chat__message--has-text .str-chat__gallery { border-radius: 16px 16px 2px 16px; } .str-chat__li--middle { margin: 0; } .str-chat__li--middle .str-chat__message-attachment--img, .str-chat__li--middle .str-chat__message-attachment-card, .str-chat__li--middle .str-chat__message .str-chat__gallery { border-radius: 2px 16px 16px 2px; } .str-chat__li--middle .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--middle .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--middle .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery { border-radius: 2px 16px 16px 2px; } .str-chat__li--middle .str-chat__message--me .str-chat__message-attachment--img, .str-chat__li--middle .str-chat__message--me .str-chat__message-attachment-card, .str-chat__li--middle .str-chat__message--me .str-chat__message .str-chat__gallery { border-radius: 16px 2px 2px 16px; } .str-chat__li--middle .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--middle .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--middle .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery { border-top-left-radius: 2px; } .str-chat__li--bottom { margin: 0 0 20px; } .str-chat__li--bottom .str-chat__message-attachment--img, .str-chat__li--bottom .str-chat__message-attachment-card, .str-chat__li--bottom .str-chat__message .str-chat__gallery { border-radius: 2px 16px 16px 2px; } .str-chat__li--bottom .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--bottom .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--bottom .str-chat__message.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery { border-radius: 2px 16px 16px 2px; } .str-chat__li--bottom .str-chat__message--me .str-chat__message-attachment--img, .str-chat__li--bottom .str-chat__message--me .str-chat__message-attachment-card, .str-chat__li--bottom .str-chat__message--me .str-chat__message .str-chat__gallery { border-radius: 16px 2px 2px 16px; } .str-chat__li--bottom .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment--img, .str-chat__li--bottom .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__message-attachment-card, .str-chat__li--bottom .str-chat__message--me.str-chat__message--has-text.str-chat__message--has-attachment .str-chat__gallery { border-top-left-radius: 2px; } .str-chat__li--single { margin-bottom: 20px; } .str-chat__li--top .str-chat__message-data, .str-chat__li--middle .str-chat__message-data { display: none; } .str-chat__li--top .str-chat__message-text-inner { border-radius: 16px 16px 16px 2px; } .str-chat__li--top .str-chat__message--me .str-chat__message-text-inner { border-radius: 16px 16px 2px 16px; } .str-chat__li--single .str-chat__message-text-inner { border-radius: 16px 16px 16px 2px; } .str-chat__li--single .str-chat__message-text-inner--has-attachment { border-radius: 2px 16px 16px 2px; } .str-chat__li--single .str-chat__message--me .str-chat__message-text-inner { border-radius: 16px 16px 2px 16px; } .str-chat__li--single .str-chat__message--me .str-chat__message-text-inner--has-attachment { border-radius: 16px 2px 2px 16px; } .str-chat__li--bottom .str-chat__message-text-inner, .str-chat__li--middle .str-chat__message-text-inner { border-radius: 2px 16px 16px 2px; } .str-chat__li--bottom .str-chat__message--me .str-chat__message-text-inner, .str-chat__li--middle .str-chat__message--me .str-chat__message-text-inner { border-radius: 16px 2px 2px 16px; } .str-chat__li--bottom .str-chat__message--me .str-chat__message-text-inner--has-attachment, .str-chat__li--middle .str-chat__message--me .str-chat__message-text-inner--has-attachment { margin: 0; } .str-chat__li--bottom .str-chat__message--me .str-chat__message-attachment-card, .str-chat__li--middle .str-chat__message--me .str-chat__message-attachment-card { margin: 0; padding: 0; border-radius: 16px 2px 2px 16px; } .str-chat__message, .str-chat__message-simple { display: inline-flex; justify-content: flex-start; align-items: flex-end; padding: 0 0 0 0; position: relative; margin: 1px 0; } .str-chat__message--system, .str-chat__message-simple--system { text-align: center; align-items: center; width: 100%; flex-direction: column; padding: 0 40px; margin: 40px 0; font-size: 10px; } .str-chat__message--system__text, .str-chat__message-simple--system__text { display: flex; align-items: center; width: 100%; } .str-chat__message--system__text p, .str-chat__message-simple--system__text p { margin: 0 25px; color: rgba(0, 0, 0, 0.5); text-transform: uppercase; font-weight: bold; } .str-chat__message--system__line, .str-chat__message-simple--system__line { flex: 1; height: 1px; width: 100%; background-color: rgba(0, 0, 0, 0.1); } .str-chat__message--system__date, .str-chat__message-simple--system__date { margin-top: 3px; text-transform: uppercase; color: rgba(0, 0, 0, 0.5); } .str-chat__message-inner, .str-chat__message-simple-inner { position: relative; } .str-chat__message-inner > .str-chat__message-simple__actions, .str-chat__message-simple-inner > .str-chat__message-simple__actions { position: absolute; top: 5px; left: 100%; } .str-chat__message-attachment-container, .str-chat__message-simple-attachment-container { display: flex; flex-direction: column; } .str-chat__message-text, .str-chat__message-simple-text { display: inline-flex; justify-content: flex-start; padding: 0 0 0 0; position: relative; } .str-chat__message-text-inner, .str-chat__message-simple-text-inner { position: relative; flex: 1; display: block; min-height: 32px; padding: 5px 10px; font-size: 15px; color: #000; border-radius: 16px 16px 16px 0; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); margin-left: 0; } .str-chat__message-text-inner p, .str-chat__message-simple-text-inner p { overflow-wrap: break-word; word-wrap: break-word; -ms-hyphens: auto; -moz-hyphens: auto; -webkit-hyphens: auto; hyphens: auto; } .str-chat__message-text-inner--focused, .str-chat__message-simple-text-inner--focused { background: #bcd8ff; border: 1px solid #006cff; margin-right: 0; margin-left: 0; } .str-chat__message-text-inner--has-attachment, .str-chat__message-simple-text-inner--has-attachment { border-radius: 2px 16px 16px 2px; } .str-chat__message-text-inner--is-emoji, .str-chat__message-simple-text-inner--is-emoji { background: transparent; border: 1px solid transparent; font-size: 32px; padding-left: 0; padding-right: 0; } .str-chat__message-attachment--img, .str-chat__message-simple-attachment--img { width: 100%; max-width: 480px; display: block; height: inherit; cursor: -moz-zoom-in; cursor: -webkit-zoom-in; cursor: zoom-in; } .str-chat__message-data, .str-chat__message-simple-data { margin-top: 5px; width: 100%; font-size: 11px; color: rgba(0, 0, 0, 0.5); } .str-chat__message-name, .str-chat__message-simple-name { font-weight: 700; margin-right: 5px; } .str-chat__message p, .str-chat__message-simple p { margin: 0; white-space: pre-line; line-height: 20px; } .str-chat__message p:not(:first-of-type), .str-chat__message-simple p:not(:first-of-type) { margin: 16px 0 0; } .str-chat__message--me, .str-chat__message-simple--me { display: flex; margin: 4px 0; justify-content: flex-end; } .str-chat__message--me .str-chat__message-text, .str-chat__message-simple--me .str-chat__message-text { display: flex; justify-content: flex-end; } .str-chat__message--me .str-chat__message-attachment-container, .str-chat__message-simple--me .str-chat__message-attachment-container { display: flex; flex-direction: column; align-items: flex-end; } .str-chat__message--me .str-chat__message-inner, .str-chat__message-simple--me .str-chat__message-inner { justify-content: flex-end; align-items: flex-end; } .str-chat__message--me .str-chat__message-inner > .str-chat__message-simple__actions, .str-chat__message-simple--me .str-chat__message-inner > .str-chat__message-simple__actions { position: absolute; top: 5px; left: unset; right: 100%; } .str-chat__message--me .str-chat__message-text-inner, .str-chat__message-simple--me .str-chat__message-text-inner { flex: initial; background: #ebebeb; border-color: transparent; text-align: right; border-radius: 16px 16px 2px 16px; margin-right: 0; } .str-chat__message--me .str-chat__message-text-inner--focused, .str-chat__message-simple--me .str-chat__message-text-inner--focused { background: #bcd8ff; border: 1px solid #006cff; margin-left: 0; margin-right: 0; } .str-chat__message--me .str-chat__message-text-inner--has-attachment, .str-chat__message-simple--me .str-chat__message-text-inner--has-attachment { border-radius: 16px 2px 2px 16px; } .str-chat__message--me .str-chat__message-text-inner--is-emoji, .str-chat__message-simple--me .str-chat__message-text-inner--is-emoji { background: transparent; border: 1px solid transparent; font-size: 32px; padding-left: 0; padding-right: 0; } .str-chat__message--me .str-chat__message-text-inner--is-emoji p, .str-chat__message-simple--me .str-chat__message-text-inner--is-emoji p { line-height: 48px; } .str-chat__message--me .str-chat__message-attachment--img, .str-chat__message-simple--me .str-chat__message-attachment--img { width: 100%; max-width: 480px; display: block; height: inherit; object-fit: cover; border: none; } .str-chat__message--me .str-chat__message-data, .str-chat__message-simple--me .str-chat__message-data { text-align: right; } .str-chat__message--with-reactions, .str-chat__message-simple--with-reactions { margin-top: 30px; } .str-chat__message-link, .str-chat__message-simple-link { color: #f0f; font-weight: 700; text-decoration: none; } .str-chat__message-mention, .str-chat__message-simple-mention { margin: 0; padding: 0; background: none; border: none; font-size: 16px; color: #f0f; font-weight: 700; } .str-chat__message--inner, .str-chat__message-simple--inner { display: flex; flex-direction: column; align-items: flex-start; } .str-chat__message--deleted, .str-chat__message-simple--deleted { margin: 0 0 0 42px; flex-direction: column; align-items: flex-start; } .str-chat__message--deleted-inner, .str-chat__message-simple--deleted-inner { background: rgba(255, 255, 255, 0.1); padding: 8px 16px; border-radius: 16px; font-size: 12px; color: #a4a4a4; } .str-chat__message--me.str-chat__message--deleted, .str-chat__message-simple--me.str-chat__message--deleted { margin: 0 42px 0 0; align-items: flex-end; } .str-chat__message--me.str-chat__message--deleted-inner, .str-chat__message-simple--me.str-chat__message--deleted-inner { background: rgba(255, 255, 255, 0.1); padding: 8px 16px; border-radius: 16px; font-size: 12px; color: #a4a4a4; } .str-chat__message--error, .str-chat__message--failed, .str-chat__message-simple--error, .str-chat__message-simple--failed { margin: 0 0 32px 42px; font-size: 12px; padding: 4px 0; } .str-chat__message--error .str-chat__message-text-inner, .str-chat__message--failed .str-chat__message-text-inner, .str-chat__message-simple--error .str-chat__message-text-inner, .str-chat__message-simple--failed .str-chat__message-text-inner { background: rgba(208, 2, 27, 0.1); border: 1px solid rgba(208, 2, 27, 0.1); } .str-chat__message--me.str-chat__message--error, .str-chat__message--me.str-chat__message--failed, .str-chat__message-simple--me.str-chat__message--error, .str-chat__message-simple--me.str-chat__message--failed { border-left: initial; margin-right: 42px; } .str-chat__message .str-chat__message-attachment-file--item:hover, .str-chat__message-simple .str-chat__message-attachment-file--item:hover { background: transparent; } .messaging.str-chat .str-chat__message, .messaging.str-chat .str-chat__message--me { margin: 1px 0; } .messaging.str-chat .str-chat__message--with-reactions, .messaging.str-chat .str-chat__message--me--with-reactions { margin-top: 30px; } .messaging.str-chat .str-chat__message-attachment--image { margin: 1px 0; max-width: 480px; } .messaging.str-chat .str-chat__message-attachment--card { margin: 1px 0; line-height: normal; } .messaging.str-chat .str-chat__message-attachment-card { margin: 1px auto; line-height: normal; } .messaging.str-chat .str-chat__message-attachment-card--title { color: #006cff; } .messaging.str-chat .str-chat__message-attachment-card--text { display: none; } .messaging.str-chat .str-chat__message-attachment-card--url { text-transform: lowercase; } .messaging.str-chat .str-chat__message--deleted { margin: 0 42px; } .messaging.str-chat .str-chat__li--middle .str-chat__message .str-chat__message-attachment--card, .messaging.str-chat .str-chat__li--middle .str-chat__message .str-chat__message-attachment--image { border-top-left-radius: 2px; } .str-chat__message-simple { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .str-chat__message-simple__actions { display: flex; margin-top: 5px; align-items: flex-start; justify-content: flex-start; min-width: 90px; } .str-chat__message-simple__actions__action { margin: 5px; display: flex; align-items: center; height: 10px; cursor: pointer; } .str-chat__message-simple__actions__action svg { fill: #000; opacity: 0.5; } .str-chat__message-simple__actions__action:hover svg { opacity: 1; } .str-chat__message-simple__actions__action--thread, .str-chat__message-simple__actions__action--reactions { display: none; } .str-chat__message-simple__actions__action--options { position: relative; display: none; } .str-chat__message-simple.str-chat__message--with-reactions .str-chat__message-simple__actions__action--reactions { display: flex; } .str-chat__message-simple:hover .str-chat__message-simple__actions__action--thread { display: flex; } .str-chat__message-simple:hover .str-chat__message-simple__actions__action--reactions { display: flex; } .str-chat__message-simple-text { display: flex; justify-content: flex-end; padding: 0 0 0 0; position: relative; } .str-chat__message-simple-text-inner { text-align: left; max-width: 460px; } .str-chat__message-simple-text-inner.str-chat__message-simple-text-inner--is-emoji { margin: 5px 0; background: transparent; } .str-chat__message-simple-text-inner.str-chat__message-simple-text-inner--is-emoji p { line-height: 48px; } .str-chat__message-simple-text-inner p { text-align: left; } .str-chat__message-simple-text-inner a { color: #006cff; font-weight: 700; text-decoration: none; } .str-chat__message-simple-text-inner blockquote { margin: 0 0 0 5px; font-style: italic; padding-left: 20px; position: relative; } .str-chat__message-simple-text-inner blockquote::before { font-size: 25px; content: '“'; font-style: italic; position: absolute; opacity: 0.5; top: 2px; left: -5px; } .str-chat__message-simple--me .str-chat__message-simple-reply-button { display: flex; justify-content: flex-end; } .str-chat__message-simple--me .str-chat__message-simple-reply-button .str-chat__message-replies-count-button { display: flex; flex-direction: row-reverse; } .str-chat__message-simple--me .str-chat__message-simple-reply-button .str-chat__message-replies-count-button svg { transform: scaleX(-1); margin-left: 5px; margin-bottom: 4px; margin-right: 0; } .str-chat__message-simple--me .str-chat__message-simple__actions { justify-content: flex-end; } .str-chat__message-simple--me .str-chat__message-attachment--image { max-width: 460px; } .str-chat__message-simple--me-text-inner--is-emoji { background-color: transparent; line-height: 32px; } .str-chat__message-simple--me .str-chat__message-simple__actions { order: -1; } .str-chat__message-simple:hover .str-chat__message-simple__actions__action--options { display: flex; } .str-chat__message-simple:hover .str-chat__message-simple__actions__action--reactions { display: flex; } .str-chat__message-simple:hover .str-chat__message-simple__actions__action--thread { display: flex; } .str-chat__simple-message--error-message { text-align: left; text-transform: uppercase; font-size: 11px; opacity: 0.5; } .str-chat__message-simple-status { margin: 10px 0px 10px 10px; order: 3; position: absolute; left: 100%; bottom: 0; line-height: 1; display: flex; justify-content: flex-end; align-items: center; z-index: 11; } .str-chat__message-simple-status-number { font-size: 10px; margin-left: 4px; position: absolute; left: 100%; color: rgba(0, 0, 0, 0.6); } .str-chat__message-simple-status > .str-chat__avatar { align-self: flex-end; margin-right: 0; } .str-chat__message-simple-status > .str-chat__tooltip { display: none; max-width: 300px; min-width: 100px; text-align: center; } .str-chat__message-simple-status:hover > .str-chat__tooltip { display: block; } .str-chat__message-simple-status::after { position: absolute; bottom: 100%; right: 0; content: ' '; width: 15px; height: 15px; } .str-chat__message-simple .str-chat__message-attachment-card { margin: 0; border-radius: 4px 16px 4px 4px; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__message-simple .str-chat__message-attachment-card--content { background: #ebebeb; } .str-chat__message-simple .str-chat__message-attachment-card--text { display: none; } .str-chat__message-simple .str-chat__message-attachment--file { margin: 0; background: #fff; border-color: transparent; border: 1px solid #ebebeb; border-radius: 0; } .str-chat__message-simple .str-chat__message-attachment--file .str-chat__message-attachment-file--item { border-color: transparent; padding: 0 10px; } .str-chat__message-simple .str-chat__message-attachment--file:first-of-type { border-radius: 16px 16px 0 0; border-bottom: transparent; } .str-chat__message-simple .str-chat__message-attachment--file:last-of-type { border-top-color: transparent; border-radius: 0 0 16px 2px; } .str-chat__message-simple .str-chat__message-attachment--file:last-of-type:first-of-type { border-bottom: 1px solid #ebebeb; border-top: 1px solid #ebebeb; border-radius: 16px 16px 16px 2px; } .str-chat__message-simple .str-chat__message-attachment-file--item { border-radius: 0; } .str-chat__message-simple--me .str-chat__message-attachment-card { border-radius: 16px 4px 4px 4px; } .str-chat__message-simple--me .str-chat__message-attachment--file { background: #ebebeb; } .str-chat__message-simple--me .str-chat__message-attachment--file:last-of-type { border-radius: 0 0 2px 16px; } .str-chat__message-simple--me .str-chat__message-attachment--file:last-of-type:first-of-type { border-radius: 16px 16px 2px 16px; } .str-chat__list--thread .str-chat__message-simple__actions { width: 30px; } .str-chat__list--thread .str-chat__message-simple__actions__action--options .str-chat__message-actions-box { right: unset; left: 100%; border-radius: 16px 16px 16px 2px; } .livestream.str-chat .str-chat__li--single { margin: 0px 0; } .str-chat__message-simple-text-inner { max-width: 218px; } .str-chat__message-simple-status { left: unset; right: 8px; bottom: 30px; } .dark.str-chat .str-chat__message-simple-text-inner { background: rgba(255, 255, 255, 0.05); color: #fff; } .dark.str-chat .str-chat__message-simple-text-inner--is-emoji { background: transparent; } .dark.str-chat .str-chat__message-simple__actions svg { fill: #fff; } .dark.str-chat .str-chat__message-simple-data { color: #fff; opacity: 0.5; } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment-card { background: transparent; } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment-card--content { background: rgba(255, 255, 255, 0.05); } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment-card--url { color: rgba(255, 255, 255, 0.5); } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment--file { border-color: transparent; background: rgba(255, 255, 255, 0.05); } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment--file a, .dark.str-chat .str-chat__message-simple .str-chat__message-attachment--file span { color: #fff; } .dark.str-chat .str-chat__message-simple .str-chat__message-attachment--file span { opacity: 0.5; } .dark.str-chat .str-chat__message-simple .str-chat__message-simple-status-number { color: rgba(255, 255, 255, 0.6); } .dark.str-chat .str-chat__message-simple--me .str-chat__message-simple-text-inner { background: rgba(0, 0, 0, 0.2); } .dark.str-chat .str-chat__message-simple--me .str-chat__message-simple-text-inner--is-emoji { background: transparent; } .dark.str-chat .str-chat__message-simple--me .str-chat__message-simple .str-chat__message-attachment-card--content { background: rgba(0, 0, 0, 0.2); } .dark.str-chat .str-chat__message-simple--me .str-chat__message-simple .str-chat__message-attachment--file { background: rgba(0, 0, 0, 0.2); } .dark.str-chat .str-chat__message-simple__actions__action--options .str-chat__actions-box { background: #67686a; box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.22), 0 1px 0 0 rgba(0, 0, 0, 0.08), 0 1px 8px 0 rgba(0, 0, 0, 0.05); } .dark.str-chat .str-chat__message--error .str-chat__message-simple-text-inner, .dark.str-chat .str-chat__message--failed .str-chat__message-simple-text-inner { background: rgba(208, 2, 27, 0.1); border: 1px solid rgba(208, 2, 27, 0.1); } .str-chat__message .str-chat__message-simple__actions__action--options .str-chat__message-actions-box { left: 100%; right: unset; } .str-chat__message .str-chat__message-simple__actions__action--options .str-chat__message-actions-box--reverse { right: 100%; left: unset; border-radius: 16px 16px 2px 16px; } .str-chat__message .str-chat__message-simple__actions__action--options .str-chat__message-actions-box--mine { right: 100%; left: unset; border-radius: 16px 16px 2px 16px; } .str-chat__message .str-chat__message-simple__actions__action--options .str-chat__message-actions-box--mine.str-chat__message-actions-box--reverse { left: 100%; right: unset; border-radius: 16px 16px 16px 2px; } .str-chat__message .str-chat__message-attachment--img { max-width: 240px; } .str-chat__message-actions-box { position: absolute; display: none; bottom: 20px; left: 40px; width: 120px; border-radius: 16px 16px 16px 0; background: #fff; background-image: linear-gradient(-180deg, rgba(255, 255, 255, 0.02) 0%, rgba(0, 0, 0, 0.02) 100%); box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.22), 0 1px 0 0 rgba(0, 0, 0, 0.08), 0 1px 8px 0 rgba(0, 0, 0, 0.05); z-index: 999; } .str-chat__message-actions-box--open { display: block; } .str-chat__message-actions-list { height: 100%; margin: 0; padding: 0; list-style-type: none; display: flex; flex-direction: column; align-items: flex-start; } .str-chat__message-actions-list-item { padding: 8px 10px; width: 100%; margin: 0; } .str-chat__message-actions-list button { background: none; text-align: left; outline: none; border: none; cursor: pointer; display: block; width: 100%; font-size: 12px; color: #000; text-decoration: none; } .str-chat__message-actions-list button:hover { color: #006cff; } .str-chat__message-actions-list button:not(:last-of-type) { border-bottom: 1px solid rgba(0, 0, 0, 0.07); } .str-chat__message-actions { position: relative; align-self: flex-start; display: flex; align-items: center; justify-content: flex-end; margin: 5px 10px; cursor: pointer; } .str-chat__message-actions { order: 2; } .str-chat__message--me .str-chat__message-actions { order: -1; } .str-chat__message-actions-reactions, .str-chat__message-actions-options { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; } .str-chat__message-actions-reactions, .str-chat__message-actions-options svg { fill: gray; position: relative; } .str-chat__message-actions-reactions:hover, .str-chat__message-actions-options:hover svg { fill: #f0f; } .str-chat__message-reactions-box { position: absolute; visibility: hidden; bottom: 30px; left: -20px; background: rgba(0, 0, 0, 0.81); background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); border-radius: 50px; padding: 16px; z-index: 100; } .str-chat__message-reactions-box--open { visibility: visible; } .str-chat__message-reactions-box::after { top: 97%; left: 30px; content: url('data:image/svg+xml; utf8, <svg width="36" height="19" viewBox="0 0 36 19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="b"><stop stop-opacity="0" offset="0%"/><stop stop-opacity=".5" offset="100%"/></linearGradient><path d="M35.284.378A15.125 15.125 0 0 0 22.81 6.946L14.55 19a14.422 14.422 0 0 0-2.223-12.786A14.456 14.456 0 0 0 .723.378h34.561z" id="a"/></defs><g fill-rule="nonzero" fill="none"><use fill="%231E1E1E" xlink:href="%23a"/><use fill="url(%23b)" xlink:href="%23a"/></g></svg>'); position: absolute; z-index: 99; } .str-chat__message-commerce { display: flex; justify-content: flex-start; align-items: flex-end; padding: 0 0 0 0; position: relative; margin: 1px 0; } .str-chat__message-commerce-inner { position: relative; } .str-chat__message-commerce-inner > .str-chat__message-commerce__actions { min-height: 10px; min-width: 30px; float: right; } .str-chat__message-commerce-inner > .str-chat__message-commerce__actions .str-chat__reaction-list { left: unset; right: 46px; } .str-chat__message-commerce .str-chat__avatar { margin-right: 8px; } .str-chat__message-commerce .str-chat__message-attachment-card--content { margin: 0; padding: 6px 8px; } .str-chat__message-commerce--top, .str-chat__message-commerce--middle { margin-left: 40px; } .str-chat__message-commerce--top .str-chat__message-commerce-data, .str-chat__message-commerce--middle .str-chat__message-commerce-data { display: none; } .str-chat__message-commerce--top .str-chat__message-commerce-text-inner { border-radius: 16px 16px 4px 4px; } .str-chat__message-commerce--bottom .str-chat__message-commerce-text-inner { border-radius: 4px 4px 16px 0; } .str-chat__message-commerce--single .str-chat__message-commerce-text-inner { border-radius: 16px 16px 16px 0; } .str-chat__message-commerce--single .str-chat__message-commerce-text-inner--has-attachment { border-radius: 4px 4px 16px 0; } .str-chat__message-commerce--middle .str-chat__message-commerce-text-inner { border-radius: 4px 4px 4px 4px; } .str-chat__message-commerce-text { display: flex; padding: 0 0 0 0; position: relative; } .str-chat__message-commerce-text-inner { position: relative; display: block; min-height: 32px; padding: 5px 10px; font-size: 15px; color: #000; border-radius: 16px 16px 16px 0; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); margin-left: 0; max-width: 345px; } .str-chat__message-commerce-text-inner p { margin: 0; white-space: pre-line; overflow-wrap: break-word; word-wrap: break-word; -ms-word-break: break-all; word-break: break-word; -ms-hyphens: auto; -moz-hyphens: auto; -webkit-hyphens: auto; hyphens: auto; } .str-chat__message-commerce-text-inner p:not(:first-of-type) { margin: 16px 0 0; } .str-chat__message-commerce-text-inner--has-attachment { border-radius: 2px 16px 16px 2px; } .str-chat__message-commerce-text-inner--is-emoji { background: transparent; border: 1px solid transparent; font-size: 32px; line-height: 48px; padding-left: 0; padding-right: 0; } .str-chat__message-commerce-attachment--img { width: 100%; max-width: 480px; display: block; height: inherit; cursor: -moz-zoom-in; cursor: -webkit-zoom-in; cursor: zoom-in; } .str-chat__message-commerce-data { margin-top: 5px; width: 100%; font-size: 11px; color: rgba(0, 0, 0, 0.5); } .str-chat__message-commerce-name { font-weight: 700; margin-right: 5px; } .str-chat__message-commerce p { margin: 0; line-height: 20px; } .str-chat__message-commerce--with-reactions { margin-top: 30px; } .str-chat__message-commerce--with-reactions .str-chat__message-commerce__actions__action--reactions { display: flex; } .str-chat__message-commerce-link { color: #f0f; font-weight: 700; text-decoration: none; } .str-chat__message-commerce-mention { margin: 0; padding: 0; background: none; border: none; font-size: 16px; color: #f0f; font-weight: 700; } .str-chat__message-commerce--inner { display: flex; flex-direction: column; align-items: flex-start; } .str-chat__message-commerce--deleted { background: #f0f0f0; float: left; padding: 8px 16px; border-radius: 16px; font-size: 12px; color: #a4a4a4; margin: 0 0 0 42px; } .str-chat__message-commerce--error { margin: 0 0 32px 0; font-size: 12px; } .str-chat__message-commerce--error .str-chat__message-text-inner { background: rgba(208, 2, 27, 0.1); border: 1px solid rgba(208, 2, 27, 0.1); } .str-chat__message-commerce--right.str-chat__message-commerce { justify-content: flex-end; margin-left: 0; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce__actions { justify-content: flex-end; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-inner > .str-chat__message-commerce__actions { float: left; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-inner > .str-chat__message-commerce__actions .str-chat__reaction-list { left: 46px; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-inner > .str-chat__message-commerce-reply-button { display: flex; justify-content: flex-end; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-inner > .str-chat__message-commerce-reply-button .str-chat__message-replies-count-button { display: flex; flex-direction: row-reverse; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-inner > .str-chat__message-commerce-reply-button .str-chat__message-replies-count-button svg { transform: scaleX(-1); margin-left: 5px; margin-bottom: 4px; margin-right: 0; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-text-inner { background: #ebebeb; border-width: 0px; margin-top: 2px; border-color: transparent; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-text-inner p { text-align: right; } .str-chat__message-commerce--right.str-chat__message-commerce > .str-chat__avatar { display: none; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-attachment { margin: 0 auto 0 30px; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-attachment--img { border-radius: 16px 16px 2px 16px; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-attachment-card { border-radius: 16px 16px 4px 16px; } .str-chat__message-commerce--right.str-chat__message-commerce--bottom, .str-chat__message-commerce--right.str-chat__message-commerce--single { margin-right: 0; } .str-chat__message-commerce--right.str-chat__message-commerce--single .str-chat__message-commerce-text-inner { border-radius: 16px 16px 4px 16px; } .str-chat__message-commerce--right.str-chat__message-commerce--single .str-chat__message-commerce-text-inner--has-attachment { border-radius: 16px 4px 4px 16px; } .str-chat__message-commerce--right.str-chat__message-commerce--bottom .str-chat__message-commerce-text-inner { border-radius: 4px 4px 4px 16px; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__avatar { order: 1; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-text { flex-direction: row-reverse; justify-content: flex-start; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-text-inner { flex: unset; } .str-chat__message-commerce--right.str-chat__message-commerce .str-chat__message-commerce-data { text-align: right; } .str-chat__message-commerce--has-text .str-chat__message-commerce-inner .str-chat__message-attachment { width: 100%; height: auto; margin: 4px auto; } .str-chat__message-commerce { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .str-chat__message-commerce__actions { display: flex; margin-top: 5px; align-items: flex-start; justify-content: flex-start; min-width: 30px; } .str-chat__message-commerce__actions__action { margin: 5px; display: flex; align-items: center; height: 10px; cursor: pointer; } .str-chat__message-commerce__actions__action svg { fill: #000; opacity: 0.5; } .str-chat__message-commerce__actions__action:hover svg { opacity: 1; } .str-chat__message-commerce__actions__action--thread, .str-chat__message-commerce__actions__action--reactions { display: none; } .str-chat__message-commerce__actions__action--options { position: relative; display: none; } .str-chat__message-commerce__actions__action--options .str-chat__message-actions-box { bottom: 10px; left: unset; right: 100%; width: 120px; border-radius: 16px 16px 2px 16px; } .str-chat__message-commerce.str-chat__message--with-reactions .str-chat__message-commerce__actions__action--reactions { display: flex; } .str-chat__message-commerce:hover .str-chat__message-commerce__actions__action--thread { display: flex; } .str-chat__message-commerce:hover .str-chat__message-commerce__actions__action--reactions { display: flex; } .str-chat__message-commerce-text { display: flex; padding: 0 0 0 0; position: relative; } .str-chat__message-commerce-text-inner { text-align: left; } .str-chat__message-commerce-text-inner.str-chat__message-commerce-text-inner--is-emoji { margin: 5px 0; background: transparent; } .str-chat__message-commerce-text-inner p { text-align: left; } .str-chat__message-commerce-text-inner a { color: #006cff; font-weight: 700; text-decoration: none; } .str-chat__message-commerce-text-inner blockquote { margin: 0 0 0 5px; font-style: italic; padding-left: 20px; position: relative; } .str-chat__message-commerce-text-inner blockquote::before { font-size: 25px; content: '“'; font-style: italic; position: absolute; opacity: 0.5; top: 2px; left: -5px; } .str-chat__message-commerce:hover .str-chat__message-commerce__actions__action--options { display: flex; } .str-chat__message-commerce:hover .str-chat__message-commerce__actions__action--reactions { display: flex; } .str-chat__message-commerce:hover .str-chat__message-commerce__actions__action--thread { display: flex; } .str-chat__commerce-message--error-message { text-align: left; text-transform: uppercase; font-size: 11px; opacity: 0.5; } .str-chat__message-commerce-status { margin: 10px 0px 10px 10px; order: 3; position: absolute; left: 100%; bottom: 0; line-height: 1; display: flex; justify-content: flex-end; align-items: center; z-index: 11; } .str-chat__message-commerce-status-number { font-size: 10px; margin-left: 4px; position: absolute; left: 100%; color: rgba(0, 0, 0, 0.6); } .str-chat__message-commerce-status > .str-chat__avatar { align-self: flex-end; margin-right: 0; } .str-chat__message-commerce-status > .str-chat__tooltip { display: none; max-width: 300px; min-width: 100px; text-align: center; } .str-chat__message-commerce-status:hover > .str-chat__tooltip { display: block; } .str-chat__message-commerce-status::after { position: absolute; bottom: 100%; right: 0; content: ' '; width: 15px; height: 15px; } .str-chat__message-commerce .str-chat__message-attachment { width: calc(100% - 30px); max-width: unset; border-radius: unset; margin: 0 auto 0 0; } .str-chat__message-commerce .str-chat__message-attachment-card { margin: 0; border-radius: 4px 16px 4px 4px; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__message-commerce .str-chat__message-attachment-card--content { background: #ebebeb; } .str-chat__message-commerce .str-chat__message-attachment-card--text { display: none; } .str-chat__list--thread .str-chat__message-commerce__actions { width: 30px; } .str-chat__list--thread .str-chat__message-commerce__actions__action--options .str-chat__message-actions-box { right: unset; left: 100%; border-radius: 16px 16px 16px 2px; } .str-chat.dark .str-chat__message-commerce-data { color: #fff; opacity: 0.5; } .str-chat.dark .str-chat__message-commerce-text-inner { background: rgba(255, 255, 255, 0.05); color: #fff; } .str-chat.dark .str-chat__message-commerce__actions svg { fill: #fff; } .str-chat.dark .str-chat__message-commerce .str-chat__message-attachment-card { background: rgba(0, 0, 0, 0.2); } .str-chat.dark .str-chat__message-commerce .str-chat__message-attachment-card--content { background: rgba(0, 0, 0, 0.2); } .str-chat.dark .str-chat__message-commerce .str-chat__message-attachment-card--title { color: #fff; } .str-chat.dark .str-chat__message-commerce .str-chat__message-attachment-card--url { color: rgba(255, 255, 255, 0.5); } .str-chat.dark .str-chat__message-commerce--right .str-chat__message-commerce-text-inner { background: rgba(0, 0, 0, 0.2); } .str-chat.dark .str-chat__message-commerce--right .str-chat__message-commerce .str-chat__message-attachment-card { background: rgba(0, 0, 0, 0.2); } .str-chat.dark .str-chat__message-commerce--right .str-chat__message-commerce .str-chat__message-attachment-card--content { background: rgba(0, 0, 0, 0.2); } .str-chat__input { background: #fff; box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05), 0 -1px 0 0 rgba(0, 0, 0, 0.07); padding: 10px; position: relative; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding-bottom: 0; } .str-chat__input--emojipicker { position: absolute; bottom: 100%; right: 0; } .str-chat__input-emojiselect { right: 20px; } .str-chat__input-fileupload { right: 46px; } .str-chat__input-emojiselect, .str-chat__input-fileupload { position: absolute; cursor: pointer; top: calc(100% - 35px); width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; background-size: 44px 44px; fill: rgba(0, 0, 0, 0.6); } .str-chat__input-emojiselect:hover, .str-chat__input-fileupload:hover { fill: #000; } .str-chat__input-footer { display: flex; justify-content: space-between; font-size: 12px; background: #fff; padding: 0 10px 10px 10px; color: gray; } .str-chat__input-footer--typing { font-style: italic; } .str-chat__input-footer--count--hidden { visibility: hidden; } .str-chat__textarea { height: auto; } .str-chat__textarea textarea { width: 100%; outline: none; padding: 11px; background: #fff; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 10px; font-size: 15px; min-height: 42px; transition: height 100ms ease-in; resize: none; } .str-chat__textarea textarea:focus { background: #fff; border: 1px solid #006cff; box-shadow: 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__textarea textarea:placeholder { color: rgba(0, 0, 0, 0.5); } .str-chat__emojisearch { bottom: calc(100%); left: 0; width: calc(100% - 20px); position: absolute; background: rgba(240, 240, 240, 0.95); box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.31), 0 0 6px 0 rgba(0, 0, 0, 0.12); z-index: -1; border-radius: 4px 4px 0 0; margin: 0 10px; } .str-chat__emojisearch__list { margin: 0; padding: 0; list-style-type: none; border-radius: 4px 4px 0 0; } .dark.str-chat .str-chat__emojisearch { background: #35373a; box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.31), 0 0 6px 0 rgba(0, 0, 0, 0.12); border-radius: 4px 4px 0 0; } .dark.str-chat .str-chat__emojisearch .rta__list-header { background: #1b1d20; border: 1px solid rgba(224, 224, 224, 0.03); box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.07); color: #fff; } .dark.str-chat .str-chat__emojisearch .rta__entity { color: #fff; } .dark.str-chat .rfu-file-previewer__file a { color: #fff; } .dark.str-chat .rfu-file-previewer__file:hover { background: transparent; } .dark.str-chat .rfu-file-previewer__close-button { color: #fff; } .rta { font-size: 14px; } .rta__entity--selected { background-color: #006cff; color: #fff; } .rta__list { border-radius: 4px 4px 0 0; } .rta__list-header { padding: 15px; font-size: 14px; } .str-chat__emoji-item { padding: 0 20px; display: flex; align-items: center; margin: 0 -8px; } .str-chat__emoji-item span { display: block; } .str-chat__emoji-item--entity { min-width: 24px; } .str-chat__emoji-item--name { font-size: 12px; } .str-chat__slash-command { padding: 10px 15px; font-size: 14px; } .str-chat__slash-command-description { font-size: 12px; } .str-chat__user-item { padding: 10px 15px; display: flex; } .str-chat .rfu-dropzone .rfu-dropzone__notifier { position: absolute; height: 100%; width: 100%; padding: 5px; z-index: 1001; display: none; } .str-chat .rfu-dropzone--accept .rfu-dropzone__notifier { background: rgba(0, 108, 255, 0.7); display: block; } .str-chat .rfu-dropzone--reject .rfu-dropzone__notifier { background: rgba(255, 0, 0, 0.7); display: block; } .rfu-dropzone__inner { width: 100%; height: 100%; padding: 0 30px; border: 1px dashed transparent; box-sizing: border-box; display: flex; text-align: center; align-items: center; justify-content: center; flex-direction: column; color: #fff; font-weight: 800; font-size: 12px; } .rfu-dropzone__inner svg { display: none; } .rfu-dropzone--reject .rfu-dropzone__inner { display: none; } .team.str-chat .str-chat__input, .livestream.str-chat .str-chat__input { padding: 10px 40px; } .team.str-chat .str-chat__input-emojiselect, .team.str-chat .str-chat__input-fileupload, .livestream.str-chat .str-chat__input-emojiselect, .livestream.str-chat .str-chat__input-fileupload { top: calc(100% - 44px); } .team.str-chat .str-chat__input-emojiselect, .livestream.str-chat .str-chat__input-emojiselect { right: 50px; } .team.str-chat .str-chat__input-fileupload, .livestream.str-chat .str-chat__input-fileupload { right: 76px; } .team.str-chat .str-chat__input-footer, .livestream.str-chat .str-chat__input-footer { padding: 0 40px 10px; } .livestream.str-chat .str-chat__input { position: relative; z-index: 1; background: rgba(255, 255, 255, 0.29); box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.1); padding: 10px 20px 5px; } .livestream.str-chat .str-chat__input textarea { border-radius: 2px; background: rgba(255, 255, 255, 0.26); border: 1px solid rgba(0, 0, 0, 0.2); font-size: 13px; padding: 12px; } .livestream.str-chat .str-chat__input-emojiselect, .livestream.str-chat .str-chat__input-fileupload { top: calc(100% - 40px); } .livestream.str-chat .str-chat__input-emojiselect { right: 30px; } .livestream.str-chat .str-chat__input-fileupload { right: 56px; } .livestream.str-chat .str-chat__input-footer { padding: 0 20px 10px; background: rgba(255, 255, 255, 0.29); } .str-chat__file-uploads { max-height: 300px; overflow-y: auto; } .rfu-file-upload-button svg { fill: rgba(0, 0, 0, 0.6); } .dark.str-chat .str-chat__input { background: rgba(255, 255, 255, 0.03); box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.34); } .dark.str-chat .str-chat__input-footer { background: rgba(255, 255, 255, 0.03); } .dark.str-chat .str-chat__input textarea { background: rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.4); border-radius: 2px; color: #fff; } .dark.str-chat .str-chat__input .str-chat__input-emojiselect svg { fill: #fff; } .dark.str-chat .str-chat__input .rfu-file-upload-button svg { fill: #fff; } .str-chat__input-flat { background: #fff; padding: 10px 40px; position: relative; } .str-chat__input-flat textarea { min-height: 56px; background: rgba(0, 0, 0, 0.05); border-radius: 8px; padding: 20px 69px; font-size: 15px; line-height: 17px; border: none; margin: 0; } .str-chat__input-flat textarea:focus { border: none; border-radius: 8px; box-shadow: 0 0 0 3px #006cff; } .str-chat__input-flat-footer { padding: 10px 40px; background: #fff; } .str-chat__input-flat-emojiselect { position: absolute; top: calc(100% - 45px); left: 25px; } .str-chat__input-flat-emojiselect svg { fill: #000; opacity: 0.5; } .str-chat__input-flat-emojiselect svg:hover { opacity: 1; } .str-chat__input-flat--emojipicker { position: absolute; bottom: calc(100%); } .str-chat__input-flat .rfu-file-upload-button { position: absolute; top: calc(100% - 40px); right: 25px; } .str-chat__input-flat .rfu-file-upload-button svg { fill: #000; opacity: 0.5; } .str-chat__input-flat .rfu-file-upload-button svg:hover { opacity: 1; } .rfu-image-previewer__image { width: 60px !important; height: 60px !important; } .rfu-image-previewer__image .rfu-thumbnail__wrapper { width: 60px !important; height: 60px !important; border-radius: 10px; } .rfu-image-previewer__image .rfu-thumbnail__wrapper .rfu-thumbnail__overlay, .rfu-image-previewer__image .rfu-thumbnail__wrapper .rfu-icon-button { padding: 0; } .rfu-image-previewer__image .rfu-thumbnail__wrapper .rfu-thumbnail__overlay svg, .rfu-image-previewer__image .rfu-thumbnail__wrapper .rfu-icon-button svg { opacity: 0.9; height: 25px; width: 25px; } .rfu-image-previewer .rfu-thumbnail-placeholder { width: 60px; height: 60px; border-radius: 10px; } .commerce.str-chat .str-chat__input-flat { padding: 10px 22px; background: transparent; } .dark.str-chat .str-chat__input-flat { background: rgba(255, 255, 255, 0.04); } .dark.str-chat .str-chat__input-flat textarea { background: rgba(255, 255, 255, 0.05); border-radius: 10px; color: #fff; } .dark.str-chat .str-chat__input-flat-emojiselect svg, .dark.str-chat .str-chat__input-flat .rfu-file-upload-button svg { fill: #fff; } .dark.str-chat.commerce .str-chat__input-flat { background: none; } .str-chat.messaging .str-chat__input-flat { padding: 10px 10px; } .str-chat__message-notification { display: block; position: absolute; align-self: center; background: #006cff; border: none; color: #fff; border-radius: 50px; padding: 4px 10px; font-size: 12px; bottom: -10px; z-index: 101; } .str-chat__list { flex: 1; overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 75px 0 0 0; } .str-chat__list-wrapper { flex: 1; position: relative; display: flex; flex-direction: column; } .str-chat__list--thread { padding: 15px 0 0 0; } .str-chat__ul { display: block; list-style-type: none; padding: 0; margin: 0; } .str-chat__connection-issue { background: rgba(208, 2, 27, 0.1); border: 1px solid rgba(208, 2, 27, 0.1); color: red; border-radius: 4px; font-size: 12px; padding: 8px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: center; } .str-chat__list-notifications { padding: 0 40px; display: flex; position: relative; flex-direction: column; justify-content: center; } .messaging.str-chat .str-chat__list { padding: 55px 40px 0; background: #fff; } .messaging.str-chat .str-chat__list-notifications { background: #fff; } .messaging.str-chat .str-chat__list { padding: 55px 10px 0; } .messaging.str-chat .str-chat__list-notifications { padding: 0 10px; } .messaging.str-chat.dark .str-chat__list { background: rgba(255, 255, 255, 0.04); } .messaging.str-chat.dark .str-chat__list-notifications { background: rgba(255, 255, 255, 0.04); } .livestream.str-chat .str-chat__list { padding: 55px 10px; } .commerce.str-chat .str-chat__list { padding: 75px 20px 0; } .commerce.str-chat .str-chat__list-notifications { padding-left: 22px; padding-right: 22px; } .str-chat__message-team { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; position: relative; display: flex; padding: 5px 40px; } .str-chat__message-team .str-chat__avatar { margin-right: 0; } .str-chat__message-team-actions { position: absolute; top: -12px; right: 0; display: flex; align-items: center; justify-content: space-between; width: 88px; height: 24px; border-radius: 100px; background: #fff; border: 1px solid #e0e0e0; box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.07); z-index: 10; visibility: hidden; } .str-chat__message-team-actions > span { position: relative; flex: 1; display: flex; justify-content: center; align-items: center; height: 100%; text-align: center; cursor: pointer; } .str-chat__message-team-actions > span > span { position: relative; flex: 1; display: flex; justify-content: center; align-items: center; height: 100%; text-align: center; } > .str-chat__message-team-actions > span:not(:last-of-type) { border-right: 1px solid #e0e0e0; } .str-chat__message-team-actions > span svg { fill: #000; opacity: 0.5; } .str-chat__message-team-actions > span:hover svg { opacity: 1; } .str-chat__message-team-actions .str-chat__message-actions-box { bottom: initial; left: initial; visibility: hidden; right: 100%; top: -10px; border-radius: 7px; } .str-chat__message-team-actions .str-chat__message-actions-box--open { visibility: visible; } .str-chat__message-team-group { position: relative; width: 100%; } .str-chat__message-team-meta { display: flex; flex-direction: column; align-items: flex-end; min-width: 50px; padding: 0 10px 0 0; justify-content: space-between; } .str-chat__message-team-meta time { text-transform: uppercase; color: rgba(0, 0, 0, 0.5); font-size: 10px; margin-bottom: 5px; text-align: right; visibility: hidden; } .str-chat__message-team-author { font-size: 15px; text-transform: capitalize; line-height: 40px; margin-left: 10px; display: flex; align-items: center; justify-content: space-between; } .str-chat__message-team-content { width: 100%; padding-left: 10px; color: #404040; font-size: 15px; line-height: 22px; font-weight: 400; border-left: 1px solid rgba(0, 0, 0, 0.1); position: relative; margin: 0 0; } .str-chat__message-team-content--image { padding-left: 0; border-left-color: transparent; } .str-chat__message-team-content p { margin: 0; white-space: pre-line; overflow-wrap: break-word; word-wrap: break-word; -ms-word-break: break-all; word-break: break-word; -ms-hyphens: auto; -moz-hyphens: auto; -webkit-hyphens: auto; hyphens: auto; } .str-chat__message-team-content p:not(:first-of-type) { margin: 16px 0 0; } .str-chat__message-team-content p a { color: #006cff; font-weight: bold; text-decoration: none; } .str-chat__message-team-content p code { background-color: #f8f8f8; border: 1px solid rgba(208, 2, 27, 0.1); border-radius: 3px; padding: 2px; } .str-chat__message-team-content pre, .str-chat__message-team-content code { font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; line-height: inherit; font-size: 12px; font-weight: 700; } .str-chat__message-team-content pre { margin: 0 5px 0 0; border-radius: 4px; background-color: #f8f8f8; border: 1px solid #d3d3d3; padding: 10px; } .str-chat__message-team-content code { width: inherit; white-space: pre-wrap; word-break: break-all; } .str-chat__message-team-content ul { margin: 0; } .str-chat__message-team-content--top:not(.str-chat__message-team-content--image)::before, .str-chat__message-team-content--single:not(.str-chat__message-team-content--image)::before { content: ''; position: absolute; top: 0; left: 0; transform: rotate(-135deg) translateX(1px); transform-origin: 0; width: 5px; height: 1px; background-color: rgba(0, 0, 0, 0.1); } .str-chat__message-team-content--top { margin: 5px 0 0; } .str-chat__message-team-content--single { margin: 5px 0 0; } .str-chat__message-team-content--middle { margin: 0 0 0; } .str-chat__message-team-content--bottom { margin: 0 0 0; } .str-chat__message-team-text--is-emoji { font-size: 33px; line-height: 42px; } .str-chat__message-team-status { position: absolute; left: 100%; bottom: 1px; line-height: 1; display: flex; justify-content: flex-end; align-items: center; z-index: 11; } .str-chat__message-team-status-number { font-size: 10px; margin-left: 4px; position: absolute; left: 100%; color: rgba(0, 0, 0, 0.6); } .str-chat__message-team-status > .str-chat__avatar { align-self: flex-end; margin-right: 0; } .str-chat__message-team-status > .str-chat__tooltip { display: none; max-width: 300px; min-width: 100px; text-align: center; } .str-chat__message-team-status:hover > .str-chat__tooltip { display: block; } .str-chat__message-team-status::after { position: absolute; bottom: 100%; right: 0; content: ' '; width: 15px; height: 15px; } .str-chat__message-team-failed { border: 0; background: none; display: flex; align-items: center; color: #ea152f; cursor: pointer; margin: 5px 0; font-size: 12px; padding: 0; } .str-chat__message-team-failed svg { margin-right: 7px; } .str-chat__message-team-form-footer { display: flex; justify-content: space-between; padding: 10px 0 5px; } .str-chat__message-team--bottom .str-chat__message-team-meta time, .str-chat__message-team--single .str-chat__message-team-meta time { visibility: visible; } .str-chat__message-team--editing { padding: 10px; background: #edf4ff; box-shadow: 0 0 11px 0 rgba(0, 0, 0, 0.06), inset 0 1px 0 0 #006cff, inset 0 -1px 0 0 #006cff; z-index: 1; } .str-chat__message-team:hover:not(.str-chat__message-team--editing, .str-chat__message-team--error) .str-chat__message-team-content { background: rgba(0, 0, 0, 0.03); } .str-chat__message-team:hover .str-chat__message-team-meta time { visibility: visible; } .str-chat__message-team:hover .str-chat__message-team-actions { visibility: visible; } .str-chat__message-team--error { padding-top: 20px; padding-bottom: 20px; } .str-chat__message-team--error .str-chat__message-team-status { display: none; } .str-chat__message-team--error .str-chat__message-team-content { background: transparent; border-width: 2px; border-color: #d0021b; } .str-chat__message-team--error .str-chat__message-team-content p { opacity: 0.5; } .str-chat__message-team--error .str-chat__message-team-content::before { content: ''; position: absolute; top: 0; left: 0; transform: rotate(-135deg) translateX(1px); transform-origin: 0; width: 5px; height: 2px; background-color: #d0021b; } .str-chat__message-team--ephemeral .str-chat__message-team-status { display: none; } .str-chat__message-team--failed .str-chat__message-team-content--text { border-color: #d0021b; } .str-chat__message-team--failed .str-chat__message-team-content--text p { opacity: 0.5; } .str-chat__message-team .str-chat__message-attachment--img { border-radius: 0; padding-left: 5px; border-left: 1px solid rgba(0, 0, 0, 0.1); } .str-chat__message-team .str-chat__message-attachment-card { margin: 0; border-radius: 4px 4px 4px 4px; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__message-team .str-chat__message-attachment-card--content { background: #ebebeb; } .str-chat__message-team .str-chat__message-attachment-card--text { display: none; } .str-chat__message-team .str-chat__input-emojiselect, .str-chat__message-team .str-chat__input-fileupload { position: static; top: initial; } .str-chat__message-team-error-header { font-size: 11px; color: rgba(0, 0, 0, 0.4); font-style: italic; text-transform: none; } .str-chat__thread-list .str-chat__message-simple__actions { min-width: 30px; } .str-chat__thread-list .str-chat__message-team { padding: 5px 10px; } .str-chat__thread-list .str-chat__message-team-actions { width: 60px; min-width: unset; } .str-chat__thread-list .str-chat__message-team-actions .str-chat__message-actions-box { top: initial; bottom: initial; bottom: -10px; } .str-chat blockquote { margin: 0 0 0 5px; font-style: italic; padding-left: 20px; position: relative; } .str-chat blockquote::before { font-size: 25px; content: '“'; font-style: italic; position: absolute; opacity: 0.5; top: 2px; left: -5px; } .str-chat.dark .str-chat__message-team-error-header { color: rgba(255, 255, 255, 0.5); opacity: 1; } .str-chat.dark .str-chat__message-team-author { color: #fff; } .str-chat.dark .str-chat__message-team-meta time { color: rgba(255, 255, 255, 0.3); } .str-chat.dark .str-chat__message-team-content, .str-chat.dark .str-chat__message-team .str-chat__message-attachment { border-color: rgba(0, 0, 0, 0.4); color: #fff; } .str-chat.dark .str-chat__message-team-content--top:not(.str-chat__message-team-content--image)::before, .str-chat.dark .str-chat__message-team-content--single:not(.str-chat__message-team-content--image)::before, .str-chat.dark .str-chat__message-team .str-chat__message-attachment--top:not(.str-chat__message-team-content--image)::before, .str-chat.dark .str-chat__message-team .str-chat__message-attachment--single:not(.str-chat__message-team-content--image)::before { background-color: rgba(0, 0, 0, 0.4); } .str-chat.dark .str-chat__message-team-content--image, .str-chat.dark .str-chat__message-team .str-chat__message-attachment--image { border-color: transparent; } .str-chat.dark .str-chat__message-team-content p code, .str-chat.dark .str-chat__message-team .str-chat__message-attachment p code { background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); } .str-chat.dark .str-chat__message-team-content pre, .str-chat.dark .str-chat__message-team .str-chat__message-attachment pre { background-color: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-file--item, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-file--item { border-color: rgba(0, 0, 0, 0.4); } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-file--item a, .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-file--item span, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-file--item a, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-file--item span { color: #fff; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-file--item span, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-file--item span { opacity: 0.4; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-file--item:hover, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-file--item:hover { background: transparent; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment--file a, .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment--file span, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment--file a, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment--file span { color: #fff; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment--file span, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment--file span { opacity: 0.4; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-card, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-card { background: transparent; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-card--content, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-card--content { background: rgba(0, 0, 0, 0.1); min-height: 58px; margin: 0; padding: 0px 16px; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-card--title, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-card--title { color: #fff; } .str-chat.dark .str-chat__message-team-content .str-chat__message-attachment-card--url, .str-chat.dark .str-chat__message-team .str-chat__message-attachment .str-chat__message-attachment-card--url { color: rgba(255, 255, 255, 0.4); } .str-chat.dark .str-chat__message-team-actions { background: #1b1d20; border: 1px solid rgba(224, 224, 224, 0.03); box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.07); border-radius: 12px; } .str-chat.dark .str-chat__message-team-actions .str-chat__message-actions-box { background: #6a6b6d; } .str-chat.dark .str-chat__message-team-actions > span { border-color: rgba(0, 0, 0, 0.04); } .str-chat.dark .str-chat__message-team-actions > span svg { fill: #fff; } .str-chat.dark .str-chat__message-team--error .str-chat__message-team-content { border-color: red; border-width: 1px; } .str-chat.dark .str-chat__message-team--error .str-chat__message-team-content p { color: rgba(255, 255, 255, 0.5); opacity: 1; } .str-chat.dark .str-chat__message-team--error .str-chat__message-team-content--top:not(.str-chat__message-team-content--image)::before, .str-chat.dark .str-chat__message-team--error .str-chat__message-team-content--single:not(.str-chat__message-team-content--image)::before { background-color: red; height: 1px; } .str-chat.dark .str-chat__message-team--editing { padding: 10px; background: rgba(0, 0, 0, 0.1); box-shadow: 0 0 11px 0 rgba(0, 0, 0, 0.06), inset 0 1px 0 0 rgba(0, 108, 255, 0.1), inset 0 -1px 0 0 rgba(0, 108, 255, 0.1); z-index: 1; } .str-chat__message-livestream { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; display: flex; width: 100%; margin: 0 0; padding: 10px 10px; border: 1px solid rgba(0, 0, 0, 0); position: relative; } .str-chat__message-livestream-left { width: 30px; } .str-chat__message-livestream-left .str-chat__avatar { margin-right: 0; } .str-chat__message-livestream-right { flex: 1; } .str-chat__message-livestream-content { position: relative; padding: 5px 10px; border: 1px solid transparent; } .str-chat__message-livestream-content > * { font-size: 13px; line-height: 20px; margin: 0; } .str-chat__message-livestream-content .str-chat__message-mention { font-size: 13px; line-height: 20px; margin: 0; } .str-chat__message-livestream-content .str-chat__message-mention:focus { outline: 1; } .str-chat__message-livestream-content p { margin: 0; white-space: pre-line; overflow-wrap: break-word; word-wrap: break-word; -ms-word-break: break-all; word-break: break-word; -ms-hyphens: auto; -moz-hyphens: auto; -webkit-hyphens: auto; hyphens: auto; } .str-chat__message-livestream-content p:not(:first-of-type) { margin: 16px 0 0; } .str-chat__message-livestream-content p code { background-color: rgba(255, 255, 255, 0.2); border: 1px solid rgba(0, 108, 255, 0.1); border-radius: 3px; padding: 2px 4px; } .str-chat__message-livestream-content p a { color: #006cff; font-weight: bold; text-decoration: none; } .str-chat__message-livestream-content pre, .str-chat__message-livestream-content code { font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; line-height: inherit; padding: 10px; font-size: 12px; font-weight: 700; } .str-chat__message-livestream-content pre { margin: 0 5px 0 0; border-radius: 4px; background-color: rgba(0, 108, 255, 0.1); border: 1px solid rgba(0, 108, 255, 0.2); } .str-chat__message-livestream-content code { width: inherit; word-break: break-all; } .str-chat__message-livestream:hover .str-chat__message-livestream-actions { display: flex; } .str-chat__message-livestream-actions { background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(0, 0, 0, 0.23); box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.07); width: 141px; height: 24px; padding: 0 4px; position: absolute; top: -12px; right: 0; border-radius: 24px; display: none; align-items: center; justify-content: space-between; } .str-chat__message-livestream-actions > span { position: relative; flex: 1; display: flex; justify-content: center; align-items: center; height: 100%; text-align: center; cursor: pointer; } .str-chat__message-livestream-actions > span:not(:last-of-type) { border-right: 1px solid #e0e0e0; } .str-chat__message-livestream-actions > span > span { position: relative; flex: 1; display: flex; justify-content: center; align-items: center; height: 100%; text-align: center; } .str-chat__message-livestream-actions > span svg { fill: #000; opacity: 0.5; } .str-chat__message-livestream-actions > span:hover svg { opacity: 1; } .str-chat__message-livestream-actions .str-chat__message-actions-box { bottom: initial; left: initial; visibility: hidden; right: 100%; top: 50%; transform: translateY(-50%); border-radius: 7px; } .str-chat__message-livestream-actions .str-chat__message-actions-box--open { visibility: visible; } .str-chat__message-livestream-time { font-size: 10px; line-height: 20px; color: rgba(0, 0, 0, 0.5); flex: 2; padding: 0 4px; } .str-chat__message-livestream-text--is-emoji { font-size: 33px; line-height: 42px; } .str-chat__message-livestream-author { margin-bottom: 8px; text-transform: capitalize; display: flex; align-items: center; justify-content: flex-start; } .str-chat__message-livestream-author strong { margin-right: 8px; } .str-chat__message-livestream:hover { background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(0, 0, 0, 0.06); box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.06); border-radius: 6px; } .str-chat__message-livestream .str-chat__message-attachment--img { border-radius: 0; } .str-chat__message-livestream .str-chat__message-attachment-card { margin: 0; border-radius: 0; background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__message-livestream .str-chat__message-attachment-card--content { background: rgba(255, 255, 255, 0.2); } .str-chat__message-livestream .str-chat__message-attachment-card--text { display: none; } .str-chat__message-livestream .str-chat__message-attachment-card--url { text-transform: lowercase; } .str-chat__message-livestream__thread-banner { text-align: center; font-size: 12px; padding: 8px; margin: 8px 10px 0 10px; background: rgba(0, 108, 255, 0.1); border-radius: 2px; color: #404040; } .str-chat__message-livestream--error .str-chat__message-livestream-content p, .str-chat__message-livestream--failed .str-chat__message-livestream-content p { color: red; } .str-chat__message-livestream--error .str-chat__message-livestream-content p svg, .str-chat__message-livestream--failed .str-chat__message-livestream-content p svg { position: relative; top: 2px; margin-right: 4px; } .str-chat__message-livestream--failed .str-chat__message-livestream-content p { cursor: pointer; } .str-chat__message-livestream--initial-message { margin: 20px 10px 0; width: auto; } .str-chat__list--thread .str-chat__message-livestream__actions { min-width: 30px; } .str-chat__list--thread .str-chat__message-livestream-actions { width: 110px; min-width: unset; } .str-chat__list--thread .str-chat__message-livestream-actions .str-chat__message-actions-box { top: initial; bottom: initial; bottom: -10px; } .livestream.dark.str-chat .str-chat__message-livestream { color: #e6e6e6; } .livestream.dark.str-chat .str-chat__message-livestream:hover { background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(0, 0, 0, 0.06); box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.06); border-radius: 6px; } .livestream.dark.str-chat .str-chat__message-livestream .str-chat__message-attachment-card { background: rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.08); } .livestream.dark.str-chat .str-chat__message-livestream .str-chat__message-attachment-card--content { background: rgba(0, 0, 0, 0.1); } .livestream.dark.str-chat .str-chat__message-livestream .str-chat__message-attachment-card--url { color: rgba(255, 255, 255, 0.79); } .livestream.dark.str-chat .str-chat__message-livestream-actions { background: #1b1d20; border: 1px solid rgba(224, 224, 224, 0.03); box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.07); } .livestream.dark.str-chat .str-chat__message-livestream-actions > span:not(:last-of-type) { border-color: rgba(255, 255, 255, 0.04); } .livestream.dark.str-chat .str-chat__message-livestream-actions svg { fill: #fff; } .livestream.dark.str-chat .str-chat__message-livestream-time { color: #fff; opacity: 0.5; } .str-chat__message-replies-count-button { display: block; border: none; background: none; padding: 0; margin-top: 8px; font-size: 12px; line-height: 15px; font-weight: 700; color: #006cff; cursor: pointer; } .str-chat__message-replies-count-button svg { fill: rgba(0, 0, 0, 0.1); margin-right: 5px; } .dark.str-chat .str-chat__message-replies-count-button svg { fill: rgba(255, 255, 255, 0.1); } .dark.str-chat.team .str-chat__message-replies-count-button svg { fill: rgba(0, 0, 0, 0.4); } .str-chat__modal { background: #000; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; opacity: 0.89; display: none; align-items: center; justify-content: center; } .str-chat__modal--open { display: flex; } .str-chat__modal__inner { max-width: 667px; background: #fff; padding: 20px; border-radius: 10px; } .str-chat__modal__close-button { position: absolute; top: 0; right: 0; padding: 20px; font-size: 14px; line-height: 10px; color: #fff; cursor: pointer; display: flex; align-items: center; } .str-chat__modal__close-button svg { position: relative; top: 1px; margin-left: 10px; fill: #fff; } .str-chat__modal__close-button:hover { opacity: 0.79; } .str-chat__modal .str-chat__edit-message-form { min-width: 300px; } .str-chat__modal .str-chat__input-emojiselect, .str-chat__modal .str-chat__input-fileupload { position: relative; top: unset; right: unset; } .str-chat.dark.messaging .str-chat__modal__inner { border: 2px solid #fff; background: #000; } .str-chat__new-channel--header { padding: 10px; display: flex; align-items: center; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .str-chat__new-channel--header__title { flex: 1; text-align: center; color: #006cff; font-weight: 700; margin-right: 45px; } .str-chat__new-channel--name { padding: 0 0 0 15px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); display: flex; align-items: center; } .str-chat__new-channel--name label { font-size: 14px; margin-right: 5px; } .str-chat__new-channel--name input { font-size: 14px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; flex: 1; width: 100%; padding: 15px 0 15px 5px; background: #fff; border: none; outline: none; } .str-chat__new-channel--name input:focus { background: #fff; box-shadow: inset 0 0 0 1px #006cff, inset 0 0 0 2px rgba(0, 108, 255, 0.36); } .str-chat__message .str-chat__reaction-list::after, .str-chat__message .str-chat__reaction-list::before, .str-chat__message-commerce .str-chat__reaction-list::after, .str-chat__message-commerce .str-chat__reaction-list::before, .str-chat__message .str-chat__reaction-list, .str-chat__message-commerce .str-chat__reaction-list { background: url('../assets/str-chat__reaction-list-sprite@1x.png') no-repeat; background-size: 59px 101px; } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 2dppx) { .str-chat__message .str-chat__reaction-list::after, .str-chat__message .str-chat__reaction-list::before, .str-chat__message-commerce .str-chat__reaction-list::after, .str-chat__message-commerce .str-chat__reaction-list::before, .str-chat__message .str-chat__reaction-list, .str-chat__message-commerce .str-chat__reaction-list { background-image: url('../assets/str-chat__reaction-list-sprite@2x.png'); } } @media only screen and (-webkit-min-device-pixel-ratio: 3), only screen and (min-resolution: 3dppx) { .str-chat__message .str-chat__reaction-list::after, .str-chat__message .str-chat__reaction-list::before, .str-chat__message-commerce .str-chat__reaction-list::after, .str-chat__message-commerce .str-chat__reaction-list::before, .str-chat__message .str-chat__reaction-list, .str-chat__message-commerce .str-chat__reaction-list { background-image: url('../assets/str-chat__reaction-list-sprite@3x.png'); } } .str-chat__message .str-chat__reaction-list, .str-chat__message-commerce .str-chat__reaction-list { position: absolute; right: 15px; top: -28px; z-index: 99; height: 33px; width: initial; background-position: 0 -66px; background-repeat: repeat-x; } .str-chat__message .str-chat__reaction-list ul, .str-chat__message-commerce .str-chat__reaction-list ul { position: relative; list-style-type: none; padding: 0; font-size: 15px; line-height: 26px; display: flex; justify-content: center; margin: -1px -15px 0 0; z-index: 888; } .str-chat__message .str-chat__reaction-list .emoji-mart-emoji, .str-chat__message-commerce .str-chat__reaction-list .emoji-mart-emoji { display: flex; } .str-chat__message .str-chat__reaction-list::after, .str-chat__message .str-chat__reaction-list::before, .str-chat__message-commerce .str-chat__reaction-list::after, .str-chat__message-commerce .str-chat__reaction-list::before { position: absolute; content: ''; top: 0; height: 33px; } .str-chat__message .str-chat__reaction-list::after, .str-chat__message-commerce .str-chat__reaction-list::after { right: -26px; width: 26px; background-position: -33px -33px; } .str-chat__message .str-chat__reaction-list::before, .str-chat__message-commerce .str-chat__reaction-list::before { left: -13px; width: 13px; background-position: 0 -33px; } .str-chat__message .str-chat__reaction-list--reverse, .str-chat__message-commerce .str-chat__reaction-list--reverse { right: initial; left: 15px; position: absolute; } .str-chat__message .str-chat__reaction-list--reverse ul, .str-chat__message-commerce .str-chat__reaction-list--reverse ul { margin: -1px -5px 0 -15px; } .str-chat__message .str-chat__reaction-list--reverse::after, .str-chat__message-commerce .str-chat__reaction-list--reverse::after { right: -13px; width: 13px; background-position: -46px 0; } .str-chat__message .str-chat__reaction-list--reverse::before, .str-chat__message-commerce .str-chat__reaction-list--reverse::before { left: -26px; width: 26px; background-position: 0 0; } .str-chat__message .str-chat__reaction-list li, .str-chat__message-commerce .str-chat__reaction-list li { display: flex; align-items: center; } .str-chat__message .str-chat__reaction-list li button, .str-chat__message-commerce .str-chat__reaction-list li button { padding: 0; } .str-chat__message .str-chat__reaction-list--counter, .str-chat__message-commerce .str-chat__reaction-list--counter { color: #fff; font-size: 12px; } .str-chat__message--me .str-chat__message-commerce-inner > .str-chat__reaction-list, .str-chat__message--right .str-chat__message-commerce-inner > .str-chat__reaction-list, .str-chat__message-commerce--me .str-chat__message-commerce-inner > .str-chat__reaction-list, .str-chat__message-commerce--right .str-chat__message-commerce-inner > .str-chat__reaction-list { left: 46px; } .str-chat__message--me .str-chat__reaction-list, .str-chat__message--right .str-chat__reaction-list, .str-chat__message-commerce--me .str-chat__reaction-list, .str-chat__message-commerce--right .str-chat__reaction-list { right: initial; left: 16px; } .str-chat__message--me .str-chat__reaction-list ul, .str-chat__message--right .str-chat__reaction-list ul, .str-chat__message-commerce--me .str-chat__reaction-list ul, .str-chat__message-commerce--right .str-chat__reaction-list ul { margin: -1px 0 0 -15px; } .str-chat__message--me .str-chat__reaction-list::after, .str-chat__message--right .str-chat__reaction-list::after, .str-chat__message-commerce--me .str-chat__reaction-list::after, .str-chat__message-commerce--right .str-chat__reaction-list::after { right: -13px; width: 13px; background-position: -46px 0; } .str-chat__message--me .str-chat__reaction-list::before, .str-chat__message--right .str-chat__reaction-list::before, .str-chat__message-commerce--me .str-chat__reaction-list::before, .str-chat__message-commerce--right .str-chat__reaction-list::before { left: -26px; width: 26px; background-position: 0 0; } .str-chat__message--me .str-chat__reaction-list--reverse, .str-chat__message--right .str-chat__reaction-list--reverse, .str-chat__message-commerce--me .str-chat__reaction-list--reverse, .str-chat__message-commerce--right .str-chat__reaction-list--reverse { right: 15px; left: initial; } .str-chat__message--me .str-chat__reaction-list--reverse ul, .str-chat__message--right .str-chat__reaction-list--reverse ul, .str-chat__message-commerce--me .str-chat__reaction-list--reverse ul, .str-chat__message-commerce--right .str-chat__reaction-list--reverse ul { margin: -1px -15px 0 0; } .str-chat__message--me .str-chat__reaction-list--reverse::after, .str-chat__message--right .str-chat__reaction-list--reverse::after, .str-chat__message-commerce--me .str-chat__reaction-list--reverse::after, .str-chat__message-commerce--right .str-chat__reaction-list--reverse::after { right: -26px; width: 26px; background-position: -33px -33px; } .str-chat__message--me .str-chat__reaction-list--reverse::before, .str-chat__message--right .str-chat__reaction-list--reverse::before, .str-chat__message-commerce--me .str-chat__reaction-list--reverse::before, .str-chat__message-commerce--right .str-chat__reaction-list--reverse::before { left: -13px; width: 13px; background-position: 0 -33px; } .str-chat__message-commerce-inner > .str-chat__reaction-list { left: unset; right: 46px; } .str-chat__message-commerce--right .str-chat__message-commerce-inner > .str-chat__reaction-list { right: unset; left: 46px; } .str-chat__message-reactions-list { list-style-type: none; margin: 0; padding: 0; display: flex; } .str-chat__message-reactions-list-item { font-size: 20px; margin: 0 5px; position: relative; line-height: 0; } .str-chat__message-reactions-list-item button { padding: 0; } .str-chat__message-reactions-list-item span[role='img'] { position: relative; display: block; top: -2px; transform: scalce(1); transition: transform 100ms ease; } .str-chat__message-reactions-list-item span[role='img']:hover { transform: scale(1.4); } .str-chat__message-reactions-list-item__count { position: absolute; top: 28px; font-size: 10px; color: #fff; font-weight: 700; left: 6px; } .str-chat__message-reactions-list-item .str-chat__avarar { margin: 0; visibility: visible; } .str-chat__message-reactions-list-item .latest-user { width: 20px; height: 20px; position: absolute; top: -24px; left: 0; } .str-chat__message-reactions-list-item .latest-user-tooltip { display: none; text-align: center; position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translate(-50%, 0); background: rgba(0, 0, 0, 0.81); border-radius: 5px; background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); max-width: 237px; padding: 4px 8px; font-size: 13px; color: #fff; } .str-chat__message-reactions-list-item .latest-user-tooltip::after { content: ''; position: absolute; top: calc(100% - 4px); left: 50%; transform: translate(-50%, 0) rotate(45deg); width: 7px; height: 7px; background-color: #1a1a1a; } .str-chat__message-reactions-list-item .latest-user-not-found { border: 1.5px solid #fff; border-radius: 50%; background-color: #f0f; width: inherit; height: inherit; } .str-chat__message-reactions-list-item .latest-user img { border: 1.5px solid #fff; border-radius: 50%; object-fit: cover; width: inherit; height: inherit; } .str-chat__message-reactions-list-item .latest-user:hover .latest-user-tooltip { display: block; } .str-chat__reaction-selector { z-index: 999; height: 60px; position: absolute; width: initial; background: rgba(24, 25, 28, 0.98); background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.05) 100%); border: 1px solid rgba(224, 224, 224, 0.03); box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.07), 0 11px 8px 0 rgba(0, 0, 0, 0.15); border-radius: 30px; display: flex; align-items: center; } .str-chat__reaction-selector ul { position: relative; z-index: 1000; margin: 0 16px; } .str-chat__reaction-selector-tooltip { position: absolute; bottom: calc(100% + 15px); background: rgba(0, 0, 0, 0.81); border-radius: 5px; background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); min-width: 85px; min-height: 24px; max-width: 100%; padding: 4px 8px; font-size: 13px; color: #fff; text-align: center; } .str-chat__reaction-selector-tooltip .arrow { position: absolute; top: calc(100% - 4px); left: 50%; transform: translate(-50%, 0) rotate(45deg); width: 7px; height: 7px; background-color: #1a1a1a; } .str-chat__reaction-selector .emoji-mart-emoji:hover { transition: 0.1s; transform: scale(1.2); cursor: pointer; } .str-chat__message .str-chat__reaction-selector, .str-chat__message-team .str-chat__reaction-selector, .str-chat__message-simple .str-chat__reaction-selector, .str-chat__message-commerce .str-chat__reaction-selector, .str-chat__message-livestream .str-chat__reaction-selector { top: -65px; left: 0; } .str-chat__message-commerce--right .str-chat__reaction-selector { left: unset; right: 0; } .str-chat__message-livestream .str-chat__reaction-selector { left: unset; top: -70px; right: 0; } .str-chat__message-team .str-chat__reaction-selector { left: unset; top: -60px; right: 0; } .str-chat__message-simple .str-chat__reaction-selector { right: unset; left: 0; } .str-chat__message-simple .str-chat__reaction-selector--reverse { right: 0; left: unset; } .str-chat__message-simple--me .str-chat__reaction-selector { left: unset; right: 0; } .str-chat__message-simple--me .str-chat__reaction-selector--reverse { right: unset; left: 0; } .str-chat__read-state { display: flex; align-items: center; justify-content: flex-end; position: absolute; right: 0; top: 100%; margin: -56px 10px 0 40px; } .str-chat__simple-reactions-list { list-style-type: none; padding: 4px 4px 2px 4px; display: inline-flex; margin: 8px 0 0 0; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 4px; line-height: 1; position: relative; } .str-chat__simple-reactions-list-tooltip { position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translate(-50%, 0); background: rgba(0, 0, 0, 0.81); border-radius: 5px; background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); min-height: 24px; width: auto; max-width: 275px; padding: 4px 8px; font-size: 13px; color: #fff; text-align: center; } .str-chat__simple-reactions-list-tooltip > .arrow { position: absolute; top: calc(100%); left: 50%; transform: translate(-50%, 0); width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid rgba(0, 0, 0, 0.81); } .str-chat__simple-reactions-list-item { margin: 0 0; cursor: pointer; } .str-chat__simple-reactions-list-item > span { line-height: 1; } .str-chat__simple-reactions-list-item .emoji-mart-emoji:hover { transition: transform 0.2s ease-in-out; transform: scale(1.2); } .str-chat__simple-reactions-list-item--last-number { font-size: 11px; display: flex; align-items: center; color: #000; } .dark.str-chat .str-chat__simple-reactions-list { border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; } .dark.str-chat .str-chat__simple-reactions-list-item--last-number { color: #fff; } .str-chat__small-message-input { margin: 10px; position: relative; z-index: 1000; } .str-chat__small-message-input-emojiselect { right: 6px; } .str-chat__small-message-input-emojiselect { right: 26px; } .str-chat__small-message-input-emojiselect, .str-chat__small-message-input-fileupload { position: absolute; cursor: pointer; bottom: 10px; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; background-size: 44px 44px; fill: rgba(0, 0, 0, 0.6); } .str-chat__small-message-input-emojiselect:hover, .str-chat__small-message-input-fileupload:hover { fill: #000; } .str-chat__small-message-input-emojipicker { position: absolute; bottom: 100%; right: 0; transform: scale(0.8); transform-origin: 100% 100%; } .str-chat__small-message-input textarea { background: transparent; min-height: 36px; font-size: 13px; padding: 10px 44px 8px 8px; } .str-chat__small-message-input textarea:focus { height: 36px; } .str-chat__small-message-input .str-chat__emojisearch { bottom: 100%; } .str-chat__small-message-input .str-chat__user-item { font-size: 14px; } .str-chat__small-message-input .rfu-file-upload-button { position: absolute; cursor: pointer; right: 28px; } .str-chat__small-message-input .rfu-dropzone .rfu-dropzone__notifier { z-index: 1000; } .dark.str-chat .str-chat__small-message-input textarea { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(0, 0, 0, 0.21); border-radius: 6px; color: #fff; } .dark.str-chat .str-chat__small-message-input .rfu-file-upload-button svg, .dark.str-chat .str-chat__small-message-input .str-chat__small-message-input-emojiselect svg { fill: #fff; } .str-chat__thread { background: #f1f1f3; flex: 1 0 300px; min-width: 300px; max-width: 300px; overflow-y: hidden; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; overflow: hidden; max-height: 100%; display: flex; flex-direction: column; padding-top: 0px; } .str-chat__thread--full { max-width: 100%; } .str-chat__thread-header { position: relative; width: 100%; background: #f1f1f3; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14); height: 70px; margin-bottom: 20px; top: 0; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; } .str-chat__thread-header-details { font-size: 14px; } .str-chat__thread-header-details small { display: block; font-size: 12px; } .str-chat__thread-start { border-radius: 4px; margin: 10px; padding: 8px; background: rgba(153, 196, 255, 0.1); text-align: center; font-size: 12px; } .str-chat__thread-list { height: 100%; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; } .str-chat__thread-list .str-chat__list { overflow-x: hidden; overflow-y: auto; padding: 40px 0; } .str-chat__thread-list .str-chat__list--thread { overflow-y: auto; } .messaging.str-chat .str-chat__thread { margin-right: 10px; margin-top: 20px; border-radius: 8px 8px 0 0; overflow: hidden; background: #fff; max-height: 100%; display: flex; flex-direction: column; padding-top: 0px; } .messaging.str-chat .str-chat__thread.str-chat__thread--full { margin: 0; } .messaging.str-chat .str-chat__thread .str-chat__gallery { justify-content: flex-end; border-radius: 0; } .messaging.str-chat .str-chat__thread .str-chat__gallery-image, .messaging.str-chat .str-chat__thread .str-chat__gallery-placeholder { width: 100px; height: 100px; } .messaging.str-chat .str-chat__thread-list { padding: 0 10px; } .messaging.str-chat .str-chat__thread-list > .str-chat__list { background: transparent; overflow-y: auto; padding: 40px 0 0; } .messaging.str-chat .str-chat__thread-list > .str-chat__list .str-chat__list--thread { padding: 0; overflow-y: hidden; } .messaging.str-chat .str-chat__thread-header { position: fixed; top: 0; right: 0; height: 100vh; background: #fff; z-index: 1000; margin: 0; width: 100vw; max-width: 100%; } .messaging.str-chat.dark .str-chat__thread { background: #282a2d; } .messaging.str-chat.dark .str-chat__thread-header { background: rgba(46, 48, 51, 0.98); box-shadow: 0 7px 9px 0 rgba(0, 0, 0, 0.03), 0 1px 0 0 rgba(0, 0, 0, 0.03); color: #fff; } .messaging.str-chat.dark .str-chat__thread-start { color: #fff; } .team.str-chat .str-chat__thread { position: fixed; top: 0; right: 0; height: 100vh; background: #fff; z-index: 1000; margin: 0; width: 100vw; max-width: 100%; } .team.str-chat .str-chat__thread-header { height: 80px; } .team.str-chat .str-chat__thread-list > .str-chat__list { overflow-y: auto; } .team.str-chat .str-chat__thread-list > .str-chat__list .str-chat__list--thread { overflow-y: hidden; } .team.str-chat.dark .str-chat__thread { background: #1d1f22; } .livestream.str-chat .str-chat__thread, .team.str-chat .str-chat__thread { background: transparent; } .livestream.str-chat .str-chat__thread-header, .team.str-chat .str-chat__thread-header { background: rgba(255, 255, 255, 0.29); } .livestream.str-chat.dark .str-chat__thread, .team.str-chat.dark .str-chat__thread { background: #1a1a1a; } .livestream.str-chat.dark .str-chat__thread-header, .team.str-chat.dark .str-chat__thread-header { background: rgba(255, 255, 255, 0.03); box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.34); color: #fff; } .livestream.str-chat.dark .str-chat__thread-start, .team.str-chat.dark .str-chat__thread-start { background: rgba(153, 196, 255, 0.1); border-radius: 4px; color: #fff; } .str-chat__typing-indicator { display: flex; visibility: hidden; align-items: center; } .str-chat__typing-indicator--typing { visibility: visible; } .str-chat__typing-indicator__avatars { display: flex; } .str-chat__typing-indicator__avatars .str-chat__avatar { border: 2px solid #fff; margin-right: -14px; } .str-chat__typing-indicator__avatars .str-chat__avatar:last-of-type { margin-right: 14px; } .str-chat__typing-indicator__dots { position: relative; background: #fff; border: 1px solid rgba(0, 0, 0, 0.08); padding: 7px; border-radius: 16px 16px 16px 2px; } .str-chat__typing-indicator__dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 3px; background: #006cff; animation: wave 1.1s linear infinite; } .str-chat__typing-indicator__dot:nth-child(2) { animation-delay: -0.9s; opacity: 0.5; } .str-chat__typing-indicator__dot:nth-child(3) { animation-delay: -0.8s; opacity: 0.2; } .dark.str-chat .str-chat__typing-indicator__avatars .str-chat__avatar { border-color: #282a2d; } .dark.str-chat .str-chat__typing-indicator__dots { background: rgba(255, 255, 255, 0.05); } @keyframes wave { 0%, 60%, 100% { transform: initial; } 30% { transform: translateY(-8px); } } .str-chat__tooltip { position: absolute; right: 0; bottom: calc(100% + 10px); display: flex; background: #000; border-radius: 4px; padding: 3px 7px; color: #fff; font-size: 11px; max-width: 300px; } .str-chat__tooltip button { outline: none; outline: 0; background: none; color: #006cff; font-size: 11px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; border: none; } .str-chat__tooltip a { color: #006cff; text-decoration: none; } .str-chat__tooltip::after { content: ''; position: absolute; bottom: -2px; right: 5px; width: 5px; height: 5px; background: #000; transform: rotate(45deg); }
Place the above file in your public
directory, then in public/index.html
you can import the file like so:
<!DOCTYPE html> <html lang="en"> <head> ... <link rel="stylesheet" href="%PUBLIC_URL%/chat-styles.css" /> ... </head> <body> ... </body> </html>
We also need to jump back into our ActionsButtons.js
component we made for our custom call UI along the bottom of the screen and connect it to our redux store so we can retrieve the unread count and show the notification badge accordingly:
... import { connect } from 'react-redux'; // Selectors // import { createStructuredSelector } from 'reselect'; import { makeSelectUnreadCount } from 'data/chat/selectors'; ... const mapStateToProps = createStructuredSelector({ unreadCount: makeSelectUnreadCount(), }); export default connect(mapStateToProps)(ActionsButtons);
Final Thoughts
In this tutorial, you’ve successfully built a fully functioning video chat application. For more information on Voxeet, head over to their website at https://voxeet.com.
If you’re interested in adding chat to your application, the Stream Chat SDKs has you covered – with React Components (that we used in this tutorial), as well as SDKs and libraries for JS, iOS SDK, Android SDK, and most other popular programming languages.
Happy coding! ✌️