Prepare for Element Call integration (#9224)

* Improve accessibility and testability of Tooltip

Adding a role to Tooltip was motivated by React Testing Library's
reliance on accessibility-related attributes to locate elements.

* Make the ReadyWatchingStore constructor safer

The ReadyWatchingStore constructor previously had a chance to
immediately call onReady, which was dangerous because it was potentially
calling the derived class's onReady at a point when the derived class
hadn't even finished construction yet. In normal usage, I guess this
never was a problem, but it was causing some of the tests I was writing
to crash. This is solved by separating out the onReady call into a start
method.

* Rename 1:1 call components to 'LegacyCall'

to reflect the fact that they're slated for removal, and to not clash
with the new Call code.

* Refactor VideoChannelStore into Call and CallStore

Call is an abstract class that currently only has a Jitsi
implementation, but this will make it easy to later add an Element Call
implementation.

* Remove WidgetReady, ClientReady, and ForceHangupCall hacks

These are no longer used by the new Jitsi call implementation, and can
be removed.

* yarn i18n

* Delete call map entries instead of inserting nulls

* Allow multiple active calls and consolidate call listeners

* Fix a race condition when creating a video room

* Un-hardcode the media device fallback labels

* Apply misc code review fixes

* yarn i18n

* Disconnect from calls more politely on logout

* Fix some strict mode errors

* Fix another updateRoom race condition
This commit is contained in:
Robin 2022-08-30 15:13:39 -04:00 committed by GitHub
parent 50f6986f6c
commit 0d6a550c33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 2573 additions and 2157 deletions

View file

@ -27,7 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { UIFeature } from "../../../settings/UIFeature";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import CallViewForRoom from '../voip/CallViewForRoom';
import LegacyCallViewForRoom from '../voip/LegacyCallViewForRoom';
import { objectHasDiff } from "../../../utils/objects";
interface IProps {
@ -123,7 +123,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
render() {
const callView = (
<CallViewForRoom
<LegacyCallViewForRoom
roomId={this.props.room.roomId}
resizeNotifier={this.props.resizeNotifier}
showApps={this.props.showApps}

View file

@ -47,7 +47,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge";
import CallEventGrouper from "../../structures/CallEventGrouper";
import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from '../../../dispatcher/actions';
import PlatformPeg from '../../../PlatformPeg';
@ -200,8 +200,8 @@ interface IProps {
// Helper to build permalinks for the room
permalinkCreator?: RoomPermalinkCreator;
// CallEventGrouper for this event
callEventGrouper?: CallEventGrouper;
// LegacyCallEventGrouper for this event
callEventGrouper?: LegacyCallEventGrouper;
// Symbol of the root node
as?: string;

View file

@ -19,11 +19,11 @@ import React, { createRef } from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import type { Call } from "../../../models/Call";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
@ -45,8 +45,9 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import VideoRoomSummary from "./VideoRoomSummary";
import { RoomTileCallSummary } from "./RoomTileCallSummary";
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
interface IProps {
room: Room;
@ -61,6 +62,7 @@ interface IState {
selected: boolean;
notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect;
call: Call | null;
messagePreview?: string;
}
@ -79,7 +81,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
private roomTileRef = createRef<HTMLDivElement>();
private notificationState: NotificationState;
private roomProps: RoomEchoChamber;
private isVideoRoom: boolean;
constructor(props: IProps) {
super(props);
@ -88,6 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
call: CallStore.instance.get(this.props.room.roomId),
// generatePreview() will return nothing if the user has previews disabled
messagePreview: "",
};
@ -95,7 +97,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isElementVideoRoom();
}
private onRoomNameUpdate = (room: Room) => {
@ -154,6 +155,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.props.room.on(RoomEvent.Name, this.onRoomNameUpdate);
CallStore.instance.on(CallStoreEvent.Call, this.onCallChanged);
// Recalculate the call for this room, since it could've changed between
// construction and mounting
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
}
public componentWillUnmount() {
@ -166,6 +172,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
}
private onAction = (payload: ActionPayload) => {
@ -185,6 +192,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
};
private onCallChanged = (call: Call, roomId: string) => {
if (roomId === this.props.room?.roomId) this.setState({ call });
};
private async generatePreview() {
if (!this.showMessagePreview) {
return null;
@ -362,10 +373,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
let subtitle;
if (this.isVideoRoom) {
if (this.state.call) {
subtitle = (
<div className="mx_RoomTile_subtitle">
<VideoRoomSummary room={this.props.room} />
<RoomTileCallSummary call={this.state.call} />
</div>
);
} else if (this.showMessagePreview && this.state.messagePreview) {

View file

@ -16,66 +16,56 @@ limitations under the License.
import React, { FC } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import type { Call } from "../../../models/Call";
import { _t, TranslatedString } from "../../../languageHandler";
import {
ConnectionState,
useConnectionState,
useConnectedMembers,
useJitsiParticipants,
} from "../../../utils/VideoChannelUtils";
import { useConnectionState, useParticipants } from "../../../hooks/useCall";
import { ConnectionState } from "../../../models/Call";
interface IProps {
room: Room;
interface Props {
call: Call;
}
const VideoRoomSummary: FC<IProps> = ({ room }) => {
const connectionState = useConnectionState(room);
const videoMembers = useConnectedMembers(room, connectionState === ConnectionState.Connected);
const jitsiParticipants = useJitsiParticipants(room);
export const RoomTileCallSummary: FC<Props> = ({ call }) => {
const connectionState = useConnectionState(call);
const participants = useParticipants(call);
let indicator: TranslatedString;
let text: TranslatedString;
let active: boolean;
let participantCount: number;
switch (connectionState) {
case ConnectionState.Disconnected:
indicator = _t("Video");
text = _t("Video");
active = false;
participantCount = videoMembers.size;
break;
case ConnectionState.Connecting:
indicator = _t("Joining…");
text = _t("Joining…");
active = true;
participantCount = videoMembers.size;
break;
case ConnectionState.Connected:
indicator = _t("Joined");
case ConnectionState.Disconnecting:
text = _t("Joined");
active = true;
participantCount = jitsiParticipants.length;
break;
}
return <span className="mx_VideoRoomSummary">
return <span className="mx_RoomTileCallSummary">
<span
className={classNames(
"mx_VideoRoomSummary_indicator",
{ "mx_VideoRoomSummary_indicator_active": active },
"mx_RoomTileCallSummary_text",
{ "mx_RoomTileCallSummary_text_active": active },
)}
>
{ indicator }
{ text }
</span>
{ participantCount ? <>
{ participants.size ? <>
{ " · " }
<span
className="mx_VideoRoomSummary_participants"
aria-label={_t("%(count)s participants", { count: participantCount })}
className="mx_RoomTileCallSummary_participants"
aria-label={_t("%(count)s participants", { count: participants.size })}
>
{ participantCount }
{ participants.size }
</span>
</> : null }
</span>;
};
export default VideoRoomSummary;

View file

@ -26,7 +26,7 @@ import DateSeparator from "../messages/DateSeparator";
import EventTile from "./EventTile";
import { shouldFormContinuation } from "../../structures/MessagePanel";
import { wantsDateSeparator } from "../../../DateUtils";
import CallEventGrouper, { buildCallEventGroupers } from "../../structures/CallEventGrouper";
import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "../../structures/LegacyCallEventGrouper";
import { haveRendererForEvent } from "../../../events/EventTileFactory";
interface IProps {
@ -44,17 +44,17 @@ export default class SearchResultTile extends React.Component<IProps> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
// A map of <callId, CallEventGrouper>
private callEventGroupers = new Map<string, CallEventGrouper>();
// A map of <callId, LegacyCallEventGrouper>
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
constructor(props, context) {
super(props, context);
this.buildCallEventGroupers(this.props.searchResult.context.getTimeline());
this.buildLegacyCallEventGroupers(this.props.searchResult.context.getTimeline());
}
private buildCallEventGroupers(events?: MatrixEvent[]): void {
this.callEventGroupers = buildCallEventGroupers(this.callEventGroupers, events);
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events);
}
public render() {