Voice rooms prototype (#8084)
* Add voice room labs flag Signed-off-by: Robin Townsend <robin@robin.town> * Add more widget actions for interacting with Jitsi Signed-off-by: Robin Townsend <robin@robin.town> * Factor out a more generic Jitsi creation utility Signed-off-by: Robin Townsend <robin@robin.town> * Add utilities for managing voice channels Signed-off-by: Robin Townsend <robin@robin.town> * Enable creation of voice rooms Signed-off-by: Robin Townsend <robin@robin.town> * Force a maximized view of voice channel widgets Signed-off-by: Robin Townsend <robin@robin.town> * Add voice channel store Signed-off-by: Robin Townsend <robin@robin.town> * Factor out a more generic FacePile Signed-off-by: Robin Townsend <robin@robin.town> * Implement room tile changes for voice rooms Signed-off-by: Robin Townsend <robin@robin.town> * Add interactive radio component to the left panel Signed-off-by: Robin Townsend <robin@robin.town> * Test voice rooms Signed-off-by: Robin Townsend <robin@robin.town> * Update name of call room type Signed-off-by: Robin Townsend <robin@robin.town> * Clarify that voice rooms are under development Signed-off-by: Robin Townsend <robin@robin.town> * Use readonly Signed-off-by: Robin Townsend <robin@robin.town> * Move acks to the end of handlers Signed-off-by: Robin Townsend <robin@robin.town> * Add comment about avatar URLs coming from Jitsi Signed-off-by: Robin Townsend <robin@robin.town> * Don't use unicode ellipses for translation reasons? Signed-off-by: Robin Townsend <robin@robin.town> * Fix tests Signed-off-by: Robin Townsend <robin@robin.town> * Fix tests, again Signed-off-by: Robin Townsend <robin@robin.town> * Remove unnecessary export Signed-off-by: Robin Townsend <robin@robin.town> * Ack Jitsi events when we wait for them Signed-off-by: Robin Townsend <robin@robin.town>
This commit is contained in:
parent
f416a970ca
commit
cfabcdda35
32 changed files with 1302 additions and 238 deletions
|
@ -25,12 +25,15 @@ import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleBu
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
|
@ -50,12 +53,19 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import VoiceChannelStore, { VoiceChannelEvent, IJitsiParticipant } from "../../../stores/VoiceChannelStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
|
||||
enum VoiceConnectionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
|
@ -70,6 +80,8 @@ interface IState {
|
|||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
messagePreview?: string;
|
||||
voiceConnectionState: VoiceConnectionState;
|
||||
voiceParticipants: IJitsiParticipant[];
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
||||
|
@ -88,6 +100,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
private isVoiceRoom: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -96,14 +109,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
voiceConnectionState: VoiceChannelStore.instance.roomId === this.props.room.roomId ?
|
||||
VoiceConnectionState.Connected : VoiceConnectionState.Disconnected,
|
||||
voiceParticipants: [],
|
||||
};
|
||||
this.generatePreview();
|
||||
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.isVoiceRoom = SettingsStore.getValue("feature_voice_rooms") && this.props.room.isCallRoom();
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room: Room) => {
|
||||
|
@ -238,7 +254,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onTileClick = (ev: React.KeyboardEvent) => {
|
||||
private onTileClick = async (ev: React.KeyboardEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
|
@ -252,6 +268,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
metricsTrigger: "RoomList",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
|
||||
// Connect to the voice channel if this is a voice room
|
||||
if (this.isVoiceRoom && this.state.voiceConnectionState === VoiceConnectionState.Disconnected) {
|
||||
await this.connectVoice();
|
||||
}
|
||||
};
|
||||
|
||||
private onActiveRoomUpdate = (isActive: boolean) => {
|
||||
|
@ -576,6 +597,68 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private updateVoiceParticipants = (participants: IJitsiParticipant[]) => {
|
||||
this.setState({ voiceParticipants: participants });
|
||||
};
|
||||
|
||||
private renderVoiceChannel(): React.ReactElement {
|
||||
if (!this.state.voiceParticipants.length) return null;
|
||||
|
||||
const faces = this.state.voiceParticipants.map(p =>
|
||||
<BaseAvatar
|
||||
key={p.participantId}
|
||||
name={p.displayName ?? p.formattedDisplayName}
|
||||
idName={p.participantId}
|
||||
// This comes directly from Jitsi, so we shouldn't apply custom media routing to it
|
||||
url={p.avatarURL}
|
||||
width={24}
|
||||
height={24}
|
||||
/>,
|
||||
);
|
||||
|
||||
// TODO: The below "join" button will eventually show up on text rooms
|
||||
// with an active voice channel, but that isn't implemented yet
|
||||
return <div className="mx_RoomTile_voiceChannel">
|
||||
<FacePile faces={faces} overflow={false} />
|
||||
{ this.isVoiceRoom ? null : (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
className="mx_RoomTile_connectVoiceButton"
|
||||
onClick={this.connectVoice.bind(this)}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
private async connectVoice() {
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Connecting });
|
||||
// TODO: Actually wait for the widget to be ready, instead of guessing.
|
||||
// This hack is only in place until we find out for sure whether design
|
||||
// wants the room view to open when connecting voice, or if this should
|
||||
// somehow connect in the background. Until then, it's not worth the
|
||||
// effort to solve this properly.
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
try {
|
||||
await VoiceChannelStore.instance.connect(this.props.room.roomId);
|
||||
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Connected });
|
||||
VoiceChannelStore.instance.once(VoiceChannelEvent.Disconnect, () => {
|
||||
this.setState({
|
||||
voiceConnectionState: VoiceConnectionState.Disconnected,
|
||||
voiceParticipants: [],
|
||||
}),
|
||||
VoiceChannelStore.instance.off(VoiceChannelEvent.Participants, this.updateVoiceParticipants);
|
||||
});
|
||||
VoiceChannelStore.instance.on(VoiceChannelEvent.Participants, this.updateVoiceParticipants);
|
||||
} catch (e) {
|
||||
logger.error("Failed to connect voice", e);
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Disconnected });
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
|
@ -607,11 +690,39 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let messagePreview = null;
|
||||
if (this.showMessagePreview && this.state.messagePreview) {
|
||||
messagePreview = (
|
||||
let subtitle;
|
||||
if (this.isVoiceRoom) {
|
||||
switch (this.state.voiceConnectionState) {
|
||||
case VoiceConnectionState.Disconnected:
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle mx_RoomTile_voiceIndicator">
|
||||
{ _t("Voice room") }
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case VoiceConnectionState.Connecting:
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle mx_RoomTile_voiceIndicator">
|
||||
{ _t("Connecting...") }
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case VoiceConnectionState.Connected:
|
||||
subtitle = (
|
||||
<div
|
||||
className={
|
||||
"mx_RoomTile_subtitle mx_RoomTile_voiceIndicator " +
|
||||
"mx_RoomTile_voiceIndicator_active"
|
||||
}
|
||||
>
|
||||
{ _t("Connected") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<div
|
||||
className="mx_RoomTile_messagePreview"
|
||||
className="mx_RoomTile_subtitle"
|
||||
id={messagePreviewId(this.props.room.roomId)}
|
||||
title={this.state.messagePreview}
|
||||
>
|
||||
|
@ -620,21 +731,20 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile_name": true,
|
||||
"mx_RoomTile_nameWithPreview": !!messagePreview,
|
||||
"mx_RoomTile_nameHasUnreadEvents": this.notificationState.isUnread,
|
||||
const titleClasses = classNames({
|
||||
"mx_RoomTile_title": true,
|
||||
"mx_RoomTile_titleWithSubtitle": !!subtitle,
|
||||
"mx_RoomTile_titleHasUnreadEvents": this.notificationState.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
const titleContainer = this.props.isMinimized ? null : (
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
<div title={name} className={titleClasses} tabIndex={-1} dir="auto">
|
||||
{ name }
|
||||
</div>
|
||||
{ messagePreview }
|
||||
{ subtitle }
|
||||
</div>
|
||||
);
|
||||
if (this.props.isMinimized) nameContainer = null;
|
||||
|
||||
let ariaLabel = name;
|
||||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||
|
@ -690,10 +800,15 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
oobData={({ avatarUrl: roomProfile.avatarMxc })}
|
||||
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
|
||||
/>
|
||||
{ nameContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
</div>
|
||||
{ this.renderVoiceChannel() }
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue