From 0425bd434899c0f6ea0af9755511625f126cc139 Mon Sep 17 00:00:00 2001 From: aforge Date: Wed, 3 May 2023 22:50:51 -0700 Subject: [PATCH 01/17] Video conferencing skeleton. --- css/base.css | 89 +++++++ src/lib/formatters.js | 2 + src/views/messages-view.jsx | 56 ++-- src/views/tinode-web.jsx | 46 +++- src/widgets/call-message.jsx | 17 +- src/widgets/chat-message.jsx | 8 +- src/widgets/context-menu.jsx | 3 +- src/widgets/vc-carousel-item.jsx | 94 +++++++ src/widgets/vc-carousel.jsx | 102 ++++++++ src/widgets/vc-panel.jsx | 424 +++++++++++++++++++++++++++++++ 10 files changed, 808 insertions(+), 33 deletions(-) create mode 100644 src/widgets/vc-carousel-item.jsx create mode 100644 src/widgets/vc-carousel.jsx create mode 100644 src/widgets/vc-panel.jsx diff --git a/css/base.css b/css/base.css index 9e0c0858..40992581 100644 --- a/css/base.css +++ b/css/base.css @@ -309,6 +309,11 @@ ul.menu > li.separator:hover { background-color: #ccc; } +ul.menu > li.disabled { + pointer-events: none; + opacity: 0.6; +} + /* The down arrow head - trigger for context menu */ .menuTrigger { position: absolute; @@ -3242,3 +3247,87 @@ ul.phone-country-selector span.country { width: unset; } } + +/* Video conferencing. */ +.call-party-speaking { + border: 2px solid red; +} + +.call-party.carousel-item .caller-name.inactive { + position: absolute; + bottom: 0.25rem; + right: 0.25rem; +} + +.call-party.carousel-item .muted { + position: absolute; + top: 0.25rem; + right: 0.25rem; +} + +.avatar-box.carousel-item { + /* Center the avatar in parent view. */ + width: 45px; + height: 45px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +#carousel-wrapper { + width: 100%; + max-width: 800px; + position: relative; + align-self: center; + padding-top: 1rem; +} + +#carousel { + overflow: auto; + scroll-behavior: smooth; + scrollbar-width: none; +} + +#carousel-prev, #carousel-next { + display: flex; + justify-content: center; + align-content: center; + background: white; + border: none; + padding: 8px; + border-radius: 50%; + outline: 0; + cursor: pointer; + position: absolute; + min-width: unset; + height: unset; +} + +#carousel-prev { + top: 50%; + left: 0; + transform: translate(50%, -50%); + display: none; +} + +#carousel-next { + top: 50%; + right: 0; + transform: translate(-50%, -50%); +} + +#carousel-content { + display: grid; + justify-content: center; + grid-gap: 16px; + grid-auto-flow: column; + margin: auto; + box-sizing: border-box; +} + +.carousel-item { + width: 180px; + height: 100px; + position: relative; +} diff --git a/src/lib/formatters.js b/src/lib/formatters.js index 1b13322b..1dfcbdd4 100644 --- a/src/lib/formatters.js +++ b/src/lib/formatters.js @@ -136,6 +136,8 @@ export function fullFormatter(style, data, values, key, stack) { attr.callState = data.state; attr.incoming = data.incoming; attr.duration = data.duration; + attr.vc = data.vc; + attr.onCallJoin = this.onCallJoin; } break; case 'VD': diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index 29790984..474296c8 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -19,6 +19,7 @@ import LogoView from './logo-view.jsx'; import MetaMessage from '../widgets/meta-message.jsx'; import PinnedMessages from '../widgets/pinned-messages.jsx'; import SendMessage from '../widgets/send-message.jsx'; +import VCPanel from '../widgets/vc-panel.jsx'; import VideoPreview from '../widgets/video-preview.jsx'; import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION, @@ -1440,23 +1441,44 @@ class MessagesView extends React.Component { onSendMessage={this.sendFileAttachment} /> ); } else if (this.state.rtcPanel) { - component2 = ( - - ); + onError={this.props.onError} + onHangup={this.handleCallHangup} + onInvite={this.props.onCallInvite} + onSendOffer={this.props.onCallSendOffer} + onIceCandidate={this.props.onCallIceCandidate} + onSendAnswer={this.props.onCallSendAnswer} /> + ); + } else { + // Video conference or stream. + component2 = ( + + ); + } } else { const topic = this.props.tinode.getTopic(this.state.topic); const isChannel = topic.isChannelType(); @@ -1537,6 +1559,7 @@ class MessagesView extends React.Component { messageNodes.push( diff --git a/src/views/tinode-web.jsx b/src/views/tinode-web.jsx index 86701c84..b908b49d 100644 --- a/src/views/tinode-web.jsx +++ b/src/views/tinode-web.jsx @@ -176,6 +176,7 @@ class TinodeWeb extends React.Component { this.handleCallSendOffer = this.handleCallSendOffer.bind(this); this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this); this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this); + this.handleVCCallJoin = this.handleVCCallJoin.bind(this); this.handleCallAccept = this.handleCallAccept.bind(this); @@ -1591,7 +1592,7 @@ class TinodeWeb extends React.Component { } let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, - archived = false, webrtc = false; + archived = false, webrtc = false, writer = false; if (topic) { subscribed = topic.isSubscribed(); archived = topic.isArchived(); @@ -1602,10 +1603,12 @@ class TinodeWeb extends React.Component { blocked = !acs.isJoiner(); self_blocked = !acs.isJoiner('want'); deleter = acs.isDeleter(); + writer = acs.isWriter(); } } - webrtc = !!this.tinode.getServerParam('iceServers'); + webrtc = writer && !!this.tinode.getServerParam('iceServers'); + const mediaServerAvailable = !!this.tinode.getServerParam('vcEndpoint'); return [ subscribed ? { @@ -1616,8 +1619,9 @@ class TinodeWeb extends React.Component { title: this.props.intl.formatMessage(messages.menu_item_audio_call), handler: this.handleStartAudioCall } : null, - subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? { + subscribed && webrtc ? { title: this.props.intl.formatMessage(messages.menu_item_video_call), + disabled: Tinode.isGroupTopicName(topicName) && !mediaServerAvailable, handler: this.handleStartVideoCall } : null, subscribed ? 'messages_clear' : null, @@ -1772,13 +1776,19 @@ class TinodeWeb extends React.Component { switch (callState) { case CALL_STATE_OUTGOING_INITATED: const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly }; - this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head) + let msg = Drafty.videoCall(audioOnly); + if (Tinode.isGroupTopicName(callTopic)) { + // Video conference. + head.vc = true; + } + return this.handleSendMessage(msg, undefined, undefined, head) .then(ctrl => { if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) { this.handleCallClose(); - return; + return ctrl; } this.setState({callSeq: ctrl.params['seq']}); + return ctrl }); break; case CALL_STATE_IN_PROGRESS: @@ -1836,6 +1846,15 @@ class TinodeWeb extends React.Component { topic.videoCall('answer', callSeq, sdp); } + handleVCCallJoin(callTopic, callSeq) { + const topic = this.tinode.getTopic(callTopic); + if (!topic) { + return; + } + + topic.videoCall('vc-join', callSeq); + } + handleCallClose() { if (this.callTimeoutTimer) { clearTimeout(this.callTimeoutTimer); @@ -1847,16 +1866,21 @@ class TinodeWeb extends React.Component { }); } - handleCallAccept(topicName) { + handleCallAccept(topicName, setCallTopic, callSeq) { const topic = this.tinode.getTopic(topicName); if (!topic) { return; } if (topic.isSubscribed()) { - this.handleTopicSelected(this.state.callTopic); - this.setState({ + this.handleTopicSelected(setCallTopic ? topicName : this.state.callTopic); + let upd = { callState: CALL_STATE_IN_PROGRESS - }); + } + if (setCallTopic) { + upd.callTopic = topicName; + upd.callSeq = callSeq; + } + this.setState(upd); } else { // We need to switch and subscribe to callTopic first. this.setState({ @@ -1894,7 +1918,7 @@ class TinodeWeb extends React.Component { } handleDataMessage(data) { - if (data.head && data.head.webrtc && data.head.webrtc == CALL_HEAD_STARTED) { + if (data.head && data.head.webrtc && data.head.webrtc == CALL_HEAD_STARTED && Tinode.isP2PTopicName(data.topic)) { // If it's a video call invite message. // See if we need to display incoming call UI. const topic = this.tinode.getTopic(data.topic); @@ -2089,11 +2113,13 @@ class TinodeWeb extends React.Component { callState={this.state.callState} callAudioOnly={this.state.callAudioOnly} onCallHangup={this.handleCallHangup} + onAcceptCall={this.handleCallAccept} onCallInvite={this.handleCallInvite} onCallSendOffer={this.handleCallSendOffer} onCallIceCandidate={this.handleCallIceCandidate} onCallSendAnswer={this.handleCallSendAnswer} + onVCJoin={this.handleVCCallJoin} errorText={this.state.errorText} errorLevel={this.state.errorLevel} diff --git a/src/widgets/call-message.jsx b/src/widgets/call-message.jsx index 650161ec..68dc5610 100644 --- a/src/widgets/call-message.jsx +++ b/src/widgets/call-message.jsx @@ -7,11 +7,17 @@ import { secondsToTime } from '../lib/strformat.js'; export default class CallMessage extends React.PureComponent { render() { + const bigIcon = this.props.vc ? 'video_call' : 'call'; const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState); const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green'); const callIcon = this.props.incoming ? (isCallDropped ? 'call_missed' : 'call_received') : (isCallDropped ? 'call_missed_outgoing' : 'call_made'); + const title = this.props.vc ? + : + this.props.incoming ? + : + ; let duration; if (isCallDropped) { switch (this.props.callState) { @@ -33,14 +39,15 @@ export default class CallMessage extends React.PureComponent { } else { duration = {secondsToTime(this.props.duration / 1000)}; } + const allowJoin = this.props.vc && this.props.callState == 'started'; return
-
call
+
{bigIcon}
-
{this.props.incoming ? - : - - }
+
{title}
{callIcon} {duration}
+ {allowJoin ? + Join + : null}
; } diff --git a/src/widgets/chat-message.jsx b/src/widgets/chat-message.jsx index 39cba7d3..c44c0c0d 100644 --- a/src/widgets/chat-message.jsx +++ b/src/widgets/chat-message.jsx @@ -29,6 +29,7 @@ class BaseChatMessage extends React.PureComponent { this.handleContextClick = this.handleContextClick.bind(this); this.handleCancelUpload = this.handleCancelUpload.bind(this); this.handleQuoteClick = this.handleQuoteClick.bind(this); + this.handleCallJoin = this.handleCallJoin.bind(this); this.formatterContext = { formatMessage: props.intl.formatMessage.bind(props.intl), @@ -38,7 +39,8 @@ class BaseChatMessage extends React.PureComponent { onImagePreview: this.handleExpandImage, onVideoPreview: this.handlePlayVideo, onFormButtonClick: this.handleFormButtonClick, - onQuoteClick: this.handleQuoteClick + onQuoteClick: this.handleQuoteClick, + onCallJoin: this.handleCallJoin }; } @@ -149,6 +151,10 @@ class BaseChatMessage extends React.PureComponent { } } + handleCallJoin() { + this.props.onAcceptCall(this.props.topic, true, this.props.seq); + } + render() { const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'); const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble'; diff --git a/src/widgets/context-menu.jsx b/src/widgets/context-menu.jsx index f6b07e8f..d519feac 100644 --- a/src/widgets/context-menu.jsx +++ b/src/widgets/context-menu.jsx @@ -493,11 +493,12 @@ class ContextMenu extends React.Component { item = this.MenuItems[item]; } if (item && item.title) { + const className = item.disabled ? 'disabled' : undefined; menu.push( item.title == '-' ?
  • : -
  • {item.title}
  • +
  • {item.title}
  • ); } count++; diff --git a/src/widgets/vc-carousel-item.jsx b/src/widgets/vc-carousel-item.jsx new file mode 100644 index 00000000..2ebabf4e --- /dev/null +++ b/src/widgets/vc-carousel-item.jsx @@ -0,0 +1,94 @@ +// Video conference carousel item. +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { + Track, +} from 'livekit-client'; + +import LetterTile from './letter-tile.jsx'; + +class VCCarouselItem extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + videoRef: undefined, + audioRef: undefined + }; + + this.handleVideoRefChange = this.handleVideoRefChange.bind(this); + this.handleAudioRefChange = this.handleAudioRefChange.bind(this); + } + + componentDidUpdate(prevProps) { + if (this.state.videoRef && !this.state.videoRef.srcObject && this.props.cameraPub && this.props.cameraPub.videoTrack) { + this.props.cameraPub.videoTrack.attach(this.state.videoRef); + } + if (this.state.audioRef && !this.state.audioRef.srcObject && this.props.micPub && this.props.micPub.audioTrack) { + this.props.micPub.audioTrack.attach(this.state.audioRef); + } + } + + handleVideoRefChange(node) { + if (!node) { return; } + this.setState({videoRef: node}); + + // Attach video track to video element. + const cameraPub = this.props.cameraPub; + const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted; + if (cameraEnabled && cameraPub.videoTrack) { + cameraPub.videoTrack.attach(node); + } + } + + handleAudioRefChange(node) { + if (!node) { return; } + this.setState({audioRef: node}); + + // Attach audio track to audio element. + const micPub = this.props.micPub; + const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted; + if (micEnabled && this.state.audioTrack) { + this.state.audioTrack.attach(node); + } + } + + render() { + const name = this.props.name; + let classes = "call-party carousel-item"; + if (this.props.isSpeaking) { + classes += " call-party-speaking"; + } + + const micPub = this.props.micPub; + const cameraPub = this.props.cameraPub; + return ( +
    + <> + + + {micPub && micPub.isMuted ? + mic_off + : null + } + {cameraPub && cameraPub.isMuted ? + <> +
    + +
    + + : null} +
    + {this.props.name} +
    + +
    + ); + } +} + +export default injectIntl(VCCarouselItem); diff --git a/src/widgets/vc-carousel.jsx b/src/widgets/vc-carousel.jsx new file mode 100644 index 00000000..204175af --- /dev/null +++ b/src/widgets/vc-carousel.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { injectIntl } from 'react-intl'; +import { + Track, +} from 'livekit-client'; + +import LetterTile from './letter-tile.jsx'; +import VCCarouselItem from './vc-carousel-item.jsx'; + +class VCCarousel extends React.Component { + constructor(props) { + super(props); + this.handlePrevClick = this.handlePrevClick.bind(this); + this.handleNextClick = this.handleNextClick.bind(this); + + this.carouselRef = React.createRef(); + this.contentRef = React.createRef(); + this.prevRef = React.createRef(); + this.nextRef = React.createRef(); + } + + handlePrevClick() { + const gap = 16; + const width = this.carouselRef.current.offsetWidth; + this.carouselRef.current.scrollBy(-(width + gap), 0); + if (this.carouselRef.current.scrollLeft - width - gap <= 0) { + this.prevRef.current.style.display = "none"; + } + if (!this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) { + this.nextRef.current.style.display = "flex"; + } + } + + handleNextClick() { + const gap = 16; + const width = this.carouselRef.current.offsetWidth; + this.carouselRef.current.scrollBy(width + gap, 0); + if (this.carouselRef.current.scrollWidth !== 0) { + this.prevRef.current.style.display = "flex"; + } + if (this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) { + this.nextRef.current.style.display = "none"; + } + } + + render() { + let peers = []; + if (this.props.participants) { + Object.entries(this.props.participants).forEach(([identity, party], i) => { + const participant = party.participant; + const cameraPub = participant.getTrack(Track.Source.Camera); + const micPub = participant.getTrack(Track.Source.Microphone); + + peers.push(); + }); + } + return ( + <> + + + ); + } +} + +export default injectIntl(VCCarousel); diff --git a/src/widgets/vc-panel.jsx b/src/widgets/vc-panel.jsx new file mode 100644 index 00000000..ed7401fa --- /dev/null +++ b/src/widgets/vc-panel.jsx @@ -0,0 +1,424 @@ +// Video conference or broadcast: local and/or peer viewports and controls. +import React from 'react'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; + +import { + connect, + LocalParticipant, + MediaDeviceFailure, + Participant, + ParticipantEvent, + RemoteParticipant, + RemoteTrack, + RemoteTrackPublication, + Room, + RoomEvent, + Track, + VideoPresets, +} from 'livekit-client'; + +import LetterTile from './letter-tile.jsx'; +import VCCarousel from './vc-carousel.jsx'; +import { makeImageUrl } from '../lib/blob-helpers.js'; + +import { MAX_PEER_TITLE_LENGTH } from '../config.js'; +import { CALL_STATE_OUTGOING_INITATED } from '../constants.js'; + +import { clipStr } from '../lib/utils.js' + +class VCPanel extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + room: undefined, + + participants: {}, + activeSpeaker: undefined, + + previousOnInfo: undefined, + }; + + this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED; + const topic = this.props.tinode.getTopic(this.props.topic); + this.isBroadcast = topic && topic.chan; + + this.localRef = React.createRef(); + this.remoteRef = React.createRef(); + + this.onInfo = this.onInfo.bind(this); + this.start = this.start.bind(this); + this.stop = this.stop.bind(this); + + + this.reportError = this.reportError.bind(this); + + this.handleCloseClick = this.handleCloseClick.bind(this); + this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this); + this.handleToggleMicClick = this.handleToggleMicClick.bind(this); + + this.handleRemoteHangup = this.handleRemoteHangup.bind(this); + + this.handleTrackSubscribed2 = this.handleTrackSubscribed2.bind(this); + this.handleTrackUnsubscribed2 = this.handleTrackUnsubscribed2.bind(this); + this.handleActiveSpeakerChange2 = this.handleActiveSpeakerChange2.bind(this); + this.handleDisconnect2 = this.handleDisconnect2.bind(this); + this.handleLocalTrackUnpublished2 = this.handleLocalTrackUnpublished2.bind(this); + this.handleLocalTrackPublished2 = this.handleLocalTrackPublished2.bind(this); + this.handleParticipantConnected2 = this.handleParticipantConnected2.bind(this); + this.handleParticipantDisconnected2 = this.handleParticipantDisconnected2.bind(this); + this.handleData2 = this.handleData2.bind(this); + this.handleDevicesChanged = this.handleDevicesChanged.bind(this); + this.startRoom = this.startRoom.bind(this); + + this.participants = {}; + } + + componentDidMount() { + const topic = this.props.tinode.getTopic(this.props.topic); + this.previousOnInfo = topic.onInfo; + topic.onInfo = this.onInfo; + this.start(); + } + + componentWillUnmount() { + const topic = this.props.tinode.getTopic(this.props.topic); + topic.onInfo = this.previousOnInfo; + this.stop(); + + } + + onInfo(info) { + if (info.what != 'call') { + return; + } + switch (info.event) { + case 'vc-token': + const ts = info.payload; + this.setState({token: ts.token}); + this.startRoom(ts.token); + break; + case 'hang-up': + this.handleRemoteHangup(info); + break; + default: + console.warn("Unknown call event", info.event); + break; + } + } + + + handleParticipantConnected2(participant) { + if (!participant) return; + if (participant instanceof RemoteParticipant) { + participant + .on(ParticipantEvent.TrackMuted, pub => { + console.log('track was muted', pub.trackSid, participant.identity); + this.forceUpdate(); // Redraw carousel. + }) + .on(ParticipantEvent.TrackUnmuted, pub => { + console.log('track was unmuted', pub.trackSid, participant.identity); + this.forceUpdate(); // Redraw carousel. + }) + .on(ParticipantEvent.IsSpeakingChanged, _ => { + this.forceUpdate(); // Redraw carousel. + }) + .on(ParticipantEvent.ConnectionQualityChanged, _ => { + this.forceUpdate(); // Redraw carousel. + }); + + const me = this.props.tinode.getMeTopic(); + const c = me.getContact(participant.identity); + const name = c && c.public && c.public.fn ? c.public.fn : participant.identity; + const photo = makeImageUrl(c && c.public ? c.public.photo : null); + const p = { + name: name, + photo: photo, + participant: participant + }; + this.participants[participant.identity] = p + console.log('participant connected', this.participants); + this.setState({participants: this.participants}, _ => { this.forceUpdate(); }); + } + } + + handleParticipantDisconnected2(participant) { + console.log('Deleting participant ', participant.identity); + delete this.participants[participant.identity]; + this.setState({participants: this.participants}); + } + + handleData2(msg, participant) { + console.log('Data message ', msg, participant); + } + + handleTrackSubscribed2(track, publication, participant) { + if (participant instanceof RemoteParticipant) { + // Check if we're presenting a remote video stream. + if (this.remoteRef.current && !(this.remoteRef.current.src != '' || this.remoteRef.srcObject)) { + track.attach(this.remoteRef.current); + } + } + // Redraw the carousel and remote view. + this.forceUpdate(); + } + + handleTrackUnsubscribed2(track, publication, participant) { + console.log('handleTrackUnsubscribed2', track); + // remove tracks from all attached elements + track.detach(); + this.forceUpdate(); + } + + handleLocalTrackPublished2(pub) { + console.log('Local track published', pub); + const track = pub.track; + if (this.localRef.current) { + const elem = this.localRef.current; + track.attach(elem); + // flip. + elem.style.transform = 'scale(-1, 1)'; + } + } + + handleLocalTrackUnpublished2(pub) { + const track = pub.track; + console.log('handleLocalTrackUnpublished2', track); + track.detach(); + // Redraw carousel. + this.forceUpdate(); + } + + handleActiveSpeakerChange2(speakers) { + console.log('handleActiveSpeakerChange2', speakers); + speakers.forEach(s => { + if (s.isSpeaking) { + console.log('Speaking participant ', s); + + // Display speaker in remote peer view. + const elem = this.remoteRef.current; + const me = this.props.tinode.getMeTopic(); + const c = me.getContact(s.identity); + const name = c && c.public && c.public.fn ? c.public.fn : undefined; + this.setState({activeSpeaker: name}); + + const cameraPub = s.getTrack(Track.Source.Camera); + const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted; + if (cameraEnabled && cameraPub.videoTrack) { + cameraPub.videoTrack.attach(elem); + } + } + }); + } + + handleDevicesChanged() { + console.log('devices changed'); + // TODO: implement. + } + + handleDisconnect2() { + console.log('disconnected from room'); + this.handleCloseClick(); + } + + startRoom(token) { + const roomOpts = { + adaptiveStream: true, + dynacast: true, + publishDefaults: { + simulcast: true, + videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216], + videoCodec: 'vp8' + }, + videoCaptureDefaults: { + resolution: VideoPresets.h720.resolution + } + }; + + const connectOpts = { + autoSubscribe: true + }; + + // creates a new room with options + const room = new Room(roomOpts); + + this.setState({room: room}); + + const startTime = Date.now(); + const topic = this.props.tinode.getTopic(this.props.topic); + const shouldPublish = this.isOutgoingCall || !this.isBroadcast; + + room + .on(RoomEvent.ParticipantConnected, this.handleParticipantConnected2) + .on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected2) + .on(RoomEvent.DataReceived, this.handleData2) + .on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed2) + .on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed2) + .on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange2) + .on(RoomEvent.Disconnected, this.handleDisconnect2) + .on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished2) + .on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished2) + + .on(RoomEvent.RoomMetadataChanged, metadata => { + console.log('VC room new metadata ', metadata); + }) + .on(RoomEvent.MediaDevicesChanged, this.handleDevicesChanged) + .on(RoomEvent.AudioPlaybackStatusChanged, e => { + // TODO: handle it. + console.log('Audio playback status change: ', e); + }) + .on(RoomEvent.MediaDevicesError, e => { + const failure = MediaDeviceFailure.getFailure(e); + this.reportError(failure); + }) + .on(RoomEvent.ConnectionQualityChanged, (quality, participant) => { + const id = participant ? participant.identity : 'nil'; + console.log('Connection quality changed', id, ' -> ', quality); + }) + .on(RoomEvent.SignalConnected, _ => { + const signalConnectionTime = Date.now() - startTime; + console.log(`signal connection established in ${signalConnectionTime}ms`); + // speed up publishing by starting to publish before it's fully connected + // publishing is accepted as soon as signal connection has established + if (shouldPublish) { + room.localParticipant.enableCameraAndMicrophone(); + console.log(`tracks published in ${Date.now() - startTime}ms`); + } + }); + + const vcEndpoint = this.props.tinode.getServerParam('vcEndpoint', null); + room.connect(vcEndpoint, token, connectOpts).then(_ => { + console.log('Connected to VC room', room.name, '. Access token', token); + + room.participants.forEach((participant) => { + this.handleParticipantConnected2(participant); + }); + this.handleParticipantConnected2(room.localParticipant); + }); + + } + + start() { + if (this.state.localStream) { + this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info'); + return; + } + if (this.isOutgoingCall) { + this.props.onCreateCall(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly) + .then(ctrl => { + const token = ctrl.params.token; + const seq = ctrl.params.seq; + this.setState({callSeq: seq, token: token}); + this.startRoom(token); + }); + } else { + this.props.onJoin(this.props.topic, this.props.seq); + } + } + + stop() { + if (this.state.room) { + this.state.room.disconnect(); + } + } + + reportError(err) { + this.props.onError(err.message, 'err'); + } + + // Call disconnected by remote. + handleRemoteHangup() { + this.handleCloseClick(); + } + + handleCloseClick() { + this.stop(); + this.props.onHangup(this.props.topic, this.props.seq); + } + + handleToggleCameraClick() { + if (!this.state.room) return; + const enabled = this.state.room.localParticipant.isCameraEnabled; + this.state.room.localParticipant.setCameraEnabled(!enabled) + .then(_ => this.forceUpdate()); + this.forceUpdate(); + } + + handleToggleMicClick() { + if (!this.state.room) return; + const enabled = this.state.room.localParticipant.isMicrophoneEnabled; + this.state.room.localParticipant.setMicrophoneEnabled(!enabled) + .then(_ => this.forceUpdate()); + + // Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off). + this.forceUpdate(); + } + + render() { + const isPublishing = this.isOutgoingCall || !this.isBroadcast; + const disabled = !isPublishing || !this.state.room || !this.state.room.localParticipant; + const audioIcon = !disabled && this.state.room.localParticipant.isMicrophoneEnabled ? 'mic' : 'mic_off'; + const videoIcon = !disabled && this.state.room.localParticipant.isCameraEnabled ? 'videocam' : 'videocam_off'; + + const peerTitle = this.state.activeSpeaker ? this.state.activeSpeaker : clipStr(this.props.title, MAX_PEER_TITLE_LENGTH); + + const remoteActive = this.remoteRef.current && (this.remoteRef.current.src != '' || this.remoteRef.current.srcObject) + + return ( + <> +
    + { !this.isBroadcast && Object.keys(this.state.participants).length > 0 ? + : + null + } +
    + { isPublishing ? +
    +
    : + null } +
    +
    +
    +
    + + + +
    +
    + + ); + } +}; + +export default injectIntl(VCPanel); From 55fc4356da41e7e006869a484adf4caa62743afb Mon Sep 17 00:00:00 2001 From: aforge Date: Mon, 8 May 2023 16:52:08 -0700 Subject: [PATCH 02/17] Remove '2' from VCPanel event handler names. --- src/widgets/vc-panel.jsx | 64 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/widgets/vc-panel.jsx b/src/widgets/vc-panel.jsx index ed7401fa..c6a0ce9c 100644 --- a/src/widgets/vc-panel.jsx +++ b/src/widgets/vc-panel.jsx @@ -59,15 +59,15 @@ class VCPanel extends React.PureComponent { this.handleRemoteHangup = this.handleRemoteHangup.bind(this); - this.handleTrackSubscribed2 = this.handleTrackSubscribed2.bind(this); - this.handleTrackUnsubscribed2 = this.handleTrackUnsubscribed2.bind(this); - this.handleActiveSpeakerChange2 = this.handleActiveSpeakerChange2.bind(this); - this.handleDisconnect2 = this.handleDisconnect2.bind(this); - this.handleLocalTrackUnpublished2 = this.handleLocalTrackUnpublished2.bind(this); - this.handleLocalTrackPublished2 = this.handleLocalTrackPublished2.bind(this); - this.handleParticipantConnected2 = this.handleParticipantConnected2.bind(this); - this.handleParticipantDisconnected2 = this.handleParticipantDisconnected2.bind(this); - this.handleData2 = this.handleData2.bind(this); + this.handleTrackSubscribed = this.handleTrackSubscribed.bind(this); + this.handleTrackUnsubscribed = this.handleTrackUnsubscribed.bind(this); + this.handleActiveSpeakerChange = this.handleActiveSpeakerChange.bind(this); + this.handleDisconnect = this.handleDisconnect.bind(this); + this.handleLocalTrackUnpublished = this.handleLocalTrackUnpublished.bind(this); + this.handleLocalTrackPublished = this.handleLocalTrackPublished.bind(this); + this.handleParticipantConnected = this.handleParticipantConnected.bind(this); + this.handleParticipantDisconnected = this.handleParticipantDisconnected.bind(this); + this.handleData = this.handleData.bind(this); this.handleDevicesChanged = this.handleDevicesChanged.bind(this); this.startRoom = this.startRoom.bind(this); @@ -108,7 +108,7 @@ class VCPanel extends React.PureComponent { } - handleParticipantConnected2(participant) { + handleParticipantConnected(participant) { if (!participant) return; if (participant instanceof RemoteParticipant) { participant @@ -142,17 +142,17 @@ class VCPanel extends React.PureComponent { } } - handleParticipantDisconnected2(participant) { + handleParticipantDisconnected(participant) { console.log('Deleting participant ', participant.identity); delete this.participants[participant.identity]; this.setState({participants: this.participants}); } - handleData2(msg, participant) { + handleData(msg, participant) { console.log('Data message ', msg, participant); } - handleTrackSubscribed2(track, publication, participant) { + handleTrackSubscribed(track, publication, participant) { if (participant instanceof RemoteParticipant) { // Check if we're presenting a remote video stream. if (this.remoteRef.current && !(this.remoteRef.current.src != '' || this.remoteRef.srcObject)) { @@ -163,14 +163,14 @@ class VCPanel extends React.PureComponent { this.forceUpdate(); } - handleTrackUnsubscribed2(track, publication, participant) { - console.log('handleTrackUnsubscribed2', track); + handleTrackUnsubscribed(track, publication, participant) { + console.log('handleTrackUnsubscribed', track); // remove tracks from all attached elements track.detach(); this.forceUpdate(); } - handleLocalTrackPublished2(pub) { + handleLocalTrackPublished(pub) { console.log('Local track published', pub); const track = pub.track; if (this.localRef.current) { @@ -181,16 +181,16 @@ class VCPanel extends React.PureComponent { } } - handleLocalTrackUnpublished2(pub) { + handleLocalTrackUnpublished(pub) { const track = pub.track; - console.log('handleLocalTrackUnpublished2', track); + console.log('handleLocalTrackUnpublished', track); track.detach(); // Redraw carousel. this.forceUpdate(); } - handleActiveSpeakerChange2(speakers) { - console.log('handleActiveSpeakerChange2', speakers); + handleActiveSpeakerChange(speakers) { + console.log('handleActiveSpeakerChange', speakers); speakers.forEach(s => { if (s.isSpeaking) { console.log('Speaking participant ', s); @@ -216,7 +216,7 @@ class VCPanel extends React.PureComponent { // TODO: implement. } - handleDisconnect2() { + handleDisconnect() { console.log('disconnected from room'); this.handleCloseClick(); } @@ -249,15 +249,15 @@ class VCPanel extends React.PureComponent { const shouldPublish = this.isOutgoingCall || !this.isBroadcast; room - .on(RoomEvent.ParticipantConnected, this.handleParticipantConnected2) - .on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected2) - .on(RoomEvent.DataReceived, this.handleData2) - .on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed2) - .on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed2) - .on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange2) - .on(RoomEvent.Disconnected, this.handleDisconnect2) - .on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished2) - .on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished2) + .on(RoomEvent.ParticipantConnected, this.handleParticipantConnected) + .on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected) + .on(RoomEvent.DataReceived, this.handleData) + .on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed) + .on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed) + .on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange) + .on(RoomEvent.Disconnected, this.handleDisconnect) + .on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished) + .on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished) .on(RoomEvent.RoomMetadataChanged, metadata => { console.log('VC room new metadata ', metadata); @@ -291,9 +291,9 @@ class VCPanel extends React.PureComponent { console.log('Connected to VC room', room.name, '. Access token', token); room.participants.forEach((participant) => { - this.handleParticipantConnected2(participant); + this.handleParticipantConnected(participant); }); - this.handleParticipantConnected2(room.localParticipant); + this.handleParticipantConnected(room.localParticipant); }); } From 981ef684a76bceb0133bb768c94ed73f1429134d Mon Sep 17 00:00:00 2001 From: aforge Date: Mon, 8 May 2023 17:01:42 -0700 Subject: [PATCH 03/17] Add livekit-client dep. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cce78976..5a4a5451 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "firebase": "^9.17.2", "libphonenumber-js": "^1.10.24", + "livekit-client": "^1.9.3", "npm-check-updates": "^16.7.12", "react": "^18.2.0", "react-dom": "^18.2.0", From f7120d824dca7ea34224a6af0027721718b72dff Mon Sep 17 00:00:00 2001 From: aforge Date: Tue, 9 May 2023 14:23:26 -0700 Subject: [PATCH 04/17] Some style fixes. --- css/base.min.css | 2 +- src/constants.js | 3 + src/i18n.min/en.json | 2 +- src/i18n/de.json | 7 + src/i18n/en.json | 7 + src/i18n/es.json | 7 + src/i18n/ex/base-en.json | 4 + src/i18n/fr.json | 7 + src/i18n/ko.json | 7 + src/i18n/ro.json | 7 + src/i18n/ru.json | 7 + src/i18n/zh-TW.json | 7 + src/i18n/zh.json | 7 + src/views/messages-view.jsx | 2 +- src/views/tinode-web.jsx | 4 +- src/widgets/vc-carousel-item.jsx | 16 +- src/widgets/vc-carousel.jsx | 24 +- src/widgets/vc-panel.jsx | 10 +- umd/246.prod.js | 2 +- umd/246.prod.js.map | 2 +- umd/311.prod.js | 2 +- umd/327.prod.js | 2 +- umd/365.prod.js | 2 +- umd/501.prod.js | 2 + umd/501.prod.js.map | 1 + umd/514.prod.js | 2 +- umd/index.dev.js | 23546 +++++++++++++++- umd/index.dev.js.map | 2 +- umd/index.prod.js | 2 +- umd/index.prod.js.map | 2 +- umd/src_i18n_min_en_json.dev.js | 2 +- umd/tinode.dev.js | 8 +- umd/tinode.dev.js.map | 2 +- umd/tinode.prod.js | 2 +- umd/tinode.prod.js.map | 2 +- ...de_modules_libphonenumber-js-7e28c7.dev.js | 268 +- ...odules_libphonenumber-js-7e28c7.dev.js.map | 2 +- 37 files changed, 23592 insertions(+), 391 deletions(-) create mode 100644 umd/501.prod.js create mode 100644 umd/501.prod.js.map diff --git a/css/base.min.css b/css/base.min.css index 1f74322e..ebb5f232 100644 --- a/css/base.min.css +++ b/css/base.min.css @@ -1 +1 @@ -*{box-sizing:border-box;font-family:Roboto,Arial,sans-serif;margin:0;padding:0;scrollbar-color:#0000004d #ffffff1a;scrollbar-width:thin;text-rendering:optimizeLegibility}body,html{background-color:silver;box-shadow:inset 0 10rem #3949ab;color:#666;font-size:10pt;height:100%;overflow:hidden;width:100%}tt{font-family:Roboto Mono,Courier,monospace}a{color:#2196f3;text-decoration:none}a:focus,a:hover{color:#0a6ebd;text-decoration:underline}a.danger{color:#d32f2f}a.danger:focus,a.danger:hover{color:#f44336}a.gray:focus,a.gray:hover{color:#999}form{line-height:1.5;padding:.5rem .75rem}form,label{font-size:1.1rem}label{margin-right:auto}label.small{color:#1e88e5;font-size:.95rem}label.large{color:#1e88e5;font-size:1.1rem}img.avatar{border-radius:50%;height:100%;width:100%}button{border:1px solid #0000;border-radius:4px;box-shadow:0 1px 3px 0 #00000040;cursor:pointer;display:inline-block;font-family:inherit;font-size:1.05rem;font-weight:400;height:2.2rem;line-height:2.2rem;margin:0 .35rem;min-width:5rem;padding-left:1.2rem;padding-right:1.2rem;text-align:center;text-transform:uppercase;touch-action:manipulation;vertical-align:middle;white-space:nowrap}button[disabled],html input[disabled]{box-shadow:none;cursor:default;filter:grayscale(50%);opacity:.65}button.round{border-radius:50%;box-shadow:0 2px .5rem 0 #00000040;height:3rem;line-height:3.5rem;margin:0;outline:none;padding:0;width:3rem;z-index:2}button.round.small{box-shadow:0 2px .35rem 0 #00000040;height:2rem;line-height:2rem;width:2rem}button:active:not([disabled]),button:hover:not([disabled]){color:#fff}button:active{box-shadow:inset 0 3px .5rem rgba(0,0,0,.125)}button:active.round.small{box-shadow:inset 0 2px .35rem rgba(0,0,0,.125)}button.primary{background-color:#1e88e5;color:#fff}button.primary:active:not([disabled]),button.primary:hover:not([disabled]){background-color:#2962ff}button.secondary{background-color:#fff;color:#2196f3}button.secondary:active:not([disabled]),button.secondary:hover:not([disabled]){background-color:#e3f2fd}button.outline{background-color:initial;border:1px solid silver;box-shadow:none;color:#2196f3}button.outline:active:not([disabled]),button.outline:hover:not([disabled]){background-color:#e8eaf6}button.danger{background-color:#ec6560;color:#fff}button.danger:active:not([disabled]),button.danger:hover:not([disabled]){background-color:#f77}button.positive{background-color:#71c46f;color:#fff}button.positive:active:not([disabled]),button.positive:hover:not([disabled]){background-color:#7fed7f}input,textarea{background:#0000;border:none;color:#666;font-family:inherit;font-size:1.1rem;margin:.25rem 0;outline:none;padding:0 .125rem}input[type=email],input[type=password],input[type=search],input[type=tel],input[type=text],textarea{border-bottom:1px solid #ccc;box-shadow:0 1px 0 #0000001a;height:2.5rem;transition:all .1s ease-in-out,height 0s}input[type=email],input[type=password],input[type=tel],input[type=text],textarea{width:100%}input[type=email]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,textarea:focus{border-bottom:1px solid #2196f3;box-shadow:0 1px 0 #2196f340}input[type=email].invalid,input[type=password].invalid,input[type=search].invalid,input[type=tel].invalid,input[type=text].invalid,textarea.invalid{border-bottom:1px solid #e57373;box-shadow:0 1px 0 #e5737340}input[type=email]::placeholder,input[type=password]::placeholder,input[type=search]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,textarea::placeholder{color:#bbb;font-weight:lighter}input[type=password].with-visibility,input[type=text].with-visibility{margin-right:-2rem;padding-right:2rem}textarea{padding-top:.45rem;resize:none}textarea.inplace-edit{font-size:unset;height:unset}::-webkit-scrollbar{width:.6rem}::-webkit-scrollbar-track{background-color:#ffffff1a;-webkit-box-shadow:inset 0 0 6px #0003;box-shadow:inset 0 0 6px #0003}::-webkit-scrollbar-thumb{background-color:#0000004d}ul.menu{background-color:#fff;border-radius:.2rem;box-shadow:0 1px 6px #00000073;list-style:none;padding-bottom:.35rem;padding-top:.35rem;position:absolute;width:12rem;z-index:4}ul.menu>li{cursor:pointer;padding:.75rem .75rem .75rem 1.25rem}ul.menu>li:hover{background-color:#f5f5f5}ul.menu>li.separator{height:1px;margin:2px;padding:0}ul.menu>li.separator,ul.menu>li.separator:hover{background-color:#ccc}.menuTrigger{background-color:inherit;border-radius:.7rem;height:1.4rem;line-height:1.4rem;opacity:0;position:absolute;right:.25rem;text-align:center;width:1.4rem}.menuTrigger a{color:#757575}li .menuTrigger{top:.25rem}li:hover .menuTrigger{opacity:.9}li:hover .menuTrigger a{color:#757575}div.alert-container{align-items:center;background:#f0f0f0d9;display:flex;height:100%;justify-content:center;overflow:hidden;padding:1rem;position:absolute;width:100%;z-index:3}div.alert{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;height:min-content;margin:auto 2rem;max-height:40rem;max-width:24rem;min-width:16rem;overflow:hidden;padding:1rem;position:relative;width:fit-content}div.alert-container .title{color:#2196f3;font-size:1.2rem;font-weight:500;padding:0 0 .35rem;position:relative;width:100%}div.alert-container .title.with-control{align-items:center;display:flex;flex:none;justify-content:space-between;padding:.5rem .5rem 0;position:relative}div.alert-container .content{padding:.5rem 0;position:relative}div.trusted-badge{margin:.25rem 0}.blue{color:#2196f3!important}.lt-blue{color:#64b5f6!important}.verified-color{color:#03a9f4!important}.staff-color{color:#689f38!important}.danger-color{color:#ffc107!important}.green{color:#388e3c}.white{color:#fff!important}.red{color:#d32f2f!important}.badge-inv{color:#fffd!important}.gray{color:#757575!important}.light-gray{color:#bbb!important}.large{font-size:120%}.small{font-size:90%}.float-right{position:absolute;right:.5rem}.hr{background-color:#f0f0f0;box-shadow:inset 0 .25rem .25rem -.25rem #00000026,inset 0 -.25rem .25rem -.25rem #00000026;display:block;height:.45rem;min-height:.3rem;width:100%}.hr.thin{height:.015rem}.ellipsized{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.flat-button{align-items:center;background-color:#fff;display:flex;font-weight:400;height:2em;line-height:2em;margin:.15rem .35rem;text-align:center;text-transform:uppercase;touch-action:manipulation;vertical-align:middle;white-space:nowrap;z-index:1}.flat-button:focus,.flat-button:hover{text-decoration:none}.flat-button.faux{background-color:#eee;border:1px solid #999;border-radius:3px;display:inline-block;height:1.4em;line-height:1.4em;margin:.15rem;padding:0 .35rem;text-transform:none}.action-button{background-color:#fafafa;border-radius:50%;bottom:1rem;box-shadow:0 2px .5rem 0 #00000040;color:#999;display:block;font-size:1.8rem;height:3.25rem;margin:0;min-height:unset;min-width:unset;outline:none;padding:0;position:absolute;right:1rem;transition:all .15s;width:3.25rem;z-index:2}.action-button:focus,.action-button:hover{background-color:#f0f0f0;color:#888}.action-button:active{background-color:#eee;color:#777}.action-button.hidden{bottom:2.625rem;font-size:1px;height:1px;line-height:1px;opacity:0;right:2.625rem;width:1px}.group-focus{outline:none}#mountPoint{height:100%;padding-bottom:1rem;padding-top:1rem}#noscript-message{background-color:#fff;border-radius:.25rem;box-shadow:1px 2px .3rem gray;display:flex;flex-direction:column;margin-left:auto;margin-right:auto;margin-top:5rem;max-width:30rem;overflow:hidden;position:relative}#noscript-title{background-color:#2196f3;color:#bbdefb;font-size:120%;font-weight:bolder;padding:1rem}#noscript-content{padding:1rem}#app-container{background-color:#fff;border-radius:.25rem;box-shadow:1px 2px .3rem gray;display:flex;height:100%;margin-left:auto;margin-right:auto;max-width:84rem;overflow:hidden;position:relative}#sidepanel{flex-grow:0;flex-shrink:0.3;min-width:18rem;width:24rem}#sidepanel,#topic-view{display:flex;flex-direction:column;height:100%;position:relative}#topic-view{overflow-x:hidden;width:100%}#info-view{display:flex;flex-direction:column;flex-grow:0;flex-shrink:0.3;height:100%;min-width:18rem;width:24rem}.center-medium-text{align-items:center;display:flex;font-size:1.4rem;height:100%;justify-content:center;text-align:center;width:100%}.nodisplay{display:none!important}.quoted{margin-left:1rem}.space-right{margin-right:.25rem}.rounded-container{background-color:#fafafa;border-radius:.1rem;margin:.25rem;padding:.25rem;position:relative}.in-place-edit{border-bottom:1px dashed #ddd;cursor:pointer;display:inline-block;font-family:inherit;font-size:1rem;margin:.25rem .125rem 0;vertical-align:initial;width:100%}.in-place-edit.short{font-size:1.1rem;height:2.5rem;line-height:2.25}.in-place-edit.disabled{border-bottom:none;color:#888;cursor:default}.in-place-edit.placeholder{color:#bbb}.in-place-edit span{display:inline-block}.in-place-edit.short span{display:block;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}table.permission-editor{width:100%}table.permission-editor td{font-size:1.1rem;margin:.35rem 0;vertical-align:middle}table.permission-editor td.checkbox{color:#999;text-align:center}.permission-editor .material-icons{font-size:1.3rem;line-height:1.3rem}.material-icons.clickable{border-bottom:none;cursor:pointer}.clickable{border-bottom:1px dashed #ddd}.clean-clickable,.clickable,.image-clickable{cursor:pointer}.panel-title{font-size:1.4rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.panel-title.deleted{text-decoration:line-through}.caption-panel{box-shadow:0 1px 2px gray;flex:none;height:4rem;overflow-x:hidden;white-space:nowrap;z-index:1}.caption-panel,.dialog-buttons{align-items:center;display:flex;padding:.5rem}.dialog-buttons{justify-content:flex-end}.dialog-buttons button{margin:0 .5rem}.avatar-box{flex-grow:0;flex-shrink:0;position:relative}.bot-form{display:flex;flex-direction:column;line-height:2rem;min-width:8rem;overflow:hidden}.bot-form div{min-height:2rem;vertical-align:middle}.bot-form button{background-color:#eef;color:#2196f3;display:inline-block;font-size:inherit;height:2rem;line-height:2rem;margin:.15rem;text-transform:none}.bot-form>div>button{width:auto}.bot-form button:active,.bot-form button:focus,.bot-form button:hover{background-color:#e3f2fd;outline:0}#side-caption-panel{background-color:#2196f3;color:#bbdefb}#side-caption-panel a{color:#bbdefb;line-height:1;padding-left:.5rem}#side-caption-panel :hover{color:#fff}#sidepanel-title{margin-left:.5rem;margin-right:auto}#self-avatar{border-radius:50%;flex:none;height:3rem;min-width:3rem;width:3rem}#self-avatar.avatar-box{font-size:1.8rem}#settings-form ul{list-style:none}#settings-form li{vertical-align:middle}#settings-form label{display:inline-block;padding-left:.5rem}input#host-name{width:100%}input.search{text-indent:1.4rem}i.search{left:.6rem;position:absolute}.info-box{display:none;overflow:hidden;padding:1rem 1.5rem .75rem .5rem;position:relative}.info-box>span{line-height:1.35rem}.info-box .cancel{position:absolute;right:.25rem;top:.5rem}.info-box.error{background-color:#ffcdd2;border-bottom:1px solid #ffbfc8;color:#b71c1c;display:flex}.info-box.warning{background-color:#fff9c4;border-bottom:1px solid #fff59d;color:#6d4c41;display:flex}.info-box.info{background-color:#e1f5fe;border-bottom:1px solid #b3e5fc;color:#0288d1;display:flex}.info-box .cancel .material-icons{color:#757575;font-size:1rem}.info-box.error .cancel .material-icons{color:#b71c1c}.info-box .icon{display:block;margin-right:.5rem}.info-box .icon .material-icons{font-size:1.5rem}.info-box.warning .icon .material-icons{color:#fbc02d}.info-box.error .icon .material-icons{color:#ef5350}.info-box.info .icon .material-icons{color:#0288d1}.flex-column{display:flex;flex-direction:column;height:100%;overflow-x:hidden}.flex-column.narrow{height:auto}.scrollable-panel{height:100%;margin-bottom:auto;margin-top:.25rem;overflow-x:hidden;overflow-y:auto;scroll-behavior:smooth}.contact-box{list-style:none;overflow-x:hidden}.contact-box>li{background:linear-gradient(90deg,#0000 3.5rem,#eee 1px,#eee) 0 100% /100% 1px no-repeat,#fff;cursor:pointer;display:flex;flex-grow:0;flex-shrink:0;min-height:3.75rem;overflow-x:hidden;padding:.35rem .25rem .5rem .35rem;position:relative}.contact-box.small>li{min-height:2.5rem;padding:.25rem .25rem .35rem .35rem}.contact-box>li:last-child{background:#fff}.contact-box>li:hover{background-color:#eceff1}.contact-box>li.selected{background-color:#cfd8dc}.contact-box>li.action{border-bottom:none;min-height:2.5rem}.contact-box .text-box{overflow-x:hidden}.contact-box .contact-title{font-size:1.05rem;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-box .contact-title.deleted{opacity:.66;text-decoration:line-through;text-decoration-thickness:.5px}.contact-box .contact-comment{font-weight:lighter}.contact-box .contact-comment,.contact-box .contact-comment>span{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-box .contact-comment b{font-weight:bolder}.contact-box .contact-comment .material-icons{color:#777;margin-bottom:.2rem}.contact-box .avatar-box{font-size:1.8rem;height:3rem;margin-right:.35rem;min-height:3rem;min-width:3rem;overflow:hidden;padding:0;position:relative;width:3rem}.contact-box .avatar{border-radius:50%;height:3rem;width:3rem}.contact-box.small .avatar-box{font-size:1.2rem;height:2rem;min-height:2rem;min-width:2rem;width:2rem}.contact-box.small .avatar{height:2rem;width:2rem}.avatar.deleted{filter:grayscale(1);opacity:.75}.contact-box .avatar-box>.material-icons{display:flex;flex-direction:column;font-size:2.4rem;justify-content:space-around;line-height:3rem}.contact-box.small .avatar-box>.material-icons{font-size:1.6rem;line-height:2rem}.contact-box .text-box{display:flex;flex-direction:column;justify-content:space-around}.contact-box .text-box>div{align-items:center;display:flex;flex-direction:row}.contact-box .action-text{cursor:pointer;font-size:.95rem;line-height:2.5rem;margin:0 auto;text-align:center}img.channel{height:1.25rem;margin-left:.3rem;vertical-align:middle;width:1.25rem}.large img.channel{height:1.45rem;margin-bottom:.15rem;margin-left:.5rem;width:1.45rem}.avatar-box .online,.offline{border-radius:50%;bottom:.1rem;flex-shrink:0;height:.75rem;min-height:.75rem;min-width:.75rem;position:absolute;right:0;width:.75rem}.avatar-box .online{background-color:#4caf50}.avatar-box .offline{background-color:silver}.online.typing{animation:typing .5s infinite}@keyframes typing{0%,to{transform:translateX(0)}50%{transform:translateX(-.3rem)}}.contact-box .checkmark.material-icons{border:0 solid #0000;color:#0097a7;overflow:visible;padding-left:.5px}.contact-box .checkmark.material-icons,.contact-box .deleted.material-icons{background-color:#fff;bottom:0;font-size:1.5rem;height:1.5rem;position:absolute;right:0;width:1.5rem}.contact-box .deleted.material-icons{color:#999}#add-topic{bottom:1rem;position:absolute;right:1rem}.panel-form{display:flex;flex-direction:column;padding:0;position:relative}.panel-form-row{align-items:center;flex-shrink:0}.panel-form-column,.panel-form-row{display:flex;flex-grow:0;line-height:1.5;padding:.5rem .75rem;position:relative}.panel-form-column{flex-direction:column;overflow-x:hidden}.panel-form-column>.panel-form-column,.panel-form-row>.panel-form-column{flex-grow:1;padding:0!important}.panel-form-column>.panel-form-row,.panel-form-row>.panel-form-row{padding:0!important}.panel-form-column>.group{margin-bottom:.25rem;margin-top:.25rem}.avatar-upload{display:flex;flex-direction:column;flex-shrink:0;font-size:4.5rem;height:10.5rem;padding:0;position:relative;width:10rem}.avatar-upload.read-only{height:auto}.avatar-upload .clear-avatar{background-image:radial-gradient(#fff 40%,#0000 75%);font-size:1rem;opacity:0;position:absolute;right:0;top:0}.avatar-upload:hover .clear-avatar{opacity:1}.avatar-upload .avatar-box{height:10rem;position:relative;width:10rem}.avatar-upload .avatar-box>.material-icons{font-size:4rem;line-height:10rem;position:relative;vertical-align:middle}.avatar-upload:after{clear:both;content:".";display:block;height:0;visibility:hidden}.avatar-upload .blank{border:1px dashed #999;color:#999;flex-shrink:0;font-size:1rem;line-height:10rem;text-align:center;vertical-align:middle}.avatar-upload .blank,.avatar-upload img.preview{border-radius:50%;height:10rem;overflow:hidden;width:10rem}.avatar-upload img.preview:before{background:50% no-repeat url(../img/broken_image.png);border-radius:50%;color:#666;content:" ";display:flex;font-size:.9rem;font-weight:lighter;height:10rem;justify-content:center;left:0;position:absolute;top:0;width:10rem}.avatar-upload .inputfile.hidden{height:.1px;opacity:0;overflow:hidden;position:absolute;width:.1px}.avatar-upload .inputfile+label.round{align-items:center;background-color:#fafafa;border-radius:50%;bottom:0;box-shadow:0 1px 4px #0006;cursor:pointer;display:flex;height:3rem;justify-content:center;line-height:3rem;position:absolute;right:0;width:3rem;z-index:1}.avatar-upload .inputfile:focus+label.round{outline:none}.avatar-upload .inputfile+label:hover,.avatar-upload .inputfile:focus+label{background-color:#e3f2fd;color:#42a5f5}#topic-caption-panel{background-color:#b0bec5}#topic-caption-panel a{color:#757575;text-decoration:none}#topic-caption-panel :hover{color:#000}#topic-caption-panel .avatar-box{font-size:1.8rem;height:3rem;margin-right:.35rem;min-width:3rem;position:relative;width:3rem}#topic-caption-panel .avatar-box>.material-icons{font-size:2.4rem;line-height:3rem}#topic-title-group{overflow-x:hidden}#hide-message-view{display:block;margin-right:.35rem}#topic-users{margin-left:auto;margin-right:1rem;width:min-content}#topic-users .avatar-box{display:inline-block;font-size:1rem;height:1.8rem;margin-right:.2rem;min-height:1.8rem;min-width:1.8rem;width:1.8rem}#topic-users .avatar-box>.material-icons{font-size:1.4rem;line-height:1.8rem}#topic-users>span{line-height:2rem;vertical-align:top}#messages-panel{background-color:#eceff1;background-image:url(../img/bg_messages.png);height:100%;overflow-y:scroll;padding:.75rem;position:relative}#messages-container{height:100%;overflow-y:hidden;position:relative}#write-only-background{background-image:repeating-linear-gradient(45deg,#ffffff80,#ffffff80 .5rem,#fffc 0,#fffc 1rem);height:100%;left:0;position:absolute;top:0;width:100%}#write-only-note{height:auto;top:50%;transform:translate(-50%,-50%);width:auto}#peer-messaging-disabled-note,#write-only-note{background-color:#fff;border-radius:.5rem;box-shadow:0 1px .5px #0000001f;color:#666;left:50%;line-height:2rem;padding:.75rem 1.25rem;position:absolute;text-align:center}#peer-messaging-disabled-note{bottom:4.3rem;transform:translate(-50%);width:fit-content}#send-message-panel{align-items:center;background-color:#eceff1;display:flex;flex:none;min-height:4rem;padding:.4rem .4rem .4rem .6rem;z-index:2}#send-message-panel #writing-disabled{font-size:1.2rem;text-align:center;width:100%}#send-message-wrapper{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;z-index:2}#sendMessage{overflow:hidden;overflow-wrap:break-word}#send-message-panel .material-icons{font-size:1.6rem;min-width:2rem}#send-message-panel .material-icons.secondary,.material-icons.disabled{color:#999!important}#send-message-panel .material-icons.secondary:hover{color:#2196f3!important}#send-message-input{max-height:6rem;min-height:2.5rem;overflow:hidden auto}.accept-invite-panel{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;width:100%;z-index:1}.accept-invite-panel .title{font-size:1.2rem;padding:.75rem}.accept-invite-panel .footer{background-color:#fafafa;padding-bottom:.35rem;text-align:center;white-space:nowrap}.accept-invite-panel button{border:1px solid #0000;box-shadow:0 1px 2px 0 #00000040;font-size:1rem;height:2em;line-height:2em;margin:.25rem}#dummy-view{align-items:center;background-color:#e0f7fa;display:flex;height:100%;justify-content:center;width:100%}#dummy-view a{display:block;text-align:center;text-decoration:none}#image-preview{background-color:#ffffffe6;display:flex;flex-direction:column;height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}#image-preview-caption-panel{background-color:#eceff1;box-shadow:0 1px 2px silver;display:flex;height:4.6rem;justify-content:space-between;line-height:2.5rem;padding:1rem .75rem 0;z-index:3}#image-preview-caption-panel a{color:#757575;text-decoration:none}#image-preview-caption-panel a:hover{color:#000}#image-preview-container{align-items:center;display:flex;height:100%;justify-content:space-around;overflow:hidden;width:100%}#image-preview-footer{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;display:flex;height:3.2rem;justify-content:space-around;line-height:1.5rem;z-index:1}#image-preview-container .material-icons{font-size:6rem}#video-container{background-color:#ffffffe6;flex-direction:column;height:100%;justify-content:space-between;left:0;top:0}#video-container,.controls{display:flex;position:absolute;width:100%}.controls{align-items:center;bottom:2.5rem;column-gap:1rem;justify-content:center;z-index:1}.incoming-call .controls{bottom:1rem}.controls button{border-radius:50%;box-shadow:0 3px 6px 0 #00000040;height:4rem;min-width:auto;width:4rem}#video-container-panel{height:100%;position:relative;width:100%}.call-party{background-color:#eee;border:2px solid #ccc;border-radius:.5rem;overflow:hidden}.call-party:hover{border:2px solid #2196f3}.call-party.peer{bottom:1rem;left:1rem;position:absolute;right:1rem;top:1rem;z-index:1}.call-party.self{bottom:.5rem;height:10rem;position:absolute;right:.5rem;width:10rem;z-index:2}.call-party.self[disabled]{display:none}video{height:100%;object-fit:contain;width:100%}.call-party.self video{transform:scaleX(-1)}.call-party[disabled] video{display:none}.caller-card{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}.caller-card .avatar-box{font-size:6rem;height:10rem;width:10rem}.caller-card.pulse .lettertile,.caller-card.pulse img.avatar{animation:active-call 1.5s linear infinite}.caller-card .avatar-box .material-icons{font-size:6rem;line-height:1.6}.call-party .caller-name{background-color:#ffffffb3;border-radius:.35rem;padding:.15rem .5rem;text-shadow:0 0 1px #fff;z-index:1}.call-party.self .caller-name.inactive{bottom:.25rem;position:absolute;right:.25rem}.caller-card .caller-name{font-size:larger;margin-top:.5rem}.caller-card .trusted-badge{display:inline-block}.call-party.peer .caller-name.inactive{bottom:.5rem;font-size:larger;left:1rem;margin:0 auto;position:absolute}.incoming-call{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;margin:auto 2rem;max-width:24rem;min-width:16rem;position:relative;width:fit-content}.incoming-call .caller-card{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;left:auto;padding:4rem 3.5rem 5.5rem;position:relative;top:auto;width:100%}.incoming-call .caller-name{background-color:#ffffffb3;border-radius:.35rem;padding:.15rem .5rem;text-shadow:0 0 1px #fff;z-index:1}.call-message{display:flex;flex-direction:row}.call-message .duration{font-weight:lighter}@keyframes active-call{0%{box-shadow:0 0 0 .1rem #2196f34d,0 0 0 .3rem #2196f34d,0 0 0 .5rem #2196f34d}to{box-shadow:0 0 0 .4rem #2196f34d,0 0 0 2rem #2196f300,0 0 0 3rem #2196f300}}.chat-box{list-style:none;margin:0 .5rem;padding:0 0 3.5rem;scroll-behavior:smooth}.chat-box.group{margin-left:-.4rem}.chat-box li{align-items:flex-end;display:flex;margin:0;padding:0}.chat-box .right{justify-content:flex-end}.bubble{border-radius:.5rem;color:#212121;display:flex;filter:drop-shadow(.5px .5px .7px rgb(0,0,0,.3));height:auto;margin:0;max-width:36rem;min-width:9rem;padding:.75rem .5rem;position:relative;text-align:left;transition:background-color .5s ease-out;transition-delay:.3s}.chat-box li.single{margin-bottom:.75rem;margin-top:.25rem}.chat-box li.first{margin-bottom:.15rem;margin-top:.25rem}.chat-box li.middle{margin-bottom:.15rem;margin-top:.15rem}.chat-box li.last{margin-bottom:.75rem;margin-top:.15rem}.left .bubble{background-color:#c5e1a5;margin-right:.5rem}.left.flash .bubble{background-color:#94a97c}.left .bubble.tip{border-bottom-left-radius:0}.right .bubble{background-color:#fafafa;margin-left:1.75rem}.right.flash .bubble{background-color:#ccc}.right .bubble.tip{border-bottom-right-radius:0}.chat-box .meta{justify-content:center;margin-bottom:.5rem;margin-top:.5rem}.chat-box .meta .bubble{color:#666;min-width:unset}.meta .bubble.deleted{background-color:#e3f2fd;padding:.75rem .75rem .5rem}.meta .bubble.date{background-color:#fffc;font-size:smaller;padding:.5rem;text-transform:uppercase}.left .bubble.tip:before{background-color:#c5e1a5;border-bottom-left-radius:.3rem;bottom:0;clip-path:polygon(100% 0,0 110%,100% 110%);-webkit-clip-path:polygon(100% 0,0 110%,100% 110%);content:"";height:.85rem;left:-.8rem;position:absolute;transition:background-color .5s ease-out;transition-delay:.3s;width:.85rem}.left.flash .bubble.tip:before{background-color:#94a97c}.right .bubble.tip:before{background-color:#fafafa;border-bottom-right-radius:.3rem;bottom:0;clip-path:polygon(0 0,100% 110%,0 110%);-webkit-clip-path:polygon(0 0,100% 110%,0 110%);content:"";height:.85rem;left:99.8%;position:absolute;transition:background-color .5s ease-out;transition-delay:.3s;width:.85rem}.right.flash .bubble.tip:before{background-color:#ccc}.chat-box .avatar-box{font-size:.8rem;height:1.4rem;margin-bottom:.8rem;margin-right:.8rem;min-height:1.4rem;min-width:1.4rem;position:relative;width:1.4rem}.bubble div.content-meta{width:100%}.bubble div.message-content{margin:0;max-width:36rem;overflow-wrap:anywhere;padding:0 .75rem;position:relative;width:100%}.bubble div.message-content:after{clear:both;content:".";display:block;height:0;visibility:hidden}.chat-box .timestamp-padding{display:inline-block;height:0;width:4rem}.chat-box .timestamp{float:right;position:relative;top:.35rem;user-select:none;white-space:nowrap}.chat-box .author,.chat-box .timestamp{color:#777;font-size:80%}.image-preview,.inline-image{border-radius:.1rem;overflow:hidden;position:relative}.inline-image{max-height:24rem;max-width:36rem}.inline-image>.rounded-container{border-left:.25rem solid #0000;border-right:.25rem solid #0000;bottom:2rem;margin:.25rem auto;position:relative}img.image-preview:before,img.inline-image:before{background:50% no-repeat #ccc url(../img/broken_image.png);color:#666;display:flex;font-size:.9rem;font-weight:lighter;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}img.inline-image:before{background-size:3rem 3rem;content:attr(alt)}img.image-preview:before{background-size:10rem 10rem;content:" "}.inline-image.video>img.inline-image:before,img.image-preview.video:before{background:50% no-repeat #ccc url(../img/broken_video.png)}.inline-image.video>img.inline-image:before{background-size:3rem 3rem}.inline-video{align-items:center;display:flex;justify-content:center;position:relative}.inline-video>img.inline-image:before{background:#ccc}.inline-video .duration{background-color:#ffffff80;border-radius:.3rem;bottom:.25rem;color:#333;font-size:80%;left:.25rem;padding:.25rem .5rem;position:absolute}.inline-video .play-control{align-items:center;background-color:#0006;border-radius:50%;cursor:pointer;display:flex;height:4rem;justify-content:center;pointer-events:none;position:absolute;width:4rem}.attachment{display:flex}.attachment:last-of-type{margin-bottom:-.45rem}#info-caption-panel{background-color:#b0bec5}#info-caption-panel a{color:#757575;line-height:1;text-decoration:none}#info-caption-panel :hover{color:#000}#info-title{margin-right:auto}#group-manager{display:flex;flex-direction:column;height:100%;overflow-y:hidden;position:relative}#group-manager-buttons{background-color:#eceff1;border-left:1px solid #ccc;box-shadow:0 -1px #0000001a;display:flex;height:4rem;justify-content:space-around;padding:.4rem;z-index:2}ul.tabbar{background:#eee;display:flex;flex-grow:0;flex-shrink:0;list-style:none;margin:0;overflow:hidden;padding:0 0 0 1rem;width:100%}ul.tabbar>li{box-shadow:inset 0 -2px 2.5px -1px #00000040;height:2.4rem;position:relative}ul.tabbar li.active{background:#fff}ul.tabbar li:after{background:#ccc;bottom:0;content:"";height:.1rem;left:0;position:absolute;width:100%}ul.tabbar li.active:after{animation:tabbar-border-expand .2s cubic-bezier(.4,0,.4,1) 0s alternate forwards;background:#2196f3;bottom:0;content:"";height:.15rem;left:0;position:absolute}@keyframes tabbar-border-expand{0%{opacity:0;width:0}to{opacity:1;width:100%}}ul.tabbar>li>a{color:#2196f399;font-size:1.3rem;font-variant:small-caps;font-weight:400;line-height:2.4rem;overflow:hidden;padding-left:1rem;padding-right:1rem;position:relative;text-align:center;text-decoration:none}ul.tabbar li.active>a{color:#2196f3}.unread{background-color:#0097a7;border-radius:50%;color:#fff;display:inline-block;font-size:.75rem;font-weight:bolder;height:1.35rem;line-height:1.45rem;margin-left:.5rem;text-align:center;vertical-align:middle;width:1.35rem}.material-icons.as-badge{color:#aaa;font-size:1.15rem;padding-left:.2rem}.badge{align-self:flex-start;background-color:#e8e8e8;border:1px solid silver;border-radius:.2rem;color:#666;display:inline-block;font-family:Roboto Mono,Courier,monospace,sans-serif;font-size:.8rem;margin:.05rem .1rem;padding:.05rem .25rem}.badge.green{background-color:#efe;border-color:#aca;color:#383}.badge.yellow{background-color:#ffe;border-color:#cc9;color:#660}.badge.blue{background-color:#eef;border-color:#aad;color:#339}.badge.red{background-color:#fee;border-color:#c99;color:#933}.badge.magenta{background-color:#fef;border-color:#c9c;color:#939}.lettertile{border-radius:50%;padding-bottom:100%;user-select:none;width:100%}.lettertile>div{align-items:center;border-radius:50%;bottom:0;color:#fafafacc;display:flex;flex-grow:0;flex-shrink:0;justify-content:center;left:0;line-height:1;padding-top:.1em;position:absolute;right:0;text-align:center;text-transform:uppercase;top:0}.lettertile:hover{color:#fff}.lettertile.disabled{opacity:.5}.lt-bg-0{background-color:#ef9a9a}.lt-bg-1{background-color:#90caf9}.lt-bg-2{background-color:#b0bec5}.lt-bg-3{background-color:#b39ddb}.lt-bg-4{background-color:#ffab91}.lt-bg-5{background-color:#a5d6a7}.lt-bg-6{background-color:#eee}.lt-bg-7{background-color:#e6ee9c}.lt-bg-8{background-color:#c5e1a5}.lt-bg-9{background-color:#f0e57d}.lt-bg-10{background-color:#f48fb1}.lt-bg-11{background-color:#9fa8da}.lt-bg-12{background-color:#ffe082}.lt-bg-13{background-color:#bcaaa4}.lt-bg-14{background-color:#80deea}.lt-bg-15{background-color:#ce93d8}.dk-bg-0{background-color:#c62828}.dk-bg-1{background-color:#ad1457}.dk-bg-2{background-color:#6a1b9a}.dk-bg-3{background-color:#4527a0}.dk-bg-4{background-color:#283593}.dk-bg-5{background-color:#1565c0}.dk-bg-6{background-color:#0277bd}.dk-bg-7{background-color:#00838f}.dk-bg-8{background-color:#00695c}.dk-bg-9{background-color:#2e7d32}.dk-bg-10{background-color:#558b2f}.dk-bg-11{background-color:#9e9d24}.dk-bg-12{background-color:#f9a825}.dk-bg-13{background-color:#ff8f00}.dk-bg-14{background-color:#ef6c00}.dk-bg-15{background-color:#d84315}.lt-fg-0{color:#ef9a9a!important}.lt-fg-1{color:#90caf9!important}.lt-fg-2{color:#b0bec5!important}.lt-fg-3{color:#b39ddb!important}.lt-fg-4{color:#ffab91!important}.lt-fg-5{color:#a5d6a7!important}.lt-fg-6{color:#eee!important}.lt-fg-7{color:#e6ee9c!important}.lt-fg-8{color:#c5e1a5!important}.lt-fg-9{color:#f0e57d!important}.lt-fg-10{color:#f48fb1!important}.lt-fg-11{color:#9fa8da!important}.lt-fg-12{color:#ffe082!important}.lt-fg-13{color:#bcaaa4!important}.lt-fg-14{color:#80deea!important}.lt-fg-15{color:#ce93d8!important}.dk-fg-0{color:#c62828!important}.dk-fg-1{color:#ad1457!important}.dk-fg-2{color:#6a1b9a!important}.dk-fg-3{color:#4527a0!important}.dk-fg-4{color:#283593!important}.dk-fg-5{color:#1565c0!important}.dk-fg-6{color:#0277bd!important}.dk-fg-7{color:#00838f!important}.dk-fg-8{color:#00695c!important}.dk-fg-9{color:#2e7d32!important}.dk-fg-10{color:#558b2f!important}.dk-fg-11{color:#9e9d24!important}.dk-fg-12{color:#f9a825!important}.dk-fg-13{color:#ff8f00!important}.dk-fg-14{color:#ef6c00!important}.dk-fg-15{color:#d84315!important}.material-icons{font-size:1.4rem;line-height:1;text-align:center;vertical-align:middle}.material-icons.small{font-size:1rem}.material-icons.medium{font-size:1.2rem}.material-icons.big{font-size:2.2rem}.material-icons.bigger{font-size:3rem}.material-icons.huge{font-size:4rem}.composed-material{display:inline-block;position:relative}.composed-material i:nth-child(2){font-size:85%;font-weight:700;left:39%;position:absolute;top:7%}#self-avatar .material-icons{font-size:2.4rem;line-height:3rem}.avatar-box .material-icons{border-radius:50%;color:#fafafacc;height:inherit;width:inherit}.chip-input{align-items:center;background:#0000;border-bottom:1px solid #ccc;box-shadow:0 1px 0 #0000001a;color:#666;cursor:text;display:flex;flex-grow:1;flex-wrap:wrap;font-size:1.1rem;font-weight:400;justify-content:flex-start;margin:.25rem 0;max-height:10rem;min-height:4rem;outline:none;overflow-y:scroll;text-align:center;transition:all .1s ease-in-out}.chip-input.focused{border-bottom:1px solid #2196f3;box-shadow:0 1px 0 #2196f340}.chip-input>input[type=text]{border:none;box-shadow:none;display:inline-block;flex:1 0 auto;min-width:4rem;width:auto}.chip,.chip-input>input[type=text]{height:1.8rem;line-height:1.8;margin:.125rem}.chip{align-items:center;background-color:#eee;border-radius:.9rem;display:flex;flex:0 0 auto;font-size:90%;max-width:12rem;overflow:hidden}.chip,.chip>span{text-overflow:ellipsis;white-space:nowrap}.chip>span{overflow-x:hidden}.chip.invalid{background-color:#fee;color:#c66}.chip>a{background-color:#aaa;border-radius:50%;color:#eee;display:inline-block;height:1.175rem;line-height:1.15;margin:.3rem;min-width:1.175rem;text-decoration:none;width:1.175rem}.chip>a:focus,.chip>a:hover{background-color:#bbb;color:#fff;text-decoration:none}.chip .avatar-box{font-size:1rem;height:1.8rem;margin-right:.2rem;min-height:1.8rem;min-width:1.8rem;position:relative;width:1.8rem}.chip .avatar-box .material-icons{font-size:1.4rem;line-height:1.8rem}.chip .spacer{display:inline-block;height:1.175rem;margin:.3rem;width:.25rem}.load-spinner-box{--size-small:2.5rem;--size-large:4rem;background-color:#fff;border-radius:50%;box-shadow:.1rem .1rem .15rem 1px #00000040;height:var(--size-small);left:0;margin-left:auto;margin-right:auto;padding:.25rem;position:absolute;right:0;top:5rem;width:var(--size-small);z-index:2}.load-spinner-box.large{height:var(--size-large);width:var(--size-large)}.load-spinner-box.clear{background-color:#fff8;-x-box-shadow:none;box-shadow:.1rem .1rem .15rem 1px #0000001f}.load-spinner-box.centered{top:calc(var(--size-small)/2 - .25rem)}.load-spinner-box.large.centered{top:calc(var(--size-large)/2 - .25rem)}.loader-spinner{animation:spin 1.5s linear infinite;border:.35rem solid #f3f3f3;border-radius:50%;border-top-color:#1e88e5;height:2rem;width:2rem}.load-spinner-box.large .loader-spinner{height:3.5rem;width:3.5rem}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.uploader{display:flex}.uploader>div{background-color:#cfd8dc;border-radius:.1rem;height:.6rem;line-height:0;margin:.35rem;padding:.15rem;position:relative;width:6rem}.uploader>div>span{background-color:#1e88e5;border-radius:.08rem;display:inline-block;height:.3rem}.highlight{color:teal;font-weight:bolder}.highlight .preview{color:#5f9ea0;font-weight:inherit}.cropper{align-self:center;height:21rem;margin:1rem auto auto;transform:translateZ(0)}.cropper,.cropper .bounding-box{border-radius:.25rem;position:relative;width:18rem}.cropper .bounding-box{background-color:#999;border:2px solid #1e88e5;box-shadow:1px 1px 3px grey;height:18rem;overflow:hidden;z-index:1}.cropper .preview{max-height:none;max-width:none;position:absolute;z-index:-1}.cropper .cutout{border:1px dashed #999;bottom:0;box-shadow:0 0 150rem 150rem #e6f6ffa8;height:17rem;left:0;margin:auto;position:absolute;right:0;top:0;width:17rem;z-index:0}.cropper .cutout.circle{border-radius:50%}.cropper .overlay{cursor:move;position:absolute;touch-action:none;z-index:1}.cropper .zoom-wrapper{height:2rem;margin:1rem auto;text-align:center;width:80%}.cropper .zoom{box-shadow:none;pointer-events:none;z-index:2}.cropper .zoom:focus{outline:none}.reply-quote{border-left:.25rem solid #039be5;border-radius:.25rem;color:#666;cursor:pointer;font-weight:lighter;line-height:1.5em;padding:.15rem .5rem .25rem;position:relative}.chat-box .bubble .reply-quote{margin:.15rem -.75rem .3rem}.chat-box .bubble .reply-quote:first-child{margin-top:-.25rem}.chat-box .left .reply-quote{background-color:#fff6}.chat-box .right .reply-quote{background-color:#8080801f}.reply-quote .inline-image{margin-bottom:.1rem;vertical-align:middle}.reply-quote img.inline-image:before{content:""}#reply-quote-preview{align-items:center;display:flex}#reply-quote-preview .reply-quote{background-color:#fafafa;cursor:auto;margin:.35rem .5rem 0 0;width:100%}#reply-quote-preview .cancel{min-width:2.4rem;text-align:center}#reply-quote-preview .inline-image{background-color:#ccc}#reply-quote-preview img.inline-image:before{content:""}#send-message-panel #reply-quote-preview{width:100%}#send-message-panel #reply-quote-preview .reply-quote{margin-bottom:.35rem}.forwarding-quote{font-size:200%;line-height:200%}.mention{font-weight:500}.forward-dialog{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;height:80%;margin:auto 1rem;overflow:hidden;padding:.5rem;position:relative;width:24rem}.forward-dialog .scrollable-panel{height:80%}.audio{align-items:center;background-color:#eee;border-radius:1.5rem;display:flex;flex:none;height:3rem;margin-left:auto}.audio canvas{height:2.6rem;margin-right:2rem;width:14rem}.audio canvas.playback{margin-right:0}.audio .duration{font-size:large}.audio-player{align-items:center;display:flex}.audio-player .material-icons.large{color:#667575cc;font-size:2.4rem}.audio-player .material-icons.disabled{color:#999c;cursor:default}.audio-player .playback{height:2.6rem;width:14rem}.audio-player canvas.playback{display:block}.audio-player div.playback{align-items:center;color:#757575cc;display:flex;font-size:small;justify-content:center}.audio-player .timer{color:#666c;font-size:smaller}#pinned-wrapper{margin-right:1rem}#pinned-wrapper,.pinned{align-items:center;display:flex;height:100%;position:relative}.pinned{background-color:#fafafa;background-position:50%;border-radius:0 .25rem .25rem 0;color:#666;cursor:pointer;font-weight:lighter;min-width:0;padding:.15rem .5rem .25rem;transition:background .4s;width:24rem}.pinned>p{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.pinned:hover{background-color:#fafdff}.pinned:active{background-color:#d8e8ff;transition:background 0s}.pinned-scroll{align-items:center;background-color:#eeee;border-radius:.25rem 0 0 .25rem;display:flex;flex-direction:column;height:100%;justify-content:space-evenly;margin-left:.35rem;padding:.1rem;position:relative}.pinned-scroll .dot{background-color:#999;border-radius:50%;height:.3rem;width:.3rem}.pinned-scroll .adot{background-color:#039be5;border-radius:50%;height:.45rem;width:.45rem}#pinned-wrapper .menuTrigger.upper{top:.25rem}#pinned-wrapper .menuTrigger.lower{bottom:.25rem}#pinned-wrapper:hover .menuTrigger{opacity:.9}#pinned-wrapper:hover .menuTrigger a{color:#757575}.drag-n-drop{align-items:center;background-color:#fffffff2;border:.15rem dashed grey;border-radius:1rem;bottom:1.5rem;color:grey;display:flex;font-size:2rem;justify-content:center;left:1.5rem;outline:.5rem solid #fffffff2;position:absolute;right:1.5rem;top:1.5rem;z-index:3}.country-flag{display:inline-block;font-size:larger}.dial-code{cursor:pointer;display:inline-block;margin-bottom:.15rem;vertical-align:middle;white-space:nowrap}ul.phone-country-selector{list-style:none;overflow-x:hidden}ul.phone-country-selector>li{cursor:pointer;font-weight:lighter;padding:.25rem .5rem .15rem;position:relative;width:100%}ul.phone-country-selector>li:hover{background-color:#eceff1}ul.phone-country-selector>li.selected{background-color:#cfd8dc}ul.phone-country-selector .dial-code{color:#1e88e5;font-weight:400;position:absolute;right:.5rem}ul.phone-country-selector span{display:inline-block}ul.phone-country-selector span.country{overflow-x:hidden;text-overflow:ellipsis}@media (min-width:1440px){#app-container{max-width:114rem}}@media (max-width:960px){#mountPoint{padding:0}.pinned{width:16rem}}@media (max-width:640px){#app-container{border-radius:0;box-shadow:none;width:100%}#info-view,#sidepanel{width:100%}.forward-dialog{border-radius:.3rem .3rem 0 0;margin:auto 0 0;width:100%}.menuTrigger{opacity:.3}#send-message-input{transition-property:-tinode-mobile}#pinned-wrapper{height:3.5rem;margin:.25rem}.pinned{width:unset}} \ No newline at end of file +*{box-sizing:border-box;font-family:Roboto,Arial,sans-serif;margin:0;padding:0;scrollbar-color:#0000004d #ffffff1a;scrollbar-width:thin;text-rendering:optimizeLegibility}body,html{background-color:silver;box-shadow:inset 0 10rem #3949ab;color:#666;font-size:10pt;height:100%;overflow:hidden;width:100%}tt{font-family:Roboto Mono,Courier,monospace}a{color:#2196f3;text-decoration:none}a:focus,a:hover{color:#0a6ebd;text-decoration:underline}a.danger{color:#d32f2f}a.danger:focus,a.danger:hover{color:#f44336}a.gray:focus,a.gray:hover{color:#999}form{line-height:1.5;padding:.5rem .75rem}form,label{font-size:1.1rem}label{margin-right:auto}label.small{color:#1e88e5;font-size:.95rem}label.large{color:#1e88e5;font-size:1.1rem}img.avatar{border-radius:50%;height:100%;width:100%}button{border:1px solid #0000;border-radius:4px;box-shadow:0 1px 3px 0 #00000040;cursor:pointer;display:inline-block;font-family:inherit;font-size:1.05rem;font-weight:400;height:2.2rem;line-height:2.2rem;margin:0 .35rem;min-width:5rem;padding-left:1.2rem;padding-right:1.2rem;text-align:center;text-transform:uppercase;touch-action:manipulation;vertical-align:middle;white-space:nowrap}button[disabled],html input[disabled]{box-shadow:none;cursor:default;filter:grayscale(50%);opacity:.65}button.round{border-radius:50%;box-shadow:0 2px .5rem 0 #00000040;height:3rem;line-height:3.5rem;margin:0;outline:none;padding:0;width:3rem;z-index:2}button.round.small{box-shadow:0 2px .35rem 0 #00000040;height:2rem;line-height:2rem;width:2rem}button:active:not([disabled]),button:hover:not([disabled]){color:#fff}button:active{box-shadow:inset 0 3px .5rem rgba(0,0,0,.125)}button:active.round.small{box-shadow:inset 0 2px .35rem rgba(0,0,0,.125)}button.primary{background-color:#1e88e5;color:#fff}button.primary:active:not([disabled]),button.primary:hover:not([disabled]){background-color:#2962ff}button.secondary{background-color:#fff;color:#2196f3}button.secondary:active:not([disabled]),button.secondary:hover:not([disabled]){background-color:#e3f2fd}button.outline{background-color:initial;border:1px solid silver;box-shadow:none;color:#2196f3}button.outline:active:not([disabled]),button.outline:hover:not([disabled]){background-color:#e8eaf6}button.danger{background-color:#ec6560;color:#fff}button.danger:active:not([disabled]),button.danger:hover:not([disabled]){background-color:#f77}button.positive{background-color:#71c46f;color:#fff}button.positive:active:not([disabled]),button.positive:hover:not([disabled]){background-color:#7fed7f}input,textarea{background:#0000;border:none;color:#666;font-family:inherit;font-size:1.1rem;margin:.25rem 0;outline:none;padding:0 .125rem}input[type=email],input[type=password],input[type=search],input[type=tel],input[type=text],textarea{border-bottom:1px solid #ccc;box-shadow:0 1px 0 #0000001a;height:2.5rem;transition:all .1s ease-in-out,height 0s}input[type=email],input[type=password],input[type=tel],input[type=text],textarea{width:100%}input[type=email]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,textarea:focus{border-bottom:1px solid #2196f3;box-shadow:0 1px 0 #2196f340}input[type=email].invalid,input[type=password].invalid,input[type=search].invalid,input[type=tel].invalid,input[type=text].invalid,textarea.invalid{border-bottom:1px solid #e57373;box-shadow:0 1px 0 #e5737340}input[type=email]::placeholder,input[type=password]::placeholder,input[type=search]::placeholder,input[type=tel]::placeholder,input[type=text]::placeholder,textarea::placeholder{color:#bbb;font-weight:lighter}input[type=password].with-visibility,input[type=text].with-visibility{margin-right:-2rem;padding-right:2rem}textarea{padding-top:.45rem;resize:none}textarea.inplace-edit{font-size:unset;height:unset}::-webkit-scrollbar{width:.6rem}::-webkit-scrollbar-track{background-color:#ffffff1a;-webkit-box-shadow:inset 0 0 6px #0003;box-shadow:inset 0 0 6px #0003}::-webkit-scrollbar-thumb{background-color:#0000004d}ul.menu{background-color:#fff;border-radius:.2rem;box-shadow:0 1px 6px #00000073;list-style:none;padding-bottom:.35rem;padding-top:.35rem;position:absolute;width:12rem;z-index:4}ul.menu>li{cursor:pointer;padding:.75rem .75rem .75rem 1.25rem}ul.menu>li:hover{background-color:#f5f5f5}ul.menu>li.separator{height:1px;margin:2px;padding:0}ul.menu>li.separator,ul.menu>li.separator:hover{background-color:#ccc}ul.menu>li.disabled{opacity:.6;pointer-events:none}.menuTrigger{background-color:inherit;border-radius:.7rem;height:1.4rem;line-height:1.4rem;opacity:0;position:absolute;right:.25rem;text-align:center;width:1.4rem}.menuTrigger a{color:#757575}li .menuTrigger{top:.25rem}li:hover .menuTrigger{opacity:.9}li:hover .menuTrigger a{color:#757575}div.alert-container{align-items:center;background:#f0f0f0d9;display:flex;height:100%;justify-content:center;overflow:hidden;padding:1rem;position:absolute;width:100%;z-index:3}div.alert{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;height:min-content;margin:auto 2rem;max-height:40rem;max-width:24rem;min-width:16rem;overflow:hidden;padding:1rem;position:relative;width:fit-content}div.alert-container .title{color:#2196f3;font-size:1.2rem;font-weight:500;padding:0 0 .35rem;position:relative;width:100%}div.alert-container .title.with-control{align-items:center;display:flex;flex:none;justify-content:space-between;padding:.5rem .5rem 0;position:relative}div.alert-container .content{padding:.5rem 0;position:relative}div.trusted-badge{margin:.25rem 0}.blue{color:#2196f3!important}.lt-blue{color:#64b5f6!important}.verified-color{color:#03a9f4!important}.staff-color{color:#689f38!important}.danger-color{color:#ffc107!important}.green{color:#388e3c}.white{color:#fff!important}.red{color:#d32f2f!important}.badge-inv{color:#fffd!important}.gray{color:#757575!important}.light-gray{color:#bbb!important}.large{font-size:120%}.small{font-size:90%}.float-right{position:absolute;right:.5rem}.hr{background-color:#f0f0f0;box-shadow:inset 0 .25rem .25rem -.25rem #00000026,inset 0 -.25rem .25rem -.25rem #00000026;display:block;height:.45rem;min-height:.3rem;width:100%}.hr.thin{height:.015rem}.ellipsized{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.flat-button{align-items:center;background-color:#fff;display:flex;font-weight:400;height:2em;line-height:2em;margin:.15rem .35rem;text-align:center;text-transform:uppercase;touch-action:manipulation;vertical-align:middle;white-space:nowrap;z-index:1}.flat-button:focus,.flat-button:hover{text-decoration:none}.flat-button.faux{background-color:#eee;border:1px solid #999;border-radius:3px;display:inline-block;height:1.4em;line-height:1.4em;margin:.15rem;padding:0 .35rem;text-transform:none}.action-button{background-color:#fafafa;border-radius:50%;bottom:1rem;box-shadow:0 2px .5rem 0 #00000040;color:#999;display:block;font-size:1.8rem;height:3.25rem;margin:0;min-height:unset;min-width:unset;outline:none;padding:0;position:absolute;right:1rem;transition:all .15s;width:3.25rem;z-index:2}.action-button:focus,.action-button:hover{background-color:#f0f0f0;color:#888}.action-button:active{background-color:#eee;color:#777}.action-button.hidden{bottom:2.625rem;font-size:1px;height:1px;line-height:1px;opacity:0;right:2.625rem;width:1px}.group-focus{outline:none}#mountPoint{height:100%;padding-bottom:1rem;padding-top:1rem}#noscript-message{background-color:#fff;border-radius:.25rem;box-shadow:1px 2px .3rem gray;display:flex;flex-direction:column;margin-left:auto;margin-right:auto;margin-top:5rem;max-width:30rem;overflow:hidden;position:relative}#noscript-title{background-color:#2196f3;color:#bbdefb;font-size:120%;font-weight:bolder;padding:1rem}#noscript-content{padding:1rem}#app-container{background-color:#fff;border-radius:.25rem;box-shadow:1px 2px .3rem gray;display:flex;height:100%;margin-left:auto;margin-right:auto;max-width:84rem;overflow:hidden;position:relative}#sidepanel{flex-grow:0;flex-shrink:0.3;min-width:18rem;width:24rem}#sidepanel,#topic-view{display:flex;flex-direction:column;height:100%;position:relative}#topic-view{overflow-x:hidden;width:100%}#info-view{display:flex;flex-direction:column;flex-grow:0;flex-shrink:0.3;height:100%;min-width:18rem;width:24rem}.center-medium-text{align-items:center;display:flex;font-size:1.4rem;height:100%;justify-content:center;text-align:center;width:100%}.nodisplay{display:none!important}.quoted{margin-left:1rem}.space-right{margin-right:.25rem}.rounded-container{background-color:#fafafa;border-radius:.1rem;margin:.25rem;padding:.25rem;position:relative}.in-place-edit{border-bottom:1px dashed #ddd;cursor:pointer;display:inline-block;font-family:inherit;font-size:1rem;margin:.25rem .125rem 0;vertical-align:initial;width:100%}.in-place-edit.short{font-size:1.1rem;height:2.5rem;line-height:2.25}.in-place-edit.disabled{border-bottom:none;color:#888;cursor:default}.in-place-edit.placeholder{color:#bbb}.in-place-edit span{display:inline-block}.in-place-edit.short span{display:block;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}table.permission-editor{width:100%}table.permission-editor td{font-size:1.1rem;margin:.35rem 0;vertical-align:middle}table.permission-editor td.checkbox{color:#999;text-align:center}.permission-editor .material-icons{font-size:1.3rem;line-height:1.3rem}.material-icons.clickable{border-bottom:none;cursor:pointer}.clickable{border-bottom:1px dashed #ddd}.clean-clickable,.clickable,.image-clickable{cursor:pointer}.panel-title{font-size:1.4rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.panel-title.deleted{text-decoration:line-through}.caption-panel{box-shadow:0 1px 2px gray;flex:none;height:4rem;overflow-x:hidden;white-space:nowrap;z-index:1}.caption-panel,.dialog-buttons{align-items:center;display:flex;padding:.5rem}.dialog-buttons{justify-content:flex-end}.dialog-buttons button{margin:0 .5rem}.avatar-box{flex-grow:0;flex-shrink:0;position:relative}.bot-form{display:flex;flex-direction:column;line-height:2rem;min-width:8rem;overflow:hidden}.bot-form div{min-height:2rem;vertical-align:middle}.bot-form button{background-color:#eef;color:#2196f3;display:inline-block;font-size:inherit;height:2rem;line-height:2rem;margin:.15rem;text-transform:none}.bot-form>div>button{width:auto}.bot-form button:active,.bot-form button:focus,.bot-form button:hover{background-color:#e3f2fd;outline:0}#side-caption-panel{background-color:#2196f3;color:#bbdefb}#side-caption-panel a{color:#bbdefb;line-height:1;padding-left:.5rem}#side-caption-panel :hover{color:#fff}#sidepanel-title{margin-left:.5rem;margin-right:auto}#self-avatar{border-radius:50%;flex:none;height:3rem;min-width:3rem;width:3rem}#self-avatar.avatar-box{font-size:1.8rem}#settings-form ul{list-style:none}#settings-form li{vertical-align:middle}#settings-form label{display:inline-block;padding-left:.5rem}input#host-name{width:100%}input.search{text-indent:1.4rem}i.search{left:.6rem;position:absolute}.info-box{display:none;overflow:hidden;padding:1rem 1.5rem .75rem .5rem;position:relative}.info-box>span{line-height:1.35rem}.info-box .cancel{position:absolute;right:.25rem;top:.5rem}.info-box.error{background-color:#ffcdd2;border-bottom:1px solid #ffbfc8;color:#b71c1c;display:flex}.info-box.warning{background-color:#fff9c4;border-bottom:1px solid #fff59d;color:#6d4c41;display:flex}.info-box.info{background-color:#e1f5fe;border-bottom:1px solid #b3e5fc;color:#0288d1;display:flex}.info-box .cancel .material-icons{color:#757575;font-size:1rem}.info-box.error .cancel .material-icons{color:#b71c1c}.info-box .icon{display:block;margin-right:.5rem}.info-box .icon .material-icons{font-size:1.5rem}.info-box.warning .icon .material-icons{color:#fbc02d}.info-box.error .icon .material-icons{color:#ef5350}.info-box.info .icon .material-icons{color:#0288d1}.flex-column{display:flex;flex-direction:column;height:100%;overflow-x:hidden}.flex-column.narrow{height:auto}.scrollable-panel{height:100%;margin-bottom:auto;margin-top:.25rem;overflow-x:hidden;overflow-y:auto;scroll-behavior:smooth}.contact-box{list-style:none;overflow-x:hidden}.contact-box>li{background:linear-gradient(90deg,#0000 3.5rem,#eee 1px,#eee) 0 100% /100% 1px no-repeat,#fff;cursor:pointer;display:flex;flex-grow:0;flex-shrink:0;min-height:3.75rem;overflow-x:hidden;padding:.35rem .25rem .5rem .35rem;position:relative}.contact-box.small>li{min-height:2.5rem;padding:.25rem .25rem .35rem .35rem}.contact-box>li:last-child{background:#fff}.contact-box>li:hover{background-color:#eceff1}.contact-box>li.selected{background-color:#cfd8dc}.contact-box>li.action{border-bottom:none;min-height:2.5rem}.contact-box .text-box{overflow-x:hidden}.contact-box .contact-title{font-size:1.05rem;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-box .contact-title.deleted{opacity:.66;text-decoration:line-through;text-decoration-thickness:.5px}.contact-box .contact-comment{font-weight:lighter}.contact-box .contact-comment,.contact-box .contact-comment>span{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-box .contact-comment b{font-weight:bolder}.contact-box .contact-comment .material-icons{color:#777;margin-bottom:.2rem}.contact-box .avatar-box{font-size:1.8rem;height:3rem;margin-right:.35rem;min-height:3rem;min-width:3rem;overflow:hidden;padding:0;position:relative;width:3rem}.contact-box .avatar{border-radius:50%;height:3rem;width:3rem}.contact-box.small .avatar-box{font-size:1.2rem;height:2rem;min-height:2rem;min-width:2rem;width:2rem}.contact-box.small .avatar{height:2rem;width:2rem}.avatar.deleted{filter:grayscale(1);opacity:.75}.contact-box .avatar-box>.material-icons{display:flex;flex-direction:column;font-size:2.4rem;justify-content:space-around;line-height:3rem}.contact-box.small .avatar-box>.material-icons{font-size:1.6rem;line-height:2rem}.contact-box .text-box{display:flex;flex-direction:column;justify-content:space-around}.contact-box .text-box>div{align-items:center;display:flex;flex-direction:row}.contact-box .action-text{cursor:pointer;font-size:.95rem;line-height:2.5rem;margin:0 auto;text-align:center}img.channel{height:1.25rem;margin-left:.3rem;vertical-align:middle;width:1.25rem}.large img.channel{height:1.45rem;margin-bottom:.15rem;margin-left:.5rem;width:1.45rem}.avatar-box .online,.offline{border-radius:50%;bottom:.1rem;flex-shrink:0;height:.75rem;min-height:.75rem;min-width:.75rem;position:absolute;right:0;width:.75rem}.avatar-box .online{background-color:#4caf50}.avatar-box .offline{background-color:silver}.online.typing{animation:typing .5s infinite}@keyframes typing{0%,to{transform:translateX(0)}50%{transform:translateX(-.3rem)}}.contact-box .checkmark.material-icons{border:0 solid #0000;color:#0097a7;overflow:visible;padding-left:.5px}.contact-box .checkmark.material-icons,.contact-box .deleted.material-icons{background-color:#fff;bottom:0;font-size:1.5rem;height:1.5rem;position:absolute;right:0;width:1.5rem}.contact-box .deleted.material-icons{color:#999}#add-topic{bottom:1rem;position:absolute;right:1rem}.panel-form{display:flex;flex-direction:column;padding:0;position:relative}.panel-form-row{align-items:center;flex-shrink:0}.panel-form-column,.panel-form-row{display:flex;flex-grow:0;line-height:1.5;padding:.5rem .75rem;position:relative}.panel-form-column{flex-direction:column;overflow-x:hidden}.panel-form-column>.panel-form-column,.panel-form-row>.panel-form-column{flex-grow:1;padding:0!important}.panel-form-column>.panel-form-row,.panel-form-row>.panel-form-row{padding:0!important}.panel-form-column>.group{margin-bottom:.25rem;margin-top:.25rem}.avatar-upload{display:flex;flex-direction:column;flex-shrink:0;font-size:4.5rem;height:10.5rem;padding:0;position:relative;width:10rem}.avatar-upload.read-only{height:auto}.avatar-upload .clear-avatar{background-image:radial-gradient(#fff 40%,#0000 75%);font-size:1rem;opacity:0;position:absolute;right:0;top:0}.avatar-upload:hover .clear-avatar{opacity:1}.avatar-upload .avatar-box{height:10rem;position:relative;width:10rem}.avatar-upload .avatar-box>.material-icons{font-size:4rem;line-height:10rem;position:relative;vertical-align:middle}.avatar-upload:after{clear:both;content:".";display:block;height:0;visibility:hidden}.avatar-upload .blank{border:1px dashed #999;color:#999;flex-shrink:0;font-size:1rem;line-height:10rem;text-align:center;vertical-align:middle}.avatar-upload .blank,.avatar-upload img.preview{border-radius:50%;height:10rem;overflow:hidden;width:10rem}.avatar-upload img.preview:before{background:50% no-repeat url(../img/broken_image.png);border-radius:50%;color:#666;content:" ";display:flex;font-size:.9rem;font-weight:lighter;height:10rem;justify-content:center;left:0;position:absolute;top:0;width:10rem}.avatar-upload .inputfile.hidden{height:.1px;opacity:0;overflow:hidden;position:absolute;width:.1px}.avatar-upload .inputfile+label.round{align-items:center;background-color:#fafafa;border-radius:50%;bottom:0;box-shadow:0 1px 4px #0006;cursor:pointer;display:flex;height:3rem;justify-content:center;line-height:3rem;position:absolute;right:0;width:3rem;z-index:1}.avatar-upload .inputfile:focus+label.round{outline:none}.avatar-upload .inputfile+label:hover,.avatar-upload .inputfile:focus+label{background-color:#e3f2fd;color:#42a5f5}#topic-caption-panel{background-color:#b0bec5}#topic-caption-panel a{color:#757575;text-decoration:none}#topic-caption-panel :hover{color:#000}#topic-caption-panel .avatar-box{font-size:1.8rem;height:3rem;margin-right:.35rem;min-width:3rem;position:relative;width:3rem}#topic-caption-panel .avatar-box>.material-icons{font-size:2.4rem;line-height:3rem}#topic-title-group{overflow-x:hidden}#hide-message-view{display:block;margin-right:.35rem}#topic-users{margin-left:auto;margin-right:1rem;width:min-content}#topic-users .avatar-box{display:inline-block;font-size:1rem;height:1.8rem;margin-right:.2rem;min-height:1.8rem;min-width:1.8rem;width:1.8rem}#topic-users .avatar-box>.material-icons{font-size:1.4rem;line-height:1.8rem}#topic-users>span{line-height:2rem;vertical-align:top}#messages-panel{background-color:#eceff1;background-image:url(../img/bg_messages.png);height:100%;overflow-y:scroll;padding:.75rem;position:relative}#messages-container{height:100%;overflow-y:hidden;position:relative}#write-only-background{background-image:repeating-linear-gradient(45deg,#ffffff80,#ffffff80 .5rem,#fffc 0,#fffc 1rem);height:100%;left:0;position:absolute;top:0;width:100%}#write-only-note{height:auto;top:50%;transform:translate(-50%,-50%);width:auto}#peer-messaging-disabled-note,#write-only-note{background-color:#fff;border-radius:.5rem;box-shadow:0 1px .5px #0000001f;color:#666;left:50%;line-height:2rem;padding:.75rem 1.25rem;position:absolute;text-align:center}#peer-messaging-disabled-note{bottom:4.3rem;transform:translate(-50%);width:fit-content}#send-message-panel{align-items:center;background-color:#eceff1;display:flex;flex:none;min-height:4rem;padding:.4rem .4rem .4rem .6rem;z-index:2}#send-message-panel #writing-disabled{font-size:1.2rem;text-align:center;width:100%}#send-message-wrapper{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;z-index:2}#sendMessage{overflow:hidden;overflow-wrap:break-word}#send-message-panel .material-icons{font-size:1.6rem;min-width:2rem}#send-message-panel .material-icons.secondary,.material-icons.disabled{color:#999!important}#send-message-panel .material-icons.secondary:hover{color:#2196f3!important}#send-message-input{max-height:6rem;min-height:2.5rem;overflow:hidden auto}.accept-invite-panel{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;width:100%;z-index:1}.accept-invite-panel .title{font-size:1.2rem;padding:.75rem}.accept-invite-panel .footer{background-color:#fafafa;padding-bottom:.35rem;text-align:center;white-space:nowrap}.accept-invite-panel button{border:1px solid #0000;box-shadow:0 1px 2px 0 #00000040;font-size:1rem;height:2em;line-height:2em;margin:.25rem}#dummy-view{align-items:center;background-color:#e0f7fa;display:flex;height:100%;justify-content:center;width:100%}#dummy-view a{display:block;text-align:center;text-decoration:none}#image-preview{background-color:#ffffffe6;display:flex;flex-direction:column;height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}#image-preview-caption-panel{background-color:#eceff1;box-shadow:0 1px 2px silver;display:flex;height:4.6rem;justify-content:space-between;line-height:2.5rem;padding:1rem .75rem 0;z-index:3}#image-preview-caption-panel a{color:#757575;text-decoration:none}#image-preview-caption-panel a:hover{color:#000}#image-preview-container{align-items:center;display:flex;height:100%;justify-content:space-around;overflow:hidden;width:100%}#image-preview-footer{background-color:#eceff1;box-shadow:0 -2px 3px -1px #0000001a;display:flex;height:3.2rem;justify-content:space-around;line-height:1.5rem;z-index:1}#image-preview-container .material-icons{font-size:6rem}#video-container{background-color:#ffffffe6;flex-direction:column;height:100%;justify-content:space-between;left:0;top:0}#video-container,.controls{display:flex;position:absolute;width:100%}.controls{align-items:center;bottom:2.5rem;column-gap:1rem;justify-content:center;z-index:1}.incoming-call .controls{bottom:1rem}.controls button{border-radius:50%;box-shadow:0 3px 6px 0 #00000040;height:4rem;min-width:auto;width:4rem}#video-container-panel{height:100%;position:relative;width:100%}.call-party{background-color:#eee;border:2px solid #ccc;border-radius:.5rem;overflow:hidden}.call-party:hover{border:2px solid #2196f3}.call-party.peer{bottom:1rem;left:1rem;position:absolute;right:1rem;top:1rem;z-index:1}.call-party.self{bottom:.5rem;height:10rem;position:absolute;right:.5rem;width:10rem;z-index:2}.call-party.self[disabled]{display:none}video{height:100%;object-fit:contain;width:100%}.call-party.self video{transform:scaleX(-1)}.call-party[disabled] video{display:none}.caller-card{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}.caller-card .avatar-box{font-size:6rem;height:10rem;width:10rem}.caller-card.pulse .lettertile,.caller-card.pulse img.avatar{animation:active-call 1.5s linear infinite}.caller-card .avatar-box .material-icons{font-size:6rem;line-height:1.6}.call-party .caller-name{background-color:#ffffffb3;border-radius:.35rem;padding:.15rem .5rem;text-shadow:0 0 1px #fff;z-index:1}.call-party.self .caller-name.inactive{bottom:.25rem;position:absolute;right:.25rem}.caller-card .caller-name{font-size:larger;margin-top:.5rem}.caller-card .trusted-badge{display:inline-block}.call-party.peer .caller-name.inactive{bottom:.5rem;font-size:larger;left:1rem;margin:0 auto;position:absolute}.incoming-call{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;margin:auto 2rem;max-width:24rem;min-width:16rem;position:relative;width:fit-content}.incoming-call .caller-card{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;left:auto;padding:4rem 3.5rem 5.5rem;position:relative;top:auto;width:100%}.incoming-call .caller-name{background-color:#ffffffb3;border-radius:.35rem;padding:.15rem .5rem;text-shadow:0 0 1px #fff;z-index:1}.call-message{display:flex;flex-direction:row}.call-message .duration{font-weight:lighter}@keyframes active-call{0%{box-shadow:0 0 0 .1rem #2196f34d,0 0 0 .3rem #2196f34d,0 0 0 .5rem #2196f34d}to{box-shadow:0 0 0 .4rem #2196f34d,0 0 0 2rem #2196f300,0 0 0 3rem #2196f300}}.chat-box{list-style:none;margin:0 .5rem;padding:0 0 3.5rem;scroll-behavior:smooth}.chat-box.group{margin-left:-.4rem}.chat-box li{align-items:flex-end;display:flex;margin:0;padding:0}.chat-box .right{justify-content:flex-end}.bubble{border-radius:.5rem;color:#212121;display:flex;filter:drop-shadow(.5px .5px .7px rgb(0,0,0,.3));height:auto;margin:0;max-width:36rem;min-width:9rem;padding:.75rem .5rem;position:relative;text-align:left;transition:background-color .5s ease-out;transition-delay:.3s}.chat-box li.single{margin-bottom:.75rem;margin-top:.25rem}.chat-box li.first{margin-bottom:.15rem;margin-top:.25rem}.chat-box li.middle{margin-bottom:.15rem;margin-top:.15rem}.chat-box li.last{margin-bottom:.75rem;margin-top:.15rem}.left .bubble{background-color:#c5e1a5;margin-right:.5rem}.left.flash .bubble{background-color:#94a97c}.left .bubble.tip{border-bottom-left-radius:0}.right .bubble{background-color:#fafafa;margin-left:1.75rem}.right.flash .bubble{background-color:#ccc}.right .bubble.tip{border-bottom-right-radius:0}.chat-box .meta{justify-content:center;margin-bottom:.5rem;margin-top:.5rem}.chat-box .meta .bubble{color:#666;min-width:unset}.meta .bubble.deleted{background-color:#e3f2fd;padding:.75rem .75rem .5rem}.meta .bubble.date{background-color:#fffc;font-size:smaller;padding:.5rem;text-transform:uppercase}.left .bubble.tip:before{background-color:#c5e1a5;border-bottom-left-radius:.3rem;bottom:0;clip-path:polygon(100% 0,0 110%,100% 110%);-webkit-clip-path:polygon(100% 0,0 110%,100% 110%);content:"";height:.85rem;left:-.8rem;position:absolute;transition:background-color .5s ease-out;transition-delay:.3s;width:.85rem}.left.flash .bubble.tip:before{background-color:#94a97c}.right .bubble.tip:before{background-color:#fafafa;border-bottom-right-radius:.3rem;bottom:0;clip-path:polygon(0 0,100% 110%,0 110%);-webkit-clip-path:polygon(0 0,100% 110%,0 110%);content:"";height:.85rem;left:99.8%;position:absolute;transition:background-color .5s ease-out;transition-delay:.3s;width:.85rem}.right.flash .bubble.tip:before{background-color:#ccc}.chat-box .avatar-box{font-size:.8rem;height:1.4rem;margin-bottom:.8rem;margin-right:.8rem;min-height:1.4rem;min-width:1.4rem;position:relative;width:1.4rem}.bubble div.content-meta{width:100%}.bubble div.message-content{margin:0;max-width:36rem;overflow-wrap:anywhere;padding:0 .75rem;position:relative;width:100%}.bubble div.message-content:after{clear:both;content:".";display:block;height:0;visibility:hidden}.chat-box .timestamp-padding{display:inline-block;height:0;width:4rem}.chat-box .timestamp{float:right;position:relative;top:.35rem;user-select:none;white-space:nowrap}.chat-box .author,.chat-box .timestamp{color:#777;font-size:80%}.image-preview,.inline-image{border-radius:.1rem;overflow:hidden;position:relative}.inline-image{max-height:24rem;max-width:36rem}.inline-image>.rounded-container{border-left:.25rem solid #0000;border-right:.25rem solid #0000;bottom:2rem;margin:.25rem auto;position:relative}img.image-preview:before,img.inline-image:before{background:50% no-repeat #ccc url(../img/broken_image.png);color:#666;display:flex;font-size:.9rem;font-weight:lighter;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}img.inline-image:before{background-size:3rem 3rem;content:attr(alt)}img.image-preview:before{background-size:10rem 10rem;content:" "}.inline-image.video>img.inline-image:before,img.image-preview.video:before{background:50% no-repeat #ccc url(../img/broken_video.png)}.inline-image.video>img.inline-image:before{background-size:3rem 3rem}.inline-video{align-items:center;display:flex;justify-content:center;position:relative}.inline-video>img.inline-image:before{background:#ccc}.inline-video .duration{background-color:#ffffff80;border-radius:.3rem;bottom:.25rem;color:#333;font-size:80%;left:.25rem;padding:.25rem .5rem;position:absolute}.inline-video .play-control{align-items:center;background-color:#0006;border-radius:50%;cursor:pointer;display:flex;height:4rem;justify-content:center;pointer-events:none;position:absolute;width:4rem}.attachment{display:flex}.attachment:last-of-type{margin-bottom:-.45rem}#info-caption-panel{background-color:#b0bec5}#info-caption-panel a{color:#757575;line-height:1;text-decoration:none}#info-caption-panel :hover{color:#000}#info-title{margin-right:auto}#group-manager{display:flex;flex-direction:column;height:100%;overflow-y:hidden;position:relative}#group-manager-buttons{background-color:#eceff1;border-left:1px solid #ccc;box-shadow:0 -1px #0000001a;display:flex;height:4rem;justify-content:space-around;padding:.4rem;z-index:2}ul.tabbar{background:#eee;display:flex;flex-grow:0;flex-shrink:0;list-style:none;margin:0;overflow:hidden;padding:0 0 0 1rem;width:100%}ul.tabbar>li{box-shadow:inset 0 -2px 2.5px -1px #00000040;height:2.4rem;position:relative}ul.tabbar li.active{background:#fff}ul.tabbar li:after{background:#ccc;bottom:0;content:"";height:.1rem;left:0;position:absolute;width:100%}ul.tabbar li.active:after{animation:tabbar-border-expand .2s cubic-bezier(.4,0,.4,1) 0s alternate forwards;background:#2196f3;bottom:0;content:"";height:.15rem;left:0;position:absolute}@keyframes tabbar-border-expand{0%{opacity:0;width:0}to{opacity:1;width:100%}}ul.tabbar>li>a{color:#2196f399;font-size:1.3rem;font-variant:small-caps;font-weight:400;line-height:2.4rem;overflow:hidden;padding-left:1rem;padding-right:1rem;position:relative;text-align:center;text-decoration:none}ul.tabbar li.active>a{color:#2196f3}.unread{background-color:#0097a7;border-radius:50%;color:#fff;display:inline-block;font-size:.75rem;font-weight:bolder;height:1.35rem;line-height:1.45rem;margin-left:.5rem;text-align:center;vertical-align:middle;width:1.35rem}.material-icons.as-badge{color:#aaa;font-size:1.15rem;padding-left:.2rem}.badge{align-self:flex-start;background-color:#e8e8e8;border:1px solid silver;border-radius:.2rem;color:#666;display:inline-block;font-family:Roboto Mono,Courier,monospace,sans-serif;font-size:.8rem;margin:.05rem .1rem;padding:.05rem .25rem}.badge.green{background-color:#efe;border-color:#aca;color:#383}.badge.yellow{background-color:#ffe;border-color:#cc9;color:#660}.badge.blue{background-color:#eef;border-color:#aad;color:#339}.badge.red{background-color:#fee;border-color:#c99;color:#933}.badge.magenta{background-color:#fef;border-color:#c9c;color:#939}.lettertile{border-radius:50%;padding-bottom:100%;user-select:none;width:100%}.lettertile>div{align-items:center;border-radius:50%;bottom:0;color:#fafafacc;display:flex;flex-grow:0;flex-shrink:0;justify-content:center;left:0;line-height:1;padding-top:.1em;position:absolute;right:0;text-align:center;text-transform:uppercase;top:0}.lettertile:hover{color:#fff}.lettertile.disabled{opacity:.5}.lt-bg-0{background-color:#ef9a9a}.lt-bg-1{background-color:#90caf9}.lt-bg-2{background-color:#b0bec5}.lt-bg-3{background-color:#b39ddb}.lt-bg-4{background-color:#ffab91}.lt-bg-5{background-color:#a5d6a7}.lt-bg-6{background-color:#eee}.lt-bg-7{background-color:#e6ee9c}.lt-bg-8{background-color:#c5e1a5}.lt-bg-9{background-color:#f0e57d}.lt-bg-10{background-color:#f48fb1}.lt-bg-11{background-color:#9fa8da}.lt-bg-12{background-color:#ffe082}.lt-bg-13{background-color:#bcaaa4}.lt-bg-14{background-color:#80deea}.lt-bg-15{background-color:#ce93d8}.dk-bg-0{background-color:#c62828}.dk-bg-1{background-color:#ad1457}.dk-bg-2{background-color:#6a1b9a}.dk-bg-3{background-color:#4527a0}.dk-bg-4{background-color:#283593}.dk-bg-5{background-color:#1565c0}.dk-bg-6{background-color:#0277bd}.dk-bg-7{background-color:#00838f}.dk-bg-8{background-color:#00695c}.dk-bg-9{background-color:#2e7d32}.dk-bg-10{background-color:#558b2f}.dk-bg-11{background-color:#9e9d24}.dk-bg-12{background-color:#f9a825}.dk-bg-13{background-color:#ff8f00}.dk-bg-14{background-color:#ef6c00}.dk-bg-15{background-color:#d84315}.lt-fg-0{color:#ef9a9a!important}.lt-fg-1{color:#90caf9!important}.lt-fg-2{color:#b0bec5!important}.lt-fg-3{color:#b39ddb!important}.lt-fg-4{color:#ffab91!important}.lt-fg-5{color:#a5d6a7!important}.lt-fg-6{color:#eee!important}.lt-fg-7{color:#e6ee9c!important}.lt-fg-8{color:#c5e1a5!important}.lt-fg-9{color:#f0e57d!important}.lt-fg-10{color:#f48fb1!important}.lt-fg-11{color:#9fa8da!important}.lt-fg-12{color:#ffe082!important}.lt-fg-13{color:#bcaaa4!important}.lt-fg-14{color:#80deea!important}.lt-fg-15{color:#ce93d8!important}.dk-fg-0{color:#c62828!important}.dk-fg-1{color:#ad1457!important}.dk-fg-2{color:#6a1b9a!important}.dk-fg-3{color:#4527a0!important}.dk-fg-4{color:#283593!important}.dk-fg-5{color:#1565c0!important}.dk-fg-6{color:#0277bd!important}.dk-fg-7{color:#00838f!important}.dk-fg-8{color:#00695c!important}.dk-fg-9{color:#2e7d32!important}.dk-fg-10{color:#558b2f!important}.dk-fg-11{color:#9e9d24!important}.dk-fg-12{color:#f9a825!important}.dk-fg-13{color:#ff8f00!important}.dk-fg-14{color:#ef6c00!important}.dk-fg-15{color:#d84315!important}.material-icons{font-size:1.4rem;line-height:1;text-align:center;vertical-align:middle}.material-icons.small{font-size:1rem}.material-icons.medium{font-size:1.2rem}.material-icons.big{font-size:2.2rem}.material-icons.bigger{font-size:3rem}.material-icons.huge{font-size:4rem}.composed-material{display:inline-block;position:relative}.composed-material i:nth-child(2){font-size:85%;font-weight:700;left:39%;position:absolute;top:7%}#self-avatar .material-icons{font-size:2.4rem;line-height:3rem}.avatar-box .material-icons{border-radius:50%;color:#fafafacc;height:inherit;width:inherit}.chip-input{align-items:center;background:#0000;border-bottom:1px solid #ccc;box-shadow:0 1px 0 #0000001a;color:#666;cursor:text;display:flex;flex-grow:1;flex-wrap:wrap;font-size:1.1rem;font-weight:400;justify-content:flex-start;margin:.25rem 0;max-height:10rem;min-height:4rem;outline:none;overflow-y:scroll;text-align:center;transition:all .1s ease-in-out}.chip-input.focused{border-bottom:1px solid #2196f3;box-shadow:0 1px 0 #2196f340}.chip-input>input[type=text]{border:none;box-shadow:none;display:inline-block;flex:1 0 auto;min-width:4rem;width:auto}.chip,.chip-input>input[type=text]{height:1.8rem;line-height:1.8;margin:.125rem}.chip{align-items:center;background-color:#eee;border-radius:.9rem;display:flex;flex:0 0 auto;font-size:90%;max-width:12rem;overflow:hidden}.chip,.chip>span{text-overflow:ellipsis;white-space:nowrap}.chip>span{overflow-x:hidden}.chip.invalid{background-color:#fee;color:#c66}.chip>a{background-color:#aaa;border-radius:50%;color:#eee;display:inline-block;height:1.175rem;line-height:1.15;margin:.3rem;min-width:1.175rem;text-decoration:none;width:1.175rem}.chip>a:focus,.chip>a:hover{background-color:#bbb;color:#fff;text-decoration:none}.chip .avatar-box{font-size:1rem;height:1.8rem;margin-right:.2rem;min-height:1.8rem;min-width:1.8rem;position:relative;width:1.8rem}.chip .avatar-box .material-icons{font-size:1.4rem;line-height:1.8rem}.chip .spacer{display:inline-block;height:1.175rem;margin:.3rem;width:.25rem}.load-spinner-box{--size-small:2.5rem;--size-large:4rem;background-color:#fff;border-radius:50%;box-shadow:.1rem .1rem .15rem 1px #00000040;height:var(--size-small);left:0;margin-left:auto;margin-right:auto;padding:.25rem;position:absolute;right:0;top:5rem;width:var(--size-small);z-index:2}.load-spinner-box.large{height:var(--size-large);width:var(--size-large)}.load-spinner-box.clear{background-color:#fff8;-x-box-shadow:none;box-shadow:.1rem .1rem .15rem 1px #0000001f}.load-spinner-box.centered{top:calc(var(--size-small)/2 - .25rem)}.load-spinner-box.large.centered{top:calc(var(--size-large)/2 - .25rem)}.loader-spinner{animation:spin 1.5s linear infinite;border:.35rem solid #f3f3f3;border-radius:50%;border-top-color:#1e88e5;height:2rem;width:2rem}.load-spinner-box.large .loader-spinner{height:3.5rem;width:3.5rem}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.uploader{display:flex}.uploader>div{background-color:#cfd8dc;border-radius:.1rem;height:.6rem;line-height:0;margin:.35rem;padding:.15rem;position:relative;width:6rem}.uploader>div>span{background-color:#1e88e5;border-radius:.08rem;display:inline-block;height:.3rem}.highlight{color:teal;font-weight:bolder}.highlight .preview{color:#5f9ea0;font-weight:inherit}.cropper{align-self:center;height:21rem;margin:1rem auto auto;transform:translateZ(0)}.cropper,.cropper .bounding-box{border-radius:.25rem;position:relative;width:18rem}.cropper .bounding-box{background-color:#999;border:2px solid #1e88e5;box-shadow:1px 1px 3px grey;height:18rem;overflow:hidden;z-index:1}.cropper .preview{max-height:none;max-width:none;position:absolute;z-index:-1}.cropper .cutout{border:1px dashed #999;bottom:0;box-shadow:0 0 150rem 150rem #e6f6ffa8;height:17rem;left:0;margin:auto;position:absolute;right:0;top:0;width:17rem;z-index:0}.cropper .cutout.circle{border-radius:50%}.cropper .overlay{cursor:move;position:absolute;touch-action:none;z-index:1}.cropper .zoom-wrapper{height:2rem;margin:1rem auto;text-align:center;width:80%}.cropper .zoom{box-shadow:none;pointer-events:none;z-index:2}.cropper .zoom:focus{outline:none}.reply-quote{border-left:.25rem solid #039be5;border-radius:.25rem;color:#666;cursor:pointer;font-weight:lighter;line-height:1.5em;padding:.15rem .5rem .25rem;position:relative}.chat-box .bubble .reply-quote{margin:.15rem -.75rem .3rem}.chat-box .bubble .reply-quote:first-child{margin-top:-.25rem}.chat-box .left .reply-quote{background-color:#fff6}.chat-box .right .reply-quote{background-color:#8080801f}.reply-quote .inline-image{margin-bottom:.1rem;vertical-align:middle}.reply-quote img.inline-image:before{content:""}#reply-quote-preview{align-items:center;display:flex}#reply-quote-preview .reply-quote{background-color:#fafafa;cursor:auto;margin:.35rem .5rem 0 0;width:100%}#reply-quote-preview .cancel{min-width:2.4rem;text-align:center}#reply-quote-preview .inline-image{background-color:#ccc}#reply-quote-preview img.inline-image:before{content:""}#send-message-panel #reply-quote-preview{width:100%}#send-message-panel #reply-quote-preview .reply-quote{margin-bottom:.35rem}.forwarding-quote{font-size:200%;line-height:200%}.mention{font-weight:500}.forward-dialog{background-color:#fff;border-radius:.3rem;box-shadow:.15rem .15rem 1.5rem #00000073;height:80%;margin:auto 1rem;overflow:hidden;padding:.5rem;position:relative;width:24rem}.forward-dialog .scrollable-panel{height:80%}.audio{align-items:center;background-color:#eee;border-radius:1.5rem;display:flex;flex:none;height:3rem;margin-left:auto}.audio canvas{height:2.6rem;margin-right:2rem;width:14rem}.audio canvas.playback{margin-right:0}.audio .duration{font-size:large}.audio-player{align-items:center;display:flex}.audio-player .material-icons.large{color:#667575cc;font-size:2.4rem}.audio-player .material-icons.disabled{color:#999c;cursor:default}.audio-player .playback{height:2.6rem;width:14rem}.audio-player canvas.playback{display:block}.audio-player div.playback{align-items:center;color:#757575cc;display:flex;font-size:small;justify-content:center}.audio-player .timer{color:#666c;font-size:smaller}#pinned-wrapper{margin-right:1rem}#pinned-wrapper,.pinned{align-items:center;display:flex;height:100%;position:relative}.pinned{background-color:#fafafa;background-position:50%;border-radius:0 .25rem .25rem 0;color:#666;cursor:pointer;font-weight:lighter;min-width:0;padding:.15rem .5rem .25rem;transition:background .4s;width:24rem}.pinned>p{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.pinned:hover{background-color:#fafdff}.pinned:active{background-color:#d8e8ff;transition:background 0s}.pinned-scroll{align-items:center;background-color:#eeee;border-radius:.25rem 0 0 .25rem;display:flex;flex-direction:column;height:100%;justify-content:space-evenly;margin-left:.35rem;padding:.1rem;position:relative}.pinned-scroll .dot{background-color:#999;border-radius:50%;height:.3rem;width:.3rem}.pinned-scroll .adot{background-color:#039be5;border-radius:50%;height:.45rem;width:.45rem}#pinned-wrapper .menuTrigger.upper{top:.25rem}#pinned-wrapper .menuTrigger.lower{bottom:.25rem}#pinned-wrapper:hover .menuTrigger{opacity:.9}#pinned-wrapper:hover .menuTrigger a{color:#757575}.drag-n-drop{align-items:center;background-color:#fffffff2;border:.15rem dashed grey;border-radius:1rem;bottom:1.5rem;color:grey;display:flex;font-size:2rem;justify-content:center;left:1.5rem;outline:.5rem solid #fffffff2;position:absolute;right:1.5rem;top:1.5rem;z-index:3}.country-flag{display:inline-block;font-size:larger}.dial-code{cursor:pointer;display:inline-block;margin-bottom:.15rem;vertical-align:middle;white-space:nowrap}ul.phone-country-selector{list-style:none;overflow-x:hidden}ul.phone-country-selector>li{cursor:pointer;font-weight:lighter;padding:.25rem .5rem .15rem;position:relative;width:100%}ul.phone-country-selector>li:hover{background-color:#eceff1}ul.phone-country-selector>li.selected{background-color:#cfd8dc}ul.phone-country-selector .dial-code{color:#1e88e5;font-weight:400;position:absolute;right:.5rem}ul.phone-country-selector span{display:inline-block}ul.phone-country-selector span.country{overflow-x:hidden;text-overflow:ellipsis}@media (min-width:1440px){#app-container{max-width:114rem}}@media (max-width:960px){#mountPoint{padding:0}.pinned{width:16rem}}@media (max-width:640px){#app-container{border-radius:0;box-shadow:none;width:100%}#info-view,#sidepanel{width:100%}.forward-dialog{border-radius:.3rem .3rem 0 0;margin:auto 0 0;width:100%}.menuTrigger{opacity:.3}#send-message-input{transition-property:-tinode-mobile}#pinned-wrapper{height:3.5rem;margin:.25rem}.pinned{width:unset}}.call-party-speaking{border:2px solid red}.call-party.carousel-item .caller-name.inactive{bottom:.25rem;position:absolute;right:.25rem}.call-party.carousel-item .muted{position:absolute;right:.25rem;top:.25rem}.avatar-box.carousel-item{height:45px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:45px}#carousel-wrapper{align-self:center;max-width:800px;padding-top:1rem;position:relative;width:100%}#carousel{overflow:auto;scroll-behavior:smooth;scrollbar-width:none}#carousel-next,#carousel-prev{align-content:center;background:#fff;border:none;border-radius:50%;cursor:pointer;display:flex;height:unset;justify-content:center;min-width:unset;outline:0;padding:8px;position:absolute}#carousel-prev{display:none;left:0;top:50%;transform:translate(50%,-50%)}#carousel-next{right:0;top:50%;transform:translate(-50%,-50%)}#carousel-content{grid-gap:16px;box-sizing:border-box;display:grid;grid-auto-flow:column;justify-content:center;margin:auto}.carousel-item{height:100px;position:relative;width:180px} \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index a5835439..bed545c5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,3 +6,6 @@ export const CALL_STATE_IN_PROGRESS = 3; // Video call message headers representing call state. export const CALL_HEAD_STARTED = 'started'; + +// Gap between video conference carousel items. +export const VC_CAROUSEL_ITEM_GAP = 16; diff --git a/src/i18n.min/en.json b/src/i18n.min/en.json index b24d29e0..2c076e80 100644 --- a/src/i18n.min/en.json +++ b/src/i18n.min/en.json @@ -1 +1 @@ -{"action_block_contact":"Block Contact","action_cancel":"cancel","action_clear_messages":"Clear Messages","action_delete_messages":"Clear Messages for All","action_leave_chat":"Leave Conversation","action_report_chat":"Report Conversation","archived_contacts":"Archived contacts ({count})","badge_danger":"Untrustworthy","badge_owner":"owner","badge_staff":"Staff-managed","badge_verified":"Verified/official","badge_you":"you","block_contact_warning":"Are you sure you want to block this contact?","blocked_contacts_link":"Blocked contacts ({count})","button_add_members":"Add members","button_cancel":"Cancel","button_confirm":"Confirm","button_create":"Create","button_delete_account":"Delete account","button_edit":"Edit","button_logout":"Logout","button_ok":"OK","button_reset":"Reset","button_send_request":"Send request","button_sign_in":"Sign in","button_sign_up":"Sign up","button_subscribe":"Subscribe","button_update":"Update","cannot_initiate_file_upload":"Cannot initiate file upload.","channel":"channel","channel_prompt":"This is a channel","chat_invitation":"You are invited to start a new chat. What would you like to do?","chat_invitation_accept":"Accept","chat_invitation_block":"Block","chat_invitation_ignore":"Ignore","clear_messages_warning":"Are you sure you want to clear all messages? It cannot be undone.","code_doesnot_match":"Code does not match","contacts_not_found":"You have no chats
    ¯∖_(ツ)_/¯","contacts_not_found_short":"No contacts match ''{query}''","credential_email_prompt":"Your registration email","delete_account":"Delete account","delete_account_warning":"Are you sure you want to delete your account? It cannot be undone.","delete_messages_warning":"Are you sure you want to delete all messages for everyone? It cannot be undone.","download_action":"download","drafty_attachment":"Attachment","drafty_form":"Form:","drafty_image":"Picture","email_dative":"email","email_prompt":"Email, e.g. jdoe@example.com","enable_peers_messaging":"Enable","enter_confirmation_code_prompt":"Confirmation code","error_invalid_id":"Invalid ID","file_attachment_too_large":"The file size {size} exceeds the {limit} limit.","forgot_password_link":"Forgot password?","full_name_prompt":"Full name, e.g. John Doe","granted_permissions":"Granted","group_has_no_members":"No members","group_user_id_prompt":"Group or User ID","image_caption_prompt":"Image caption","invalid_content":"invalid content","invalid_security_token":"Invalid security token","label_client":"Client:","label_content_type":"Content type:","label_default_access_mode":"Default access mode:","label_file_name":"File name:","label_group_members":"Group members:","label_incognito_mode":"Incognito mode:","label_message_sound":"Message sound:","label_muting_topic":"Muted:","label_other_user":"Other","label_password":"Password","label_permissions":"Permissions:","label_private":"Private comment","label_push_notifications":"Notification alerts:","label_push_notifications_disabled":"Notification alerts (requires HTTPS):","label_reset_password":"Send a password reset email:","label_sdk":"SDK:","label_server":"Server:","label_server_address":"Server address:","label_server_to_use":"Server to use:","label_size":"Size:","label_topic_name":"Name","label_user_contacts":"Contacts","label_user_id":"ID:","label_wire_transport":"Wire transport:","label_you":"You:","label_your_name":"Your name","label_your_permissions":"Your permissions:","last_seen_timestamp":"Last seen","leave_chat_warning":"Are you sure you want to leave this conversation?","link_contact_us":"Contact Us","link_privacy_policy":"Privacy Policy","link_terms_of_service":"Terms of Service","login_prompt":"Login","menu_item_archive_topic":"Archive","menu_item_block":"Block","menu_item_clear_messages":"Clear messages","menu_item_clear_messages_for_all":"Clear for All","menu_item_delete":"Delete","menu_item_delete_for_all":"Delete for All","menu_item_delete_topic":"Delete","menu_item_edit_permissions":"Edit permissions","menu_item_info":"Info","menu_item_member_delete":"Remove","menu_item_mute":"Mute","menu_item_reply":"Reply","menu_item_restore_topic":"Restore","menu_item_send_retry":"Retry","menu_item_unblock":"Unblock","menu_item_unmute":"Unmute","message_sending":"sending...","message_sending_failed":"failed","messages_not_readable":"no access to messages","messaging_disabled_prompt":"Messaging disabled","more_online_members":"+{overflow} more","new_message_prompt":"New message","new_password_placeholder":"Enter new password","no_connection":"No connection","no_contacts":"You have no contacts :-(","numeric_confirmation_code_prompt":"Numbers only","online_now":"online now","password_prompt":"Password","password_unchanged_prompt":"Unchanged","peers_messaging_disabled":"Peer's messaging is disabled.","permission_admin":"Approve ({val})","permission_delete":"Delete ({val})","permission_join":"Join ({val})","permission_owner":"Owner ({val})","permission_pres":"Get notified ({val})","permission_read":"Read ({val})","permission_share":"Share ({val})","permission_write":"Write ({val})","phone_dative":"phone","private_editing_placeholder":"Visible to you only","push_init_failed":"Failed to initialize push notifications","reconnect_countdown":"Disconnected. Reconnecting in {seconds}…","reconnect_now":"Try now","reload_update":"Reload","report_chat_warning":"Are you sure you want to block and report this conversation?","requested_permissions":"Requested","save_attachment":"save","search_for_contacts":"Use search to find contacts","search_no_results":"Search returned no results","search_placeholder":"List like email:alice@example.com, tel:17025550003...","sidepanel_title_acc_notifications":"Notifications","sidepanel_title_acc_support":"Support","sidepanel_title_account_settings":"Account Settings","sidepanel_title_archive":"Archived Chats","sidepanel_title_blocked":"Blocked Chats","sidepanel_title_cred":"Confirm Credentials","sidepanel_title_login":"Sign In","sidepanel_title_newtpk":"Start New Chat","sidepanel_title_register":"Create Account","sidepanel_title_reset":"Reset Password","sidepanel_title_settings":"Settings","stay_logged_in":"Stay logged in","tabtitle_find_user":"find","tabtitle_group_by_id":"by id","tabtitle_new_group":"new group","tags_editor_no_tags":"Add some tags","tags_not_found":"No tags defined. Add some.","title_all_contacts":"All Contacts","title_group_members":"Group Members","title_manage_tags":"Manage","title_not_found":"Not found","title_permissions":"Permissions","title_tag_manager":"Tags (search & discovery)","topic_block_warning":"Are you sure you want to block this conversation?","topic_delete_warning":"Are you sure you want to delete this conversation?","topic_name_editing_placeholder":"Freeform name of the group","unnamed_topic":"Unnamed","update_available":"Update available.","upload_finishing":"finishing...","user_not_found":"Not found","description_editing_placeholder":"Optional description","label_description":"Description","button_security":"Security","panel_title_crop":"Drag to Adjust","panel_title_general":"General","panel_title_members":"Members","panel_title_security":"Security","panel_title_info":"Info","permissions_anonymous":"Anonymous","permissions_authenticated":"Authenticated","topic_delete":"Delete Conversation","permissions_user":"User's Permissions","password_reset_email_sent":"An email with security code has been sent.","label_unarchive_topic":"Archived:","menu_item_forward":"Forward","forward_to":"Forward to","forward_to_search_placeholder":"Search contacts","label_new_password":"New password","drafty_unknown":"Unsupported","calls_incoming":"Incoming call","calls_outgoing":"Outgoing call","calls_you_label":"You","menu_item_video_call":"Video call","already_in_call":"You already in an ongoing call!","call_cancelled":"cancelled","call_missed":"missed","call_declined":"declined","call_disconnected":"disconnected","label_use_secure_connection":"Use secure connection","cred_confirmed_successfully":"Confirmed successfully","menu_item_edit":"Edit","editing_message":"Editing","message_edited_marker":", edited","icon_title_add_image":"Add image","icon_title_attach_file":"Attach file","icon_title_delete":"Delete","icon_title_pause":"Pause","icon_title_play":"Play","icon_title_record_voice":"Record voice message","icon_title_resume":"Resume","icon_title_send":"Send message","drag_file":"Drag file here","drafty_video":"Video recording","call_in_progress":"in progress","menu_item_audio_call":"Call","unrecognized_video_format":"Format of this video is not recognized","loading_note":"Loading...","password_i_have_code":"I have code","label_reset_password_tel":"Send a password reset SMS:","mobile_phone_number":"Mobile phone number","password_reset_sms_sent":"A text message with security code has been sent.","mobile_number_required":"Mobile phone number required","current_email":"Current email","current_phone":"Current phone number","new_email":"New email","new_phone_number":"New phone number","change_email":"Change email","change_phone":"Change phone number","call_busy":"busy","failed_to_init_audio":"Failed to initialize audio recording","pin_message":"Pin","unpin_message":"Unpin"} \ No newline at end of file +{"action_block_contact":"Block Contact","action_cancel":"cancel","action_clear_messages":"Clear Messages","action_delete_messages":"Clear Messages for All","action_leave_chat":"Leave Conversation","action_report_chat":"Report Conversation","archived_contacts":"Archived contacts ({count})","badge_danger":"Untrustworthy","badge_owner":"owner","badge_staff":"Staff-managed","badge_verified":"Verified/official","badge_you":"you","block_contact_warning":"Are you sure you want to block this contact?","blocked_contacts_link":"Blocked contacts ({count})","button_add_members":"Add members","button_cancel":"Cancel","button_confirm":"Confirm","button_create":"Create","button_delete_account":"Delete account","button_edit":"Edit","button_logout":"Logout","button_ok":"OK","button_reset":"Reset","button_send_request":"Send request","button_sign_in":"Sign in","button_sign_up":"Sign up","button_subscribe":"Subscribe","button_update":"Update","cannot_initiate_file_upload":"Cannot initiate file upload.","channel":"channel","channel_prompt":"This is a channel","chat_invitation":"You are invited to start a new chat. What would you like to do?","chat_invitation_accept":"Accept","chat_invitation_block":"Block","chat_invitation_ignore":"Ignore","clear_messages_warning":"Are you sure you want to clear all messages? It cannot be undone.","code_doesnot_match":"Code does not match","contacts_not_found":"You have no chats
    ¯∖_(ツ)_/¯","contacts_not_found_short":"No contacts match ''{query}''","credential_email_prompt":"Your registration email","delete_account":"Delete account","delete_account_warning":"Are you sure you want to delete your account? It cannot be undone.","delete_messages_warning":"Are you sure you want to delete all messages for everyone? It cannot be undone.","download_action":"download","drafty_attachment":"Attachment","drafty_form":"Form:","drafty_image":"Picture","email_dative":"email","email_prompt":"Email, e.g. jdoe@example.com","enable_peers_messaging":"Enable","enter_confirmation_code_prompt":"Confirmation code","error_invalid_id":"Invalid ID","file_attachment_too_large":"The file size {size} exceeds the {limit} limit.","forgot_password_link":"Forgot password?","full_name_prompt":"Full name, e.g. John Doe","granted_permissions":"Granted","group_has_no_members":"No members","group_user_id_prompt":"Group or User ID","image_caption_prompt":"Image caption","invalid_content":"invalid content","invalid_security_token":"Invalid security token","label_client":"Client:","label_content_type":"Content type:","label_default_access_mode":"Default access mode:","label_file_name":"File name:","label_group_members":"Group members:","label_incognito_mode":"Incognito mode:","label_message_sound":"Message sound:","label_muting_topic":"Muted:","label_other_user":"Other","label_password":"Password","label_permissions":"Permissions:","label_private":"Private comment","label_push_notifications":"Notification alerts:","label_push_notifications_disabled":"Notification alerts (requires HTTPS):","label_reset_password":"Send a password reset email:","label_sdk":"SDK:","label_server":"Server:","label_server_address":"Server address:","label_server_to_use":"Server to use:","label_size":"Size:","label_topic_name":"Name","label_user_contacts":"Contacts","label_user_id":"ID:","label_wire_transport":"Wire transport:","label_you":"You:","label_your_name":"Your name","label_your_permissions":"Your permissions:","last_seen_timestamp":"Last seen","leave_chat_warning":"Are you sure you want to leave this conversation?","link_contact_us":"Contact Us","link_privacy_policy":"Privacy Policy","link_terms_of_service":"Terms of Service","login_prompt":"Login","menu_item_archive_topic":"Archive","menu_item_block":"Block","menu_item_clear_messages":"Clear messages","menu_item_clear_messages_for_all":"Clear for All","menu_item_delete":"Delete","menu_item_delete_for_all":"Delete for All","menu_item_delete_topic":"Delete","menu_item_edit_permissions":"Edit permissions","menu_item_info":"Info","menu_item_member_delete":"Remove","menu_item_mute":"Mute","menu_item_reply":"Reply","menu_item_restore_topic":"Restore","menu_item_send_retry":"Retry","menu_item_unblock":"Unblock","menu_item_unmute":"Unmute","message_sending":"sending...","message_sending_failed":"failed","messages_not_readable":"no access to messages","messaging_disabled_prompt":"Messaging disabled","more_online_members":"+{overflow} more","new_message_prompt":"New message","new_password_placeholder":"Enter new password","no_connection":"No connection","no_contacts":"You have no contacts :-(","numeric_confirmation_code_prompt":"Numbers only","online_now":"online now","password_prompt":"Password","password_unchanged_prompt":"Unchanged","peers_messaging_disabled":"Peer's messaging is disabled.","permission_admin":"Approve ({val})","permission_delete":"Delete ({val})","permission_join":"Join ({val})","permission_owner":"Owner ({val})","permission_pres":"Get notified ({val})","permission_read":"Read ({val})","permission_share":"Share ({val})","permission_write":"Write ({val})","phone_dative":"phone","private_editing_placeholder":"Visible to you only","push_init_failed":"Failed to initialize push notifications","reconnect_countdown":"Disconnected. Reconnecting in {seconds}…","reconnect_now":"Try now","reload_update":"Reload","report_chat_warning":"Are you sure you want to block and report this conversation?","requested_permissions":"Requested","save_attachment":"save","search_for_contacts":"Use search to find contacts","search_no_results":"Search returned no results","search_placeholder":"List like email:alice@example.com, tel:17025550003...","sidepanel_title_acc_notifications":"Notifications","sidepanel_title_acc_support":"Support","sidepanel_title_account_settings":"Account Settings","sidepanel_title_archive":"Archived Chats","sidepanel_title_blocked":"Blocked Chats","sidepanel_title_cred":"Confirm Credentials","sidepanel_title_login":"Sign In","sidepanel_title_newtpk":"Start New Chat","sidepanel_title_register":"Create Account","sidepanel_title_reset":"Reset Password","sidepanel_title_settings":"Settings","stay_logged_in":"Stay logged in","tabtitle_find_user":"find","tabtitle_group_by_id":"by id","tabtitle_new_group":"new group","tags_editor_no_tags":"Add some tags","tags_not_found":"No tags defined. Add some.","title_all_contacts":"All Contacts","title_group_members":"Group Members","title_manage_tags":"Manage","title_not_found":"Not found","title_permissions":"Permissions","title_tag_manager":"Tags (search & discovery)","topic_block_warning":"Are you sure you want to block this conversation?","topic_delete_warning":"Are you sure you want to delete this conversation?","topic_name_editing_placeholder":"Freeform name of the group","unnamed_topic":"Unnamed","update_available":"Update available.","upload_finishing":"finishing...","user_not_found":"Not found","description_editing_placeholder":"Optional description","label_description":"Description","button_security":"Security","panel_title_crop":"Drag to Adjust","panel_title_general":"General","panel_title_members":"Members","panel_title_security":"Security","panel_title_info":"Info","permissions_anonymous":"Anonymous","permissions_authenticated":"Authenticated","topic_delete":"Delete Conversation","permissions_user":"User's Permissions","password_reset_email_sent":"An email with security code has been sent.","label_unarchive_topic":"Archived:","menu_item_forward":"Forward","forward_to":"Forward to","forward_to_search_placeholder":"Search contacts","label_new_password":"New password","drafty_unknown":"Unsupported","calls_incoming":"Incoming call","calls_outgoing":"Outgoing call","calls_you_label":"You","menu_item_video_call":"Video call","already_in_call":"You already in an ongoing call!","call_cancelled":"cancelled","call_missed":"missed","call_declined":"declined","call_disconnected":"disconnected","label_use_secure_connection":"Use secure connection","cred_confirmed_successfully":"Confirmed successfully","menu_item_edit":"Edit","editing_message":"Editing","message_edited_marker":", edited","icon_title_add_image":"Add image","icon_title_attach_file":"Attach file","icon_title_delete":"Delete","icon_title_pause":"Pause","icon_title_play":"Play","icon_title_record_voice":"Record voice message","icon_title_resume":"Resume","icon_title_send":"Send message","drag_file":"Drag file here","drafty_video":"Video recording","call_in_progress":"in progress","menu_item_audio_call":"Call","unrecognized_video_format":"Format of this video is not recognized","loading_note":"Loading...","password_i_have_code":"I have code","label_reset_password_tel":"Send a password reset SMS:","mobile_phone_number":"Mobile phone number","password_reset_sms_sent":"A text message with security code has been sent.","mobile_number_required":"Mobile phone number required","current_email":"Current email","current_phone":"Current phone number","new_email":"New email","new_phone_number":"New phone number","change_email":"Change email","change_phone":"Change phone number","call_busy":"busy","failed_to_init_audio":"Failed to initialize audio recording","pin_message":"Pin","unpin_message":"Unpin","calls_conference":"Conference call"} \ No newline at end of file diff --git a/src/i18n/de.json b/src/i18n/de.json index 87d1ce27..e8a9919d 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -1647,5 +1647,12 @@ "description": "Menu item [Unpin] for un-pinning the message.", "missing": true, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 9cff048d..9ccc2d4e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1647,5 +1647,12 @@ "description": "Menu item [Unpin] for un-pinning the message.", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "Conference call", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": false, + "obsolete": false } } diff --git a/src/i18n/es.json b/src/i18n/es.json index 146e4496..3c436249 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/ex/base-en.json b/src/i18n/ex/base-en.json index 02be202a..39aa4ab1 100644 --- a/src/i18n/ex/base-en.json +++ b/src/i18n/ex/base-en.json @@ -143,6 +143,10 @@ "defaultMessage": "missed", "description": "Label for missed incoming call" }, + "calls_conference": { + "defaultMessage": "Conference call", + "description": "Conference call label" + }, "calls_incoming": { "defaultMessage": "Incoming call", "description": "Incoming call label" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index d6ad4413..7ca81141 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/ko.json b/src/i18n/ko.json index f4dc7947..185d4ae9 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/ro.json b/src/i18n/ro.json index b1bf5ef3..ecb46c18 100644 --- a/src/i18n/ro.json +++ b/src/i18n/ro.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 0dada6f6..87115984 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/zh-TW.json b/src/i18n/zh-TW.json index d51abd6e..f3859a26 100644 --- a/src/i18n/zh-TW.json +++ b/src/i18n/zh-TW.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/i18n/zh.json b/src/i18n/zh.json index c21b655b..e8d3f5f7 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -1647,5 +1647,12 @@ "description": "Error message when audio is not available", "missing": false, "obsolete": false + }, + "calls_conference": { + "translation": "", + "defaultMessage": "Conference call", + "description": "Conference call label", + "missing": true, + "obsolete": false } } diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index 474296c8..952a6d87 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -1442,7 +1442,7 @@ class MessagesView extends React.Component { ); } else if (this.state.rtcPanel) { // P2P call. - if (this.state.rtcPanel.startsWith("usr")) { + if (Tinode.isP2PTopicName(this.state.rtcPanel)) { component2 = ( +
    <> @@ -90,5 +86,3 @@ class VCCarouselItem extends React.PureComponent { ); } } - -export default injectIntl(VCCarouselItem); diff --git a/src/widgets/vc-carousel.jsx b/src/widgets/vc-carousel.jsx index 204175af..ca0e3801 100644 --- a/src/widgets/vc-carousel.jsx +++ b/src/widgets/vc-carousel.jsx @@ -1,13 +1,13 @@ import React from 'react'; -import { injectIntl } from 'react-intl'; -import { - Track, -} from 'livekit-client'; + +import { Track } from 'livekit-client'; import LetterTile from './letter-tile.jsx'; import VCCarouselItem from './vc-carousel-item.jsx'; -class VCCarousel extends React.Component { +import { VC_CAROUSEL_ITEM_GAP } from '../constants.js'; + +export default class VCCarousel extends React.Component { constructor(props) { super(props); this.handlePrevClick = this.handlePrevClick.bind(this); @@ -20,26 +20,26 @@ class VCCarousel extends React.Component { } handlePrevClick() { - const gap = 16; + const gap = VC_CAROUSEL_ITEM_GAP; const width = this.carouselRef.current.offsetWidth; this.carouselRef.current.scrollBy(-(width + gap), 0); if (this.carouselRef.current.scrollLeft - width - gap <= 0) { - this.prevRef.current.style.display = "none"; + this.prevRef.current.style.display = 'none'; } if (!this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) { - this.nextRef.current.style.display = "flex"; + this.nextRef.current.style.display = 'flex'; } } handleNextClick() { - const gap = 16; + const gap = VC_CAROUSEL_ITEM_GAP; const width = this.carouselRef.current.offsetWidth; this.carouselRef.current.scrollBy(width + gap, 0); if (this.carouselRef.current.scrollWidth !== 0) { - this.prevRef.current.style.display = "flex"; + this.prevRef.current.style.display = 'flex'; } if (this.contentRef.current.scrollWidth - width - gap <= this.carouselRef.current.scrollLeft + width) { - this.nextRef.current.style.display = "none"; + this.nextRef.current.style.display = 'none'; } } @@ -98,5 +98,3 @@ class VCCarousel extends React.Component { ); } } - -export default injectIntl(VCCarousel); diff --git a/src/widgets/vc-panel.jsx b/src/widgets/vc-panel.jsx index c6a0ce9c..efde75b8 100644 --- a/src/widgets/vc-panel.jsx +++ b/src/widgets/vc-panel.jsx @@ -14,7 +14,7 @@ import { Room, RoomEvent, Track, - VideoPresets, + VideoPresets } from 'livekit-client'; import LetterTile from './letter-tile.jsx'; @@ -50,7 +50,6 @@ class VCPanel extends React.PureComponent { this.start = this.start.bind(this); this.stop = this.stop.bind(this); - this.reportError = this.reportError.bind(this); this.handleCloseClick = this.handleCloseClick.bind(this); @@ -85,7 +84,6 @@ class VCPanel extends React.PureComponent { const topic = this.props.tinode.getTopic(this.props.topic); topic.onInfo = this.previousOnInfo; this.stop(); - } onInfo(info) { @@ -362,19 +360,19 @@ class VCPanel extends React.PureComponent { const peerTitle = this.state.activeSpeaker ? this.state.activeSpeaker : clipStr(this.props.title, MAX_PEER_TITLE_LENGTH); - const remoteActive = this.remoteRef.current && (this.remoteRef.current.src != '' || this.remoteRef.current.srcObject) + const remoteActive = this.remoteRef.current && (this.remoteRef.current.src || this.remoteRef.current.srcObject) return ( <>
    - { !this.isBroadcast && Object.keys(this.state.participants).length > 0 ? + {!this.isBroadcast && Object.keys(this.state.participants).length > 0 ? : null }
    - { isPublishing ? + {isPublishing ?