Update video rooms to new design specs (#8207)
* Remove radio component * "Voice room" → "video room" * Remove interactivity from video room tiles * Update connection state when joining via widget * Simplify room header buttons for video rooms * Split out video room creation into a separate menu option * Simplify room options for video rooms * Update video room tile layout * Tell the Jitsi widget whether it's a video channel * Update tests * "Voice" → "video" in more places * Fix tests * Re-add frame to immersive Jitsi widgets * Comment ack * Make updateDevices more readable * Type FacePile
This commit is contained in:
@@ -36,6 +36,7 @@ import dis from './dispatcher/dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import Modal from './Modal';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import VideoChannelStore from "./stores/VideoChannelStore";
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import { sendLoginRequest } from "./Login";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
@@ -796,6 +797,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.instance.start();
|
||||
CallHandler.instance.start();
|
||||
if (SettingsStore.getValue("feature_video_rooms")) VideoChannelStore.instance.start();
|
||||
|
||||
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
|
||||
// the thing just wastes CPU cycles, but should result in no actual functionality
|
||||
@@ -909,6 +911,7 @@ export function stopMatrixClient(unsetClient = true): void {
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
Presence.stop();
|
||||
if (SettingsStore.getValue("feature_video_rooms")) VideoChannelStore.instance.stop();
|
||||
ActiveWidgetStore.instance.stop();
|
||||
IntegrationManagers.sharedInstance().stopWatching();
|
||||
Mjolnir.sharedInstance().stop();
|
||||
|
||||
@@ -41,7 +41,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import IndicatorScrollbar from "./IndicatorScrollbar";
|
||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import VoiceChannelRadio from "../views/voip/VoiceChannelRadio";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
@@ -441,7 +440,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
{ roomList }
|
||||
</div>
|
||||
</div>
|
||||
{ SettingsStore.getValue("feature_voice_rooms") && <VoiceChannelRadio /> }
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
@@ -677,7 +678,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
break;
|
||||
}
|
||||
case 'view_create_room':
|
||||
this.createRoom(payload.public, payload.defaultName);
|
||||
this.createRoom(payload.public, payload.defaultName, payload.type);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
@@ -994,8 +995,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.setPage(PageType.LegacyGroupView);
|
||||
}
|
||||
|
||||
private async createRoom(defaultPublic = false, defaultName?: string) {
|
||||
private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType) {
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||
type,
|
||||
defaultPublic,
|
||||
defaultName,
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import { getVoiceChannel } from "../../utils/VoiceChannelUtils";
|
||||
import { getVideoChannel } from "../../utils/VideoChannelUtils";
|
||||
import AppTile from "../views/elements/AppTile";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
@@ -375,7 +375,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
};
|
||||
|
||||
private getMainSplitContentType = (room: Room) => {
|
||||
if (SettingsStore.getValue("feature_voice_rooms") && room.isCallRoom()) {
|
||||
if (SettingsStore.getValue("feature_video_rooms") && room.isCallRoom()) {
|
||||
return MainSplitContentType.Video;
|
||||
}
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
|
||||
@@ -2140,7 +2140,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
</>;
|
||||
break;
|
||||
case MainSplitContentType.Video: {
|
||||
const app = getVoiceChannel(this.state.room.roomId);
|
||||
const app = getVideoChannel(this.state.room.roomId);
|
||||
if (!app) break;
|
||||
mainSplitContentClassName = "mx_MainSplit_video";
|
||||
mainSplitBody = <AppTile
|
||||
@@ -2157,19 +2157,32 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName);
|
||||
|
||||
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
||||
let onCallPlaced = this.onCallPlaced;
|
||||
let onAppsClick = this.onAppsClick;
|
||||
let onForgetClick = this.onForgetClick;
|
||||
let onSearchClick = this.onSearchClick;
|
||||
if (this.state.mainSplitContentType !== MainSplitContentType.Timeline) {
|
||||
// Disable phase buttons and action button to have a simplified header
|
||||
// and enable (not disable) the RightPanelPhases.Timeline button
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
];
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
|
||||
// Simplify the header for other main split types
|
||||
switch (this.state.mainSplitContentType) {
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
];
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
break;
|
||||
case MainSplitContentType.Video:
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
];
|
||||
onCallPlaced = null;
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -2189,7 +2202,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
|
||||
appsShown={this.state.showApps}
|
||||
onCallPlaced={this.onCallPlaced}
|
||||
onCallPlaced={onCallPlaced}
|
||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
@@ -29,6 +29,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
||||
import { useFeatureEnabled } from "../../hooks/useSettings";
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||
@@ -57,7 +58,7 @@ import {
|
||||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import { RoomFacePile } from "../views/elements/FacePile";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {
|
||||
AddExistingToSpace,
|
||||
defaultDmsRenderer,
|
||||
@@ -297,7 +298,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
||||
</div>
|
||||
}
|
||||
</RoomTopic>
|
||||
{ space.getJoinRule() === "public" && <RoomFacePile room={space} /> }
|
||||
{ space.getJoinRule() === "public" && <FacePile room={space} /> }
|
||||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
@@ -309,6 +310,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
||||
const canCreateSpace = shouldShowComponent(UIComponent.CreateSpaces);
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
@@ -322,20 +324,35 @@ const SpaceLandingAddButton = ({ space }) => {
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ canCreateRoom && <IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
{ canCreateRoom && <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
PosthogTrackers.trackInteraction("WebSpaceHomeCreateRoomButton", e);
|
||||
if (await showCreateNewRoom(space)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/> }
|
||||
PosthogTrackers.trackInteraction("WebSpaceHomeCreateRoomButton", e);
|
||||
if (await showCreateNewRoom(space)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
if (await showCreateNewRoom(space, RoomType.UnstableCall)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/> }
|
||||
</> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
@@ -437,7 +454,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
||||
<div className="mx_SpaceRoomView_landing_infoBar">
|
||||
<SpaceInfo space={space} />
|
||||
<div className="mx_SpaceRoomView_landing_infoBar_interactive">
|
||||
<RoomFacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import Modal from "../../../Modal";
|
||||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
@@ -105,6 +105,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||
}
|
||||
|
||||
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isCallRoom();
|
||||
|
||||
let inviteOption: JSX.Element;
|
||||
if (room.canInvite(cli.getUserId()) && !isDm) {
|
||||
@@ -233,11 +234,27 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||
/>;
|
||||
}
|
||||
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
let filesOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
filesOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Files")}
|
||||
iconClassName="mx_RoomTile_iconFiles"
|
||||
/>;
|
||||
}
|
||||
|
||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
let pinsOption: JSX.Element;
|
||||
if (pinningEnabled) {
|
||||
if (pinningEnabled && !isVideoRoom) {
|
||||
pinsOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
@@ -256,6 +273,37 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||
</IconizedContextMenuOption>;
|
||||
}
|
||||
|
||||
let widgetsOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
widgetsOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Widgets")}
|
||||
iconClassName="mx_RoomTile_iconWidgets"
|
||||
/>;
|
||||
}
|
||||
|
||||
let exportChatOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
exportChatOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Export room dialog', '', ExportDialog, { room });
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Export chat")}
|
||||
iconClassName="mx_RoomTile_iconExport"
|
||||
/>;
|
||||
}
|
||||
|
||||
const onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -295,35 +343,9 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||
{ notificationOption }
|
||||
{ favouriteOption }
|
||||
{ peopleOption }
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Files")}
|
||||
iconClassName="mx_RoomTile_iconFiles"
|
||||
/>
|
||||
|
||||
{ filesOption }
|
||||
{ pinsOption }
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Widgets")}
|
||||
iconClassName="mx_RoomTile_iconWidgets"
|
||||
/>
|
||||
|
||||
{ widgetsOption }
|
||||
{ lowPriorityOption }
|
||||
{ copyLinkOption }
|
||||
|
||||
@@ -343,17 +365,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||
iconClassName="mx_RoomTile_iconSettings"
|
||||
/>
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Export room dialog', '', ExportDialog, { room });
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Export chat")}
|
||||
iconClassName="mx_RoomTile_iconExport"
|
||||
/>
|
||||
{ exportChatOption }
|
||||
|
||||
{ SettingsStore.getValue("developerMode") && <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
|
||||
@@ -21,15 +21,12 @@ import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import withValidation, { IFieldState } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Heading from "../typography/Heading";
|
||||
import Field from "../elements/Field";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
@@ -40,6 +37,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
interface IProps {
|
||||
type?: RoomType;
|
||||
defaultPublic?: boolean;
|
||||
defaultName?: string;
|
||||
parentSpace?: Room;
|
||||
@@ -48,7 +46,6 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
type?: RoomType;
|
||||
joinRule: JoinRule;
|
||||
isPublic: boolean;
|
||||
isEncrypted: boolean;
|
||||
@@ -80,7 +77,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
type: null,
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(),
|
||||
joinRule,
|
||||
@@ -100,7 +96,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
private roomCreateOptions() {
|
||||
const opts: IOpts = {};
|
||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||
opts.roomType = this.state.type;
|
||||
opts.roomType = this.props.type;
|
||||
createOpts.name = this.state.name;
|
||||
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
@@ -180,10 +176,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onTypeChange = (type: RoomType | "text") => {
|
||||
this.setState({ type: type === "text" ? null : type });
|
||||
};
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
@@ -229,6 +221,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
});
|
||||
|
||||
render() {
|
||||
const isVideoRoom = this.props.type === RoomType.UnstableCall;
|
||||
|
||||
let aliasField;
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
@@ -319,8 +313,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
);
|
||||
}
|
||||
|
||||
let title = _t("Create a room");
|
||||
if (!this.props.parentSpace) {
|
||||
let title;
|
||||
if (isVideoRoom) {
|
||||
title = _t("Create a video room");
|
||||
} else if (this.props.parentSpace) {
|
||||
title = _t("Create a room");
|
||||
} else {
|
||||
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||
}
|
||||
|
||||
@@ -333,20 +331,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
{ SettingsStore.getValue("feature_voice_rooms") ? <>
|
||||
<Heading size="h3">{ _t("Room type") }</Heading>
|
||||
<StyledRadioGroup
|
||||
name="type"
|
||||
value={this.state.type ?? "text"}
|
||||
onChange={this.onTypeChange}
|
||||
definitions={[
|
||||
{ value: "text", label: _t("Text room") },
|
||||
{ value: RoomType.UnstableCall, label: _t("Voice & video room") },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Heading size="h3">{ _t("Room details") }</Heading>
|
||||
</> : null }
|
||||
<Field
|
||||
ref={this.nameField}
|
||||
label={_t('Name')}
|
||||
@@ -390,7 +374,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={_t('Create Room')}
|
||||
<DialogButtons primaryButton={isVideoRoom ? _t('Create video room') : _t('Create room')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes, ReactNode, useContext } from "react";
|
||||
import React, { FC, HTMLAttributes, ReactNode, useContext } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { sortBy } from "lodash";
|
||||
@@ -26,48 +26,17 @@ import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import { useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
faces: ReactNode[];
|
||||
overflow: boolean;
|
||||
tooltip?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const FacePile = ({ faces, overflow, tooltip, children, ...props }: IProps) => {
|
||||
const pileContents = <>
|
||||
{ overflow ? <span className="mx_FacePile_more" /> : null }
|
||||
{ faces }
|
||||
</>;
|
||||
|
||||
return <div {...props} className="mx_FacePile">
|
||||
{ tooltip ? (
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ pileContents }
|
||||
</TextWithTooltip>
|
||||
) : (
|
||||
<div className="mx_FacePile_faces">
|
||||
{ pileContents }
|
||||
</div>
|
||||
) }
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
||||
const DEFAULT_NUM_FACES = 5;
|
||||
|
||||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||
|
||||
interface IRoomProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
room: Room;
|
||||
onlyKnownUsers?: boolean;
|
||||
numShown?: number;
|
||||
}
|
||||
|
||||
export const RoomFacePile = (
|
||||
{ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IRoomProps,
|
||||
) => {
|
||||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||
|
||||
const FacePile: FC<IProps> = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const isJoined = room.getMyMembership() === "join";
|
||||
let members = useRoomMembers(room);
|
||||
@@ -89,8 +58,6 @@ export const RoomFacePile = (
|
||||
// We reverse the order of the shown faces in CSS to simplify their visual overlap,
|
||||
// reverse members in tooltip order to make the order between the two match up.
|
||||
const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", ");
|
||||
const faces = shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} />);
|
||||
|
||||
let tooltip: ReactNode;
|
||||
if (props.onClick) {
|
||||
@@ -123,9 +90,16 @@ export const RoomFacePile = (
|
||||
}
|
||||
}
|
||||
|
||||
return <FacePile faces={faces} overflow={members.length > numShown} tooltip={tooltip}>
|
||||
return <div {...props} className="mx_FacePile">
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
|
||||
{ shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" />) }
|
||||
</TextWithTooltip>
|
||||
{ onlyKnownUsers && <span className="mx_FacePile_summary">
|
||||
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
||||
</span> }
|
||||
</FacePile>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
||||
@@ -42,7 +42,7 @@ import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "./PinnedMessagesCard";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
@@ -269,6 +269,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isCallRoom();
|
||||
|
||||
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
|
||||
const header = <React.Fragment>
|
||||
@@ -297,7 +298,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
</React.Fragment>;
|
||||
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
@@ -308,18 +309,19 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
{ memberCount }
|
||||
</span>
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ _t("Files") }
|
||||
</Button>
|
||||
{ pinningEnabled && <Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{ _t("Pinned") }
|
||||
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
|
||||
{ pinCount }
|
||||
</span> }
|
||||
</Button> }
|
||||
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ pinningEnabled && !isVideoRoom &&
|
||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{ _t("Pinned") }
|
||||
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
|
||||
{ pinCount }
|
||||
</span> }
|
||||
</Button> }
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ _t("Export chat") }
|
||||
</Button>
|
||||
</Button> }
|
||||
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
|
||||
{ _t("Share room") }
|
||||
</Button>
|
||||
@@ -330,6 +332,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||
|
||||
{
|
||||
SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& !isVideoRoom
|
||||
&& shouldShowComponent(UIComponent.AddIntegrations)
|
||||
&& <AppsSection room={room} />
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.inRoom &&
|
||||
this.props.onCallPlaced &&
|
||||
!this.context.tombstone &&
|
||||
SettingsStore.getValue("showCallButtonsInComposer")
|
||||
) {
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
@@ -222,8 +223,8 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
||||
showCreateRoom
|
||||
? (<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -235,6 +236,19 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showCreateNewRoom(activeSpace, RoomType.UnstableCall);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
@@ -254,17 +268,32 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
||||
</IconizedContextMenuOptionList>;
|
||||
} else if (menuDisplayed) {
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
{ showCreateRoom && <IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
/> }
|
||||
{ showCreateRoom && <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: RoomType.UnstableCall,
|
||||
});
|
||||
}}
|
||||
/> }
|
||||
</> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
|
||||
@@ -16,11 +16,12 @@ limitations under the License.
|
||||
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useEventEmitterState, useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
|
||||
@@ -127,6 +128,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
|
||||
return SpaceStore.instance.allRoomsInHome;
|
||||
});
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const pendingActions = usePendingActions();
|
||||
|
||||
const filterCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||
@@ -195,19 +197,31 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
/>;
|
||||
}
|
||||
|
||||
let createNewRoomOption: JSX.Element;
|
||||
let newRoomOptions: JSX.Element;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
|
||||
createNewRoomOption = <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
label={_t("Create new room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
newRoomOptions = <>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
label={_t("New room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
label={_t("New video room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace, RoomType.UnstableCall);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/> }
|
||||
</>;
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
@@ -217,7 +231,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
{ createNewRoomOption }
|
||||
{ newRoomOptions }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
@@ -262,12 +276,11 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
} else if (plusMenuDisplayed) {
|
||||
let startChatOpt: JSX.Element;
|
||||
let createRoomOpt: JSX.Element;
|
||||
let newRoomOpts: JSX.Element;
|
||||
let joinRoomOpt: JSX.Element;
|
||||
|
||||
if (canCreateRooms) {
|
||||
startChatOpt = (
|
||||
newRoomOpts = <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
@@ -278,11 +291,9 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
createRoomOpt = (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -291,7 +302,20 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: RoomType.UnstableCall,
|
||||
});
|
||||
closePlusMenu();
|
||||
}}
|
||||
/> }
|
||||
</>;
|
||||
}
|
||||
if (canExploreRooms) {
|
||||
joinRoomOpt = (
|
||||
@@ -314,8 +338,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ startChatOpt }
|
||||
{ createRoomOpt }
|
||||
{ newRoomOpts }
|
||||
{ joinRoomOpt }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
|
||||
@@ -33,10 +33,7 @@ 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 MemberAvatar from "../avatars/MemberAvatar";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
@@ -55,17 +52,16 @@ import IconizedContextMenu, {
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import VoiceChannelStore, { VoiceChannelEvent, IJitsiParticipant } from "../../../stores/VoiceChannelStore";
|
||||
import { getConnectedMembers } from "../../../utils/VoiceChannelUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../../stores/VideoChannelStore";
|
||||
import { getConnectedMembers } from "../../../utils/VideoChannelUtils";
|
||||
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 {
|
||||
enum VideoStatus {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
}
|
||||
|
||||
@@ -83,10 +79,10 @@ interface IState {
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
messagePreview?: string;
|
||||
voiceConnectionState: VoiceConnectionState;
|
||||
// Active voice channel members, according to room state
|
||||
voiceMembers: RoomMember[];
|
||||
// Active voice channel members, according to Jitsi
|
||||
videoStatus: VideoStatus;
|
||||
// Active video channel members, according to room state
|
||||
videoMembers: RoomMember[];
|
||||
// Active video channel members, according to Jitsi
|
||||
jitsiParticipants: IJitsiParticipant[];
|
||||
}
|
||||
|
||||
@@ -106,27 +102,28 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
private isVoiceRoom: boolean;
|
||||
private isVideoRoom: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const videoConnected = VideoChannelStore.instance.roomId === this.props.room.roomId;
|
||||
|
||||
this.state = {
|
||||
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,
|
||||
voiceMembers: [],
|
||||
jitsiParticipants: [],
|
||||
videoStatus: videoConnected ? VideoStatus.Connected : VideoStatus.Disconnected,
|
||||
videoMembers: getConnectedMembers(this.props.room.currentState),
|
||||
jitsiParticipants: videoConnected ? VideoChannelStore.instance.participants : [],
|
||||
};
|
||||
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();
|
||||
this.isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isCallRoom();
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room: Room) => {
|
||||
@@ -165,8 +162,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
prevProps.room?.currentState?.off(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
prevProps.room?.currentState?.off(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
this.updateVideoStatus();
|
||||
prevProps.room?.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
}
|
||||
@@ -177,7 +175,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
if (this.state.selected) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
this.updateVoiceMembers();
|
||||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
@@ -188,7 +185,13 @@ 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);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoStatus);
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoStatus);
|
||||
if (VideoChannelStore.instance.roomId === this.props.room.roomId) {
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@@ -198,13 +201,16 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
this.props.room.currentState.off(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
this.props.room.currentState.off(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoStatus);
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoStatus);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
@@ -255,11 +261,6 @@ 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) => {
|
||||
@@ -584,87 +585,24 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
);
|
||||
}
|
||||
|
||||
private updateVoiceMembers = () => {
|
||||
this.setState({ voiceMembers: getConnectedMembers(this.props.room.currentState) });
|
||||
private updateVideoMembers = () => {
|
||||
this.setState({ videoMembers: getConnectedMembers(this.props.room.currentState) });
|
||||
};
|
||||
|
||||
private updateVideoStatus = () => {
|
||||
if (VideoChannelStore.instance.roomId === this.props.room?.roomId) {
|
||||
this.setState({ videoStatus: VideoStatus.Connected });
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
} else {
|
||||
this.setState({ videoStatus: VideoStatus.Disconnected });
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
};
|
||||
|
||||
private updateJitsiParticipants = (participants: IJitsiParticipant[]) => {
|
||||
this.setState({ jitsiParticipants: participants });
|
||||
};
|
||||
|
||||
private renderVoiceChannel(): React.ReactElement | null {
|
||||
let faces;
|
||||
if (this.state.voiceConnectionState === VoiceConnectionState.Connected) {
|
||||
faces = this.state.jitsiParticipants.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}
|
||||
/>,
|
||||
);
|
||||
} else if (this.state.voiceMembers.length) {
|
||||
faces = this.state.voiceMembers.map(m =>
|
||||
<MemberAvatar
|
||||
key={m.userId}
|
||||
member={m}
|
||||
width={24}
|
||||
height={24}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
const waitForConnect = VoiceChannelStore.instance.connect(this.props.room.roomId);
|
||||
// Participant data comes down the event channel quickly, so prepare in advance
|
||||
VoiceChannelStore.instance.on(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
try {
|
||||
await waitForConnect;
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Connected });
|
||||
|
||||
VoiceChannelStore.instance.once(VoiceChannelEvent.Disconnect, () => {
|
||||
this.setState({
|
||||
voiceConnectionState: VoiceConnectionState.Disconnected,
|
||||
jitsiParticipants: [],
|
||||
}),
|
||||
VoiceChannelStore.instance.off(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
});
|
||||
} catch (e) {
|
||||
// If it failed, clean up our advance preparations
|
||||
logger.error("Failed to connect voice", e);
|
||||
VoiceChannelStore.instance.off(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
@@ -692,34 +630,44 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
if (this.isVideoRoom) {
|
||||
let videoText: string;
|
||||
let videoActive: boolean;
|
||||
let participantCount: number;
|
||||
|
||||
switch (this.state.videoStatus) {
|
||||
case VideoStatus.Disconnected:
|
||||
videoText = _t("Video");
|
||||
videoActive = false;
|
||||
participantCount = this.state.videoMembers.length;
|
||||
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>
|
||||
);
|
||||
case VideoStatus.Connected:
|
||||
videoText = _t("Connected");
|
||||
videoActive = true;
|
||||
participantCount = this.state.jitsiParticipants.length;
|
||||
}
|
||||
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle">
|
||||
<span
|
||||
className={classNames({
|
||||
"mx_RoomTile_videoIndicator": true,
|
||||
"mx_RoomTile_videoIndicator_active": videoActive,
|
||||
})}
|
||||
>
|
||||
{ videoText }
|
||||
</span>
|
||||
{ participantCount ? <>
|
||||
{ " · " }
|
||||
<span
|
||||
className="mx_RoomTile_videoParticipants"
|
||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
||||
>
|
||||
{ participantCount }
|
||||
</span>
|
||||
</> : null }
|
||||
</div>
|
||||
);
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<div
|
||||
@@ -800,15 +748,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
displayBadge={this.props.isMinimized}
|
||||
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
|
||||
/>
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
</div>
|
||||
{ this.renderVoiceChannel() }
|
||||
</div>
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
</Button>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import VoiceChannelStore, { VoiceChannelEvent } from "../../../stores/VoiceChannelStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
const _VoiceChannelRadio: FC<{ roomId: string }> = ({ roomId }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const room = cli.getRoom(roomId);
|
||||
const store = VoiceChannelStore.instance;
|
||||
|
||||
const [audioMuted, setAudioMuted] = useState<boolean>(store.audioMuted);
|
||||
const [videoMuted, setVideoMuted] = useState<boolean>(store.videoMuted);
|
||||
|
||||
useEventEmitter(store, VoiceChannelEvent.MuteAudio, () => setAudioMuted(true));
|
||||
useEventEmitter(store, VoiceChannelEvent.UnmuteAudio, () => setAudioMuted(false));
|
||||
useEventEmitter(store, VoiceChannelEvent.MuteVideo, () => setVideoMuted(true));
|
||||
useEventEmitter(store, VoiceChannelEvent.UnmuteVideo, () => setVideoMuted(false));
|
||||
|
||||
return <div className="mx_VoiceChannelRadio">
|
||||
<div className="mx_VoiceChannelRadio_statusBar">
|
||||
<DecoratedRoomAvatar room={room} avatarSize={36} />
|
||||
<div className="mx_VoiceChannelRadio_titleContainer">
|
||||
<div className="mx_VoiceChannelRadio_status">{ _t("Connected") }</div>
|
||||
<div className="mx_VoiceChannelRadio_name">{ room.name }</div>
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_VoiceChannelRadio_disconnectButton"
|
||||
title={_t("Disconnect")}
|
||||
onClick={() => store.disconnect()}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_VoiceChannelRadio_controlBar">
|
||||
<AccessibleButton
|
||||
className={classNames({
|
||||
"mx_VoiceChannelRadio_videoButton": true,
|
||||
"mx_VoiceChannelRadio_button_active": !videoMuted,
|
||||
})}
|
||||
onClick={() => videoMuted ? store.unmuteVideo() : store.muteVideo()}
|
||||
>
|
||||
{ videoMuted ? _t("Video off") : _t("Video") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={classNames({
|
||||
"mx_VoiceChannelRadio_audioButton": true,
|
||||
"mx_VoiceChannelRadio_button_active": !audioMuted,
|
||||
})}
|
||||
onClick={() => audioMuted ? store.unmuteAudio() : store.muteAudio()}
|
||||
>
|
||||
{ audioMuted ? _t("Mic off") : _t("Mic") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const VoiceChannelRadio: FC<{}> = () => {
|
||||
const store = VoiceChannelStore.instance;
|
||||
|
||||
const [activeChannel, setActiveChannel] = useState<string>(VoiceChannelStore.instance.roomId);
|
||||
useEventEmitter(store, VoiceChannelEvent.Connect, () =>
|
||||
setActiveChannel(VoiceChannelStore.instance.roomId),
|
||||
);
|
||||
useEventEmitter(store, VoiceChannelEvent.Disconnect, () =>
|
||||
setActiveChannel(null),
|
||||
);
|
||||
|
||||
return activeChannel ? <_VoiceChannelRadio roomId={activeChannel} /> : null;
|
||||
};
|
||||
|
||||
export default VoiceChannelRadio;
|
||||
+5
-5
@@ -42,7 +42,7 @@ import { isJoinedOrNearlyJoined } from "./utils/membership";
|
||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
||||
import SpaceStore from "./stores/spaces/SpaceStore";
|
||||
import { makeSpaceParentEvent } from "./utils/space";
|
||||
import { VOICE_CHANNEL_MEMBER, addVoiceChannel } from "./utils/VoiceChannelUtils";
|
||||
import { VIDEO_CHANNEL_MEMBER, addVideoChannel } from "./utils/VideoChannelUtils";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
@@ -128,11 +128,11 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
||||
[RoomCreateTypeField]: opts.roomType,
|
||||
};
|
||||
|
||||
// In voice rooms, allow all users to send voice member updates
|
||||
// In video rooms, allow all users to send video member updates
|
||||
if (opts.roomType === RoomType.UnstableCall) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
[VOICE_CHANNEL_MEMBER]: 0,
|
||||
[VIDEO_CHANNEL_MEMBER]: 0,
|
||||
// Annoyingly, we have to reiterate all the defaults here
|
||||
[EventType.RoomName]: 50,
|
||||
[EventType.RoomAvatar]: 50,
|
||||
@@ -262,9 +262,9 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
||||
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
|
||||
}
|
||||
}).then(() => {
|
||||
// Set up voice rooms with a Jitsi widget
|
||||
// Set up video rooms with a Jitsi widget
|
||||
if (opts.roomType === RoomType.UnstableCall) {
|
||||
return addVoiceChannel(roomId, createOpts.name);
|
||||
return addVideoChannel(roomId, createOpts.name);
|
||||
}
|
||||
}).then(function() {
|
||||
// NB createRoom doesn't block on the client seeing the echo that the
|
||||
|
||||
+13
-16
@@ -868,7 +868,7 @@
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Threaded messaging": "Threaded messaging",
|
||||
"Custom user status messages": "Custom user status messages",
|
||||
"Voice & video rooms (under active development)": "Voice & video rooms (under active development)",
|
||||
"Video rooms (under active development)": "Video rooms (under active development)",
|
||||
"Render simple counters in room header": "Render simple counters in room header",
|
||||
"Multiple integration managers (requires manual setup)": "Multiple integration managers (requires manual setup)",
|
||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||
@@ -1002,12 +1002,6 @@
|
||||
"Your camera is turned off": "Your camera is turned off",
|
||||
"Your camera is still enabled": "Your camera is still enabled",
|
||||
"Dial": "Dial",
|
||||
"Connected": "Connected",
|
||||
"Disconnect": "Disconnect",
|
||||
"Video off": "Video off",
|
||||
"Video": "Video",
|
||||
"Mic off": "Mic off",
|
||||
"Mic": "Mic",
|
||||
"Dialpad": "Dialpad",
|
||||
"Mute the microphone": "Mute the microphone",
|
||||
"Unmute the microphone": "Unmute the microphone",
|
||||
@@ -1359,6 +1353,7 @@
|
||||
"The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.",
|
||||
"Disconnect identity server": "Disconnect identity server",
|
||||
"Disconnect from the identity server <idserver />?": "Disconnect from the identity server <idserver />?",
|
||||
"Disconnect": "Disconnect",
|
||||
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.",
|
||||
"You should:": "You should:",
|
||||
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "check your browser plugins for anything that might block the identity server (such as Privacy Badger)",
|
||||
@@ -1757,8 +1752,9 @@
|
||||
"Add people": "Add people",
|
||||
"Start chat": "Start chat",
|
||||
"Explore rooms": "Explore rooms",
|
||||
"Create new room": "Create new room",
|
||||
"New room": "New room",
|
||||
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
|
||||
"New video room": "New video room",
|
||||
"Add existing room": "Add existing room",
|
||||
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
|
||||
"Explore public rooms": "Explore public rooms",
|
||||
@@ -1851,9 +1847,10 @@
|
||||
"Low Priority": "Low Priority",
|
||||
"Copy room link": "Copy room link",
|
||||
"Leave": "Leave",
|
||||
"Join": "Join",
|
||||
"Voice room": "Voice room",
|
||||
"Connecting...": "Connecting...",
|
||||
"Video": "Video",
|
||||
"Connected": "Connected",
|
||||
"%(count)s participants|other": "%(count)s participants",
|
||||
"%(count)s participants|one": "1 participant",
|
||||
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
||||
"%(count)s unread messages including mentions.|one": "1 unread mention.",
|
||||
"%(count)s unread messages.|other": "%(count)s unread messages.",
|
||||
@@ -2207,6 +2204,7 @@
|
||||
"Application window": "Application window",
|
||||
"Share content": "Share content",
|
||||
"Backspace": "Backspace",
|
||||
"Join": "Join",
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
@@ -2420,20 +2418,18 @@
|
||||
"Enable end-to-end encryption": "Enable end-to-end encryption",
|
||||
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
|
||||
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
|
||||
"Create a video room": "Create a video room",
|
||||
"Create a room": "Create a room",
|
||||
"Create a public room": "Create a public room",
|
||||
"Create a private room": "Create a private room",
|
||||
"Room type": "Room type",
|
||||
"Text room": "Text room",
|
||||
"Voice & video room": "Voice & video room",
|
||||
"Room details": "Room details",
|
||||
"Topic (optional)": "Topic (optional)",
|
||||
"Room visibility": "Room visibility",
|
||||
"Private room (invite only)": "Private room (invite only)",
|
||||
"Public room": "Public room",
|
||||
"Visible to space members": "Visible to space members",
|
||||
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
|
||||
"Create Room": "Create Room",
|
||||
"Create video room": "Create video room",
|
||||
"Create room": "Create room",
|
||||
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
|
||||
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
|
||||
"Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
|
||||
@@ -3021,6 +3017,7 @@
|
||||
"Unable to look up room ID from server": "Unable to look up room ID from server",
|
||||
"Preview": "Preview",
|
||||
"View": "View",
|
||||
"Create new room": "Create new room",
|
||||
"No results for \"%(query)s\"": "No results for \"%(query)s\"",
|
||||
"Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.",
|
||||
"Find a room…": "Find a room…",
|
||||
|
||||
@@ -237,10 +237,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||
default: false,
|
||||
controller: new CustomStatusController(),
|
||||
},
|
||||
"feature_voice_rooms": {
|
||||
"feature_video_rooms": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Rooms,
|
||||
displayName: _td("Voice & video rooms (under active development)"),
|
||||
displayName: _td("Video rooms (under active development)"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
// Reload to ensure that the left panel etc. get remounted
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
|
||||
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "./ActiveWidgetStore";
|
||||
import {
|
||||
VIDEO_CHANNEL,
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
IVideoChannelMemberContent,
|
||||
getVideoChannel,
|
||||
} from "../utils/VideoChannelUtils";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
|
||||
export enum VideoChannelEvent {
|
||||
Connect = "connect",
|
||||
Disconnect = "disconnect",
|
||||
Participants = "participants",
|
||||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
avatarURL: string;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Holds information about the currently active video channel.
|
||||
*/
|
||||
export default class VideoChannelStore extends EventEmitter {
|
||||
private static _instance: VideoChannelStore;
|
||||
|
||||
public static get instance(): VideoChannelStore {
|
||||
if (!VideoChannelStore._instance) {
|
||||
VideoChannelStore._instance = new VideoChannelStore();
|
||||
}
|
||||
return VideoChannelStore._instance;
|
||||
}
|
||||
|
||||
private readonly cli = MatrixClientPeg.get();
|
||||
private activeChannel: ClientWidgetApi;
|
||||
private _roomId: string;
|
||||
private _participants: IJitsiParticipant[];
|
||||
|
||||
public get roomId(): string {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
public get participants(): IJitsiParticipant[] {
|
||||
return this._participants;
|
||||
}
|
||||
|
||||
public start = () => {
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate);
|
||||
};
|
||||
|
||||
public stop = () => {
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Update, this.onActiveWidgetUpdate);
|
||||
};
|
||||
|
||||
private setConnected = async (roomId: string) => {
|
||||
const jitsi = getVideoChannel(roomId);
|
||||
if (!jitsi) throw new Error(`No video channel in room ${roomId}`);
|
||||
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi));
|
||||
if (!messaging) throw new Error(`Failed to bind video channel in room ${roomId}`);
|
||||
|
||||
this.activeChannel = messaging;
|
||||
this._roomId = roomId;
|
||||
this._participants = [];
|
||||
|
||||
this.activeChannel.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.activeChannel.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
|
||||
this.emit(VideoChannelEvent.Connect);
|
||||
|
||||
// Tell others that we're connected, by adding our device to room state
|
||||
await this.updateDevices(devices => Array.from(new Set(devices).add(this.cli.getDeviceId())));
|
||||
};
|
||||
|
||||
private setDisconnected = async () => {
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
|
||||
this.activeChannel = null;
|
||||
this._participants = null;
|
||||
|
||||
try {
|
||||
// Tell others that we're disconnected, by removing our device from room state
|
||||
await this.updateDevices(devices => {
|
||||
const devicesSet = new Set(devices);
|
||||
devicesSet.delete(this.cli.getDeviceId());
|
||||
return Array.from(devicesSet);
|
||||
});
|
||||
} finally {
|
||||
// Save this for last, since updateDevices needs the room ID
|
||||
this._roomId = null;
|
||||
this.emit(VideoChannelEvent.Disconnect);
|
||||
}
|
||||
};
|
||||
|
||||
private ack = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
// Even if we don't have a reply to a given widget action, we still need
|
||||
// to give the widget API something to acknowledge receipt
|
||||
this.activeChannel.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
private updateDevices = async (fn: (devices: string[]) => string[]) => {
|
||||
if (!this.roomId) {
|
||||
logger.error("Tried to update devices while disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
const room = this.cli.getRoom(this.roomId);
|
||||
const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, this.cli.getUserId());
|
||||
const devices = devicesState?.getContent<IVideoChannelMemberContent>()?.devices ?? [];
|
||||
|
||||
await this.cli.sendStateEvent(
|
||||
this.roomId, VIDEO_CHANNEL_MEMBER, { devices: fn(devices) }, this.cli.getUserId(),
|
||||
);
|
||||
};
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.ack(ev);
|
||||
await this.setDisconnected();
|
||||
};
|
||||
|
||||
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._participants = ev.detail.data.participants as IJitsiParticipant[];
|
||||
this.emit(VideoChannelEvent.Participants, ev.detail.data.participants);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onActiveWidgetUpdate = async () => {
|
||||
if (this.activeChannel) {
|
||||
// We got disconnected from the previous video channel, so clean up
|
||||
await this.setDisconnected();
|
||||
}
|
||||
|
||||
// If the new active widget is a video channel, that means we joined
|
||||
if (ActiveWidgetStore.instance.getPersistentWidgetId() === VIDEO_CHANNEL) {
|
||||
await this.setConnected(ActiveWidgetStore.instance.getPersistentRoomId());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
|
||||
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
|
||||
import {
|
||||
VOICE_CHANNEL_MEMBER,
|
||||
IVoiceChannelMemberContent,
|
||||
getVoiceChannel,
|
||||
} from "../utils/VoiceChannelUtils";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
|
||||
export enum VoiceChannelEvent {
|
||||
Connect = "connect",
|
||||
Disconnect = "disconnect",
|
||||
Participants = "participants",
|
||||
MuteAudio = "mute_audio",
|
||||
UnmuteAudio = "unmute_audio",
|
||||
MuteVideo = "mute_video",
|
||||
UnmuteVideo = "unmute_video",
|
||||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
avatarURL: string;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Holds information about the currently active voice channel.
|
||||
*/
|
||||
export default class VoiceChannelStore extends EventEmitter {
|
||||
private static _instance: VoiceChannelStore;
|
||||
private static readonly TIMEOUT = 8000;
|
||||
|
||||
public static get instance(): VoiceChannelStore {
|
||||
if (!VoiceChannelStore._instance) {
|
||||
VoiceChannelStore._instance = new VoiceChannelStore();
|
||||
}
|
||||
return VoiceChannelStore._instance;
|
||||
}
|
||||
|
||||
private readonly cli = MatrixClientPeg.get();
|
||||
private activeChannel: ClientWidgetApi;
|
||||
private _roomId: string;
|
||||
private _participants: IJitsiParticipant[];
|
||||
private _audioMuted: boolean;
|
||||
private _videoMuted: boolean;
|
||||
|
||||
public get roomId(): string {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
public get participants(): IJitsiParticipant[] {
|
||||
return this._participants;
|
||||
}
|
||||
|
||||
public get audioMuted(): boolean {
|
||||
return this._audioMuted;
|
||||
}
|
||||
|
||||
public get videoMuted(): boolean {
|
||||
return this._videoMuted;
|
||||
}
|
||||
|
||||
public connect = async (roomId: string) => {
|
||||
if (this.activeChannel) await this.disconnect();
|
||||
|
||||
const jitsi = getVoiceChannel(roomId);
|
||||
if (!jitsi) throw new Error(`No voice channel in room ${roomId}`);
|
||||
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi));
|
||||
if (!messaging) throw new Error(`Failed to bind voice channel in room ${roomId}`);
|
||||
|
||||
this.activeChannel = messaging;
|
||||
this._roomId = roomId;
|
||||
|
||||
// Participant data and mute state will come down the event pipeline very quickly,
|
||||
// so prepare in advance
|
||||
messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
|
||||
// Actually perform the join
|
||||
const waitForJoin = this.waitForAction(ElementWidgetActions.JoinCall);
|
||||
messaging.transport.send(ElementWidgetActions.JoinCall, {});
|
||||
try {
|
||||
await waitForJoin;
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.activeChannel = null;
|
||||
this._roomId = null;
|
||||
|
||||
messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
this.emit(VoiceChannelEvent.Connect);
|
||||
|
||||
// Tell others that we're connected, by adding our device to room state
|
||||
await this.updateDevices(devices => Array.from(new Set(devices).add(this.cli.getDeviceId())));
|
||||
};
|
||||
|
||||
public disconnect = async () => {
|
||||
this.assertConnected();
|
||||
|
||||
const waitForHangup = this.waitForAction(ElementWidgetActions.HangupCall);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
await waitForHangup;
|
||||
|
||||
// onHangup cleans up for us
|
||||
};
|
||||
|
||||
public muteAudio = async () => {
|
||||
this.assertConnected();
|
||||
|
||||
const waitForMute = this.waitForAction(ElementWidgetActions.MuteAudio);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.MuteAudio, {});
|
||||
await waitForMute;
|
||||
};
|
||||
|
||||
public unmuteAudio = async () => {
|
||||
this.assertConnected();
|
||||
|
||||
const waitForUnmute = this.waitForAction(ElementWidgetActions.UnmuteAudio);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.UnmuteAudio, {});
|
||||
await waitForUnmute;
|
||||
};
|
||||
|
||||
public muteVideo = async () => {
|
||||
this.assertConnected();
|
||||
|
||||
const waitForMute = this.waitForAction(ElementWidgetActions.MuteVideo);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.MuteVideo, {});
|
||||
await waitForMute;
|
||||
};
|
||||
|
||||
public unmuteVideo = async () => {
|
||||
this.assertConnected();
|
||||
|
||||
const waitForUnmute = this.waitForAction(ElementWidgetActions.UnmuteVideo);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.UnmuteVideo, {});
|
||||
await waitForUnmute;
|
||||
};
|
||||
|
||||
private assertConnected = () => {
|
||||
if (!this.activeChannel) throw new Error("Not connected to any voice channel");
|
||||
};
|
||||
|
||||
private waitForAction = async (action: ElementWidgetActions) => {
|
||||
const wait = new Promise<void>(resolve =>
|
||||
this.activeChannel.once(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.ack(ev);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
if (await timeout(wait, false, VoiceChannelStore.TIMEOUT) === false) {
|
||||
throw new Error("Communication with voice channel timed out");
|
||||
}
|
||||
};
|
||||
|
||||
private ack = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.activeChannel.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
private updateDevices = async (fn: (devices: string[]) => string[]) => {
|
||||
if (!this.roomId) {
|
||||
logger.error("Tried to update devices while disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = this.cli.getRoom(this.roomId)
|
||||
.currentState.getStateEvents(VOICE_CHANNEL_MEMBER, this.cli.getUserId())
|
||||
?.getContent<IVoiceChannelMemberContent>()?.devices ?? [];
|
||||
|
||||
await this.cli.sendStateEvent(
|
||||
this.roomId, VOICE_CHANNEL_MEMBER, { devices: fn(devices) }, this.cli.getUserId(),
|
||||
);
|
||||
};
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.ack(ev);
|
||||
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
|
||||
this.activeChannel = null;
|
||||
this._participants = null;
|
||||
this._audioMuted = null;
|
||||
this._videoMuted = null;
|
||||
|
||||
try {
|
||||
// Tell others that we're disconnected, by removing our device from room state
|
||||
await this.updateDevices(devices => {
|
||||
const devicesSet = new Set(devices);
|
||||
devicesSet.delete(this.cli.getDeviceId());
|
||||
return Array.from(devicesSet);
|
||||
});
|
||||
} finally {
|
||||
// Save this for last, since updateDevices needs the room ID
|
||||
this._roomId = null;
|
||||
this.emit(VoiceChannelEvent.Disconnect);
|
||||
}
|
||||
};
|
||||
|
||||
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._participants = ev.detail.data.participants as IJitsiParticipant[];
|
||||
this.emit(VoiceChannelEvent.Participants, ev.detail.data.participants);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._audioMuted = true;
|
||||
this.emit(VoiceChannelEvent.MuteAudio);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._audioMuted = false;
|
||||
this.emit(VoiceChannelEvent.UnmuteAudio);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._videoMuted = true;
|
||||
this.emit(VoiceChannelEvent.MuteVideo);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this._videoMuted = false;
|
||||
this.emit(VoiceChannelEvent.UnmuteVideo);
|
||||
this.ack(ev);
|
||||
};
|
||||
}
|
||||
@@ -22,26 +22,26 @@ import WidgetStore, { IApp } from "../stores/WidgetStore";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import WidgetUtils from "./WidgetUtils";
|
||||
|
||||
export const VOICE_CHANNEL = "io.element.voice";
|
||||
export const VOICE_CHANNEL_MEMBER = "io.element.voice.member";
|
||||
export const VIDEO_CHANNEL = "io.element.video";
|
||||
export const VIDEO_CHANNEL_MEMBER = "io.element.video.member";
|
||||
|
||||
export interface IVoiceChannelMemberContent {
|
||||
export interface IVideoChannelMemberContent {
|
||||
// Connected device IDs
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
export const getVoiceChannel = (roomId: string): IApp => {
|
||||
export const getVideoChannel = (roomId: string): IApp => {
|
||||
const apps = WidgetStore.instance.getApps(roomId);
|
||||
return apps.find(app => WidgetType.JITSI.matches(app.type) && app.id === VOICE_CHANNEL);
|
||||
return apps.find(app => WidgetType.JITSI.matches(app.type) && app.id === VIDEO_CHANNEL);
|
||||
};
|
||||
|
||||
export const addVoiceChannel = async (roomId: string, roomName: string) => {
|
||||
await WidgetUtils.addJitsiWidget(roomId, CallType.Voice, "Voice channel", VOICE_CHANNEL, roomName);
|
||||
export const addVideoChannel = async (roomId: string, roomName: string) => {
|
||||
await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", VIDEO_CHANNEL, roomName);
|
||||
};
|
||||
|
||||
export const getConnectedMembers = (state: RoomState): RoomMember[] =>
|
||||
state.getStateEvents(VOICE_CHANNEL_MEMBER)
|
||||
state.getStateEvents(VIDEO_CHANNEL_MEMBER)
|
||||
// Must have a device connected and still be joined to the room
|
||||
.filter(e => e.getContent<IVoiceChannelMemberContent>().devices?.length)
|
||||
.filter(e => e.getContent<IVideoChannelMemberContent>().devices?.length)
|
||||
.map(e => state.getMember(e.getStateKey()))
|
||||
.filter(member => member.membership === "join");
|
||||
@@ -36,6 +36,7 @@ import { Jitsi } from "../widgets/Jitsi";
|
||||
import { objectClone } from "./objects";
|
||||
import { _t } from "../languageHandler";
|
||||
import { IApp } from "../stores/WidgetStore";
|
||||
import { VIDEO_CHANNEL } from "./VideoChannelUtils";
|
||||
|
||||
// How long we wait for the state event echo to come back from the server
|
||||
// before waitFor[Room/User]Widget rejects its promise
|
||||
@@ -469,6 +470,7 @@ export default class WidgetUtils {
|
||||
conferenceId: confId,
|
||||
roomName: oobRoomName ?? MatrixClientPeg.get().getRoom(roomId)?.name,
|
||||
isAudioOnly: type === CallType.Voice,
|
||||
isVideoChannel: widgetId === VIDEO_CHANNEL,
|
||||
domain,
|
||||
auth,
|
||||
});
|
||||
@@ -515,6 +517,7 @@ export default class WidgetUtils {
|
||||
'conferenceDomain=$domain',
|
||||
'conferenceId=$conferenceId',
|
||||
'isAudioOnly=$isAudioOnly',
|
||||
'isVideoChannel=$isVideoChannel',
|
||||
'displayName=$matrix_display_name',
|
||||
'avatarUrl=$matrix_avatar_url',
|
||||
'userId=$matrix_user_id',
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
@@ -92,12 +93,13 @@ export const showAddExistingRooms = (space: Room): void => {
|
||||
);
|
||||
};
|
||||
|
||||
export const showCreateNewRoom = async (space: Room): Promise<boolean> => {
|
||||
export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise<boolean> => {
|
||||
const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
|
||||
"Space Landing",
|
||||
"Create Room",
|
||||
CreateRoomDialog,
|
||||
{
|
||||
type,
|
||||
defaultPublic: space.getJoinRule() === JoinRule.Public,
|
||||
parentSpace: space,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user