diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 7925686bf1..bc343f535c 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -190,7 +190,6 @@ limitations under the License. position: relative; padding: 8px 16px; border-radius: 8px; - min-height: 56px; box-sizing: border-box; display: grid; diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0b..f2ceb086c4 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -43,6 +43,14 @@ limitations under the License. } } + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + .mx_CallEvent_info { display: flex; flex-direction: row; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 8629682693..1d43c3030d 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_EventTile[data-layout=bubble], -.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { +.mx_EventListSummary[data-layout=bubble] { --avatarSize: 32px; --gutterSize: 11px; --cornerRadius: 12px; @@ -155,12 +155,20 @@ limitations under the License. .mx_EventTile_avatar { position: absolute; top: 0; + line-height: 1; + z-index: 9; img { box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } + &.mx_EventTile_noSender { + .mx_EventTile_avatar { + top: -19px; + } + } + .mx_BaseAvatar, .mx_EventTile_avatar { line-height: 1; @@ -216,90 +224,6 @@ limitations under the License. border-left-color: $eventbubble-reply-color; } - &.mx_EventTile_bubbleContainer, - &.mx_EventTile_info, - & ~ .mx_EventListSummary[data-expanded=false] { - --backgroundColor: transparent; - --gutterSize: 0; - - display: flex; - align-items: center; - justify-content: center; - padding: 5px 0; - - .mx_EventTile_avatar { - position: static; - order: -1; - margin-right: 5px; - } - } - - & ~ .mx_EventListSummary { - --maxWidth: 80%; - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - .mx_EventListSummary_toggle { - float: none; - margin: 0; - order: 9; - margin-left: 5px; - } - .mx_EventListSummary_avatars { - padding-top: 0; - } - - &::after { - content: ""; - clear: both; - } - - .mx_EventTile { - margin: 0 6px; - } - - .mx_EventTile_line { - margin: 0 5px; - > a { - left: auto; - right: 0; - transform: translateX(calc(100% + 5px)); - } - } - - .mx_MessageActionBar { - transform: translate3d(90%, 0, 0); - } - } - - & ~ .mx_EventListSummary[data-expanded=false] { - padding: 0 34px; - } - - /* events that do not require bubble layout */ - & ~ .mx_EventListSummary, - &.mx_EventTile_bad { - .mx_EventTile_line { - background: transparent; - } - - &:hover { - &::before { - background: transparent; - } - } - } - - & + .mx_EventListSummary { - .mx_EventTile { - margin-top: 0; - padding: 2px 0; - } - } - - .mx_EventListSummary_toggle { - margin-right: 55px; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -334,3 +258,78 @@ limitations under the License. max-width: 100%; } } + +.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], +.mx_EventTile.mx_EventTile_info[data-layout=bubble], +.mx_EventListSummary[data-layout=bubble][data-expanded=false] { + --backgroundColor: transparent; + --gutterSize: 0; + + display: flex; + align-items: center; + justify-content: center; + padding: 5px 0; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } +} + +.mx_EventListSummary[data-layout=bubble] { + --maxWidth: 80%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: calc(var(--gutterSize) + var(--avatarSize)); + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + margin-left: 5px; + margin-right: 55px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + padding: 2px 0; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(90%, 0, 0); + } +} + +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + padding: 0 34px; +} + +/* events that do not require bubble layout */ +.mx_EventListSummary[data-layout=bubble], +.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bda4a15f45..4a419244ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -133,7 +133,7 @@ $hover-select-border: 4px; } &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line { + & ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { padding-left: calc($left-gutter + 18px); } @@ -573,6 +573,12 @@ $hover-select-border: 4px; color: $accent-color-alt; } +.mx_EventTile_content .markdown-body blockquote { + border-left: 2px solid $blockquote-bar-color; + border-radius: 2px; + padding: 0 10px; +} + .mx_EventTile_content .markdown-body .hljs { display: inline !important; } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 97190807ca..578c0325d2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -116,6 +116,11 @@ $irc-line-height: $font-18px; .mx_EditMessageComposer_buttons { position: relative; } + + .mx_ReactionsRow { + padding-left: 0; + padding-right: 0; + } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index f3e204e415..fd21e5f348 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -60,8 +60,6 @@ limitations under the License. $reply-lines: 2; $line-height: $font-22px; - pointer-events: none; - text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/res/img/voip/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 0000000000..a2f3bc73ac --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 0000000000..5e3993584e --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 086cc30952..e7c1dda54f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -484,28 +484,18 @@ export default class CallHandler extends EventEmitter { break; case CallState.Ended: { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { this.play(AudioID.Busy); let title; let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { title = _t("User Busy"); description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { + } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { title = _t("Call Failed"); description = _t("The call could not be established"); } @@ -514,7 +504,7 @@ export default class CallHandler extends EventEmitter { title, description, }); } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting ) { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { title: _t("Answered Elsewhere"), diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 384f20cd4e..ce3b530858 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -74,6 +74,14 @@ export default class CallEventGrouper extends EventEmitter { return this.hangup?.getContent()?.reason; } + public get rejectParty(): string { + return this.reject?.getSender(); + } + + public get gotRejected(): boolean { + return Boolean(this.reject); + } + /** * Returns true if there are only events from the other side - we missed the call */ diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 39ede68a75..698a36127b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -618,7 +618,15 @@ export default class MessagePanel extends React.Component { for (const Grouper of groupers) { if (Grouper.canStartGroup(this, mxEv)) { - grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); + grouper = new Grouper( + this, + mxEv, + prevEvent, + lastShownEvent, + this.props.layout, + nextEvent, + nextTile, + ); } } if (!grouper) { @@ -981,6 +989,7 @@ abstract class BaseGrouper { public readonly event: MatrixEvent, public readonly prevEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent, + protected readonly layout: Layout, public readonly nextEvent?: MatrixEvent, public readonly nextEventTile?: MatrixEvent, ) { @@ -1107,6 +1116,7 @@ class CreationGrouper extends BaseGrouper { onToggle={panel.onHeightChanged} // Update scroll state summaryMembers={[ev.sender]} summaryText={summaryText} + layout={this.layout} > { eventTiles } , @@ -1134,10 +1144,11 @@ class RedactionGrouper extends BaseGrouper { ev: MatrixEvent, prevEvent: MatrixEvent, lastShownEvent: MatrixEvent, + layout: Layout, nextEvent: MatrixEvent, nextEventTile: MatrixEvent, ) { - super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile); + super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile); this.events = [ev]; } @@ -1202,6 +1213,7 @@ class RedactionGrouper extends BaseGrouper { onToggle={panel.onHeightChanged} // Update scroll state summaryMembers={Array.from(senders)} summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })} + layout={this.layout} > { eventTiles } , @@ -1230,8 +1242,9 @@ class MemberGrouper extends BaseGrouper { public readonly event: MatrixEvent, public readonly prevEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent, + protected readonly layout: Layout, ) { - super(panel, event, prevEvent, lastShownEvent); + super(panel, event, prevEvent, lastShownEvent, layout); this.events = [event]; } @@ -1306,6 +1319,7 @@ class MemberGrouper extends BaseGrouper { events={this.events} onToggle={panel.onHeightChanged} // Update scroll state startExpanded={highlightInMels} + layout={this.layout} > { eventTiles } , diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 1d16755106..112f8d2c21 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component { private readonly itemlist = createRef(); private unmounted = false; private scrollTimeout: Timer; + // Are we currently trying to backfill? private isFilling: boolean; + // Is the current fill request caused by a props update? + private isFillingDueToPropsUpdate = false; + // Did another request to check the fill state arrive while we were trying to backfill? private fillRequestWhileRunning: boolean; + // Is that next fill request scheduled because of a props update? + private pendingFillDueToPropsUpdate: boolean; private scrollState: IScrollState; private preventShrinkingState: IPreventShrinkingState; private unfillDebouncer: number; @@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component { // adding events to the top). // // This will also re-check the fill state, in case the paginate was inadequate - this.checkScroll(); + this.checkScroll(true); this.updatePreventShrinking(); } @@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component { // after an update to the contents of the panel, check that the scroll is // where it ought to be, and set off pagination requests if necessary. - public checkScroll = () => { + public checkScroll = (isFromPropsUpdate = false) => { if (this.unmounted) { return; } this.restoreSavedScrollState(); - this.checkFillState(); + this.checkFillState(0, isFromPropsUpdate); }; // return true if the content is fully scrolled down right now; else false. @@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component { } // check the scroll state and send out backfill requests if necessary. - public checkFillState = async (depth = 0): Promise => { + public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise => { if (this.unmounted) { return; } @@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component { // don't allow more than 1 chain of calls concurrently // do make a note when a new request comes in while already running one, // so we can trigger a new chain of calls once done. + // However, we make an exception for when we're already filling due to a + // props (or children) update, because very often the children include + // spinners to say whether we're paginating or not, so this would cause + // infinite paginating. if (isFirstCall) { - if (this.isFilling) { + if (this.isFilling && !this.isFillingDueToPropsUpdate) { debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request"); this.fillRequestWhileRunning = true; + this.pendingFillDueToPropsUpdate = isFromPropsUpdate; return; } debuglog("isFilling: setting"); this.isFilling = true; + this.isFillingDueToPropsUpdate = isFromPropsUpdate; } const itemlist = this.itemlist.current; @@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component { if (isFirstCall) { debuglog("isFilling: clearing"); this.isFilling = false; + this.isFillingDueToPropsUpdate = false; } if (this.fillRequestWhileRunning) { + const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate; this.fillRequestWhileRunning = false; - this.checkFillState(); + this.pendingFillDueToPropsUpdate = false; + this.checkFillState(0, refillDueToPropsUpdate); } }; diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index b1cc9c773d..d66319ca73 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { useStateToggle } from "../../../hooks/useStateToggle"; import AccessibleButton from "./AccessibleButton"; +import { Layout } from '../../../settings/Layout'; interface IProps { // An array of member events to summarise @@ -38,6 +39,8 @@ interface IProps { children: ReactNode[]; // Called when the event list expansion is toggled onToggle?(): void; + // The layout currently used + layout?: Layout; } const EventListSummary: React.FC = ({ @@ -48,6 +51,7 @@ const EventListSummary: React.FC = ({ startExpanded, summaryMembers = [], summaryText, + layout, }) => { const [expanded, toggleExpanded] = useStateToggle(startExpanded); @@ -63,7 +67,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +96,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -103,6 +107,7 @@ const EventListSummary: React.FC = ({ EventListSummary.defaultProps = { startExpanded: false, + layout: Layout.Group, }; export default EventListSummary; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..604e6c63b0 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,12 +25,15 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Layout } from '../../../settings/Layout'; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" summaryLength?: number; // The maximum number of avatars to display in the summary avatarsMaxLength?: number; + // The currently selected layout + layout: Layout; } interface IUserEvents { @@ -67,6 +70,7 @@ export default class MemberEventListSummary extends React.Component { summaryLength: 1, threshold: 3, avatarsMaxLength: 5, + layout: Layout.Group, }; shouldComponentUpdate(nextProps) { @@ -453,6 +457,7 @@ export default class MemberEventListSummary extends React.Component { startExpanded={this.props.startExpanded} children={this.props.children} summaryMembers={[...latestUserAvatarMember.values()]} + layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />; } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index c81055bfb7..2de66f897a 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; interface IProps { mxEvent: MatrixEvent; @@ -69,6 +70,18 @@ export default class CallEvent extends React.Component { this.setState({ callState: newState }); }; + private renderCallBackButton(text: string): JSX.Element { + return ( + + { text } + + ); + } + private renderContent(state: CallState | CustomCallState): JSX.Element { if (state === CallState.Ringing) { const silenceClass = classNames({ @@ -103,8 +116,18 @@ export default class CallEvent extends React.Component { } if (state === CallState.Ended) { const hangupReason = this.props.callEventGrouper.hangupReason; + const gotRejected = this.props.callEventGrouper.gotRejected; + const rejectParty = this.props.callEventGrouper.rejectParty; - if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) { + if (gotRejected) { + const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty; + return ( +
    + { weDeclinedCall ? _t("You declined this call") : _t("They declined this call") } + { this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) } +
    + ); + } else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( @@ -116,6 +139,13 @@ export default class CallEvent extends React.Component { { _t("This call has ended") } ); + } else if (hangupReason === CallErrorCode.InviteTimeout) { + return ( +
    + { _t("They didn't pick up") } + { this.renderCallBackButton(_t("Call again")) } +
    + ); } let reason; @@ -133,8 +163,6 @@ export default class CallEvent extends React.Component { // (as opposed to an error code they gave but we don't know about, // in which case we show the error code) reason = _t("An unknown error occurred"); - } else if (hangupReason === CallErrorCode.InviteTimeout) { - reason = _t("No answer"); } else if (hangupReason === CallErrorCode.UserBusy) { reason = _t("The user you called is busy."); } else { @@ -163,13 +191,7 @@ export default class CallEvent extends React.Component { return (
    { _t("You missed this call") } - - { _t("Call back") } - + { this.renderCallBackButton(_t("Call back")) }
    ); } @@ -186,11 +208,17 @@ export default class CallEvent extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); const isVoice = this.props.callEventGrouper.isVoice; const callType = isVoice ? _t("Voice call") : _t("Video call"); - const content = this.renderContent(this.state.callState); + const callState = this.state.callState; + const hangupReason = this.props.callEventGrouper.hangupReason; + const content = this.renderContent(callState); const className = classNames({ mx_CallEvent: true, mx_CallEvent_voice: isVoice, mx_CallEvent_video: !isVoice, + mx_CallEvent_missed: ( + callState === CustomCallState.Missed || + (callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout) + ), }); return ( diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 87876e685f..884d004551 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -896,6 +896,7 @@ export default class EventTile extends React.Component { mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', + mx_EventTile_noSender: this.props.hideSender, }); // If the tile is in the Sending state, don't speak the message. @@ -1165,8 +1166,9 @@ export default class EventTile extends React.Component { /> { keyRequestInfo } { actionBar } + { this.props.layout === Layout.IRC && (reactionsRow) } - { reactionsRow } + { this.props.layout !== Layout.IRC && (reactionsRow) } { msgOption } ) ); diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 895d9773e4..6261b9965f 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -146,7 +146,7 @@ export default class CallPreview extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); - window.addEventListener("resize", this.snap); + window.addEventListener("resize", this.onResize); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -156,7 +156,7 @@ export default class CallPreview extends React.Component { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); - window.removeEventListener("resize", this.snap); + window.removeEventListener("resize", this.onResize); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -164,6 +164,10 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } + private onResize = (): void => { + this.snap(false); + }; + private animationCallback = () => { // If the PiP isn't being dragged and there is only a tiny difference in // the desiredTranslation and translation, quit the animationCallback @@ -207,7 +211,7 @@ export default class CallPreview extends React.Component { } } - private snap = () => { + private snap(animate?: boolean): void { const translationX = this.desiredTranslationX; const translationY = this.desiredTranslationY; // We subtract the PiP size from the window size in order to calculate @@ -236,10 +240,17 @@ export default class CallPreview extends React.Component { this.desiredTranslationY = PADDING.top; } - // We start animating here because we want the PiP to move when we're - // resizing the window - this.scheduledUpdate.mark(); - }; + if (animate) { + // We start animating here because we want the PiP to move when we're + // resizing the window + this.scheduledUpdate.mark(); + } else { + this.setState({ + translationX: this.desiredTranslationX, + translationY: this.desiredTranslationY, + }); + } + } private onRoomViewStoreUpdate = () => { if (RoomViewStore.getRoomId() === this.state.roomId) return; @@ -310,7 +321,7 @@ export default class CallPreview extends React.Component { private onEndMoving = () => { this.moving = false; - this.snap(); + this.snap(true); }; public render() { @@ -333,6 +344,7 @@ export default class CallPreview extends React.Component { secondaryCall={this.state.secondaryCall} pipMode={true} onMouseDownOnHeader={this.onStartMoving} + onResize={this.onResize} /> ); diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 95cc5ee3e3..03a9e89bd7 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -16,7 +16,7 @@ limitations under the License. import classnames from 'classnames'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; -import React, { createRef } from 'react'; +import React from 'react'; import SettingsStore from "../../../settings/SettingsStore"; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; @@ -48,7 +48,7 @@ interface IState { @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.Component { - private element = createRef(); + private element: HTMLVideoElement; constructor(props: IProps) { super(props); @@ -66,10 +66,10 @@ export default class VideoFeed extends React.Component { componentWillUnmount() { this.updateFeed(this.props.feed, null); - this.element.current?.removeEventListener('resize', this.onResize); } componentDidUpdate(prevProps: IProps) { + this.element?.addEventListener('resize', this.onResize); this.updateFeed(prevProps.feed, this.props.feed); } @@ -80,6 +80,13 @@ export default class VideoFeed extends React.Component { }; } + private setElementRef = (element: HTMLVideoElement): void => { + if (!element) element.removeEventListener('resize', this.onResize); + + this.element = element; + element.addEventListener('resize', this.onResize); + }; + private updateFeed(oldFeed: CallFeed, newFeed: CallFeed) { if (oldFeed === newFeed) return; @@ -94,7 +101,7 @@ export default class VideoFeed extends React.Component { } private playMedia() { - const element = this.element.current; + const element = this.element; if (!element) return; // We play audio in AudioFeed, not here element.muted = true; @@ -117,7 +124,7 @@ export default class VideoFeed extends React.Component { } private stopMedia() { - const element = this.element.current; + const element = this.element; if (!element) return; element.pause(); @@ -175,7 +182,7 @@ export default class VideoFeed extends React.Component { ); } else { return ( -