Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Release video rooms as a beta feature #8431

Merged
merged 28 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ddcbbfd
Remove blank header from video room view frame
robintown Apr 27, 2022
bd066a6
Add a beta card for video rooms
robintown Apr 27, 2022
7b07716
Rename the 'disclaimer' on beta cards to 'FAQ'
robintown Apr 27, 2022
fb7b486
Add beta pills to video room creation buttons
robintown Apr 27, 2022
c99792d
Remove duplicate tooltips from face piles
robintown Apr 27, 2022
7f44269
Merge branch 'develop' into video-rooms-beta
robintown May 2, 2022
50bd2cd
Add beta pill to headers of video rooms
robintown May 2, 2022
59703c0
Factor RoomInfoLine out of SpaceRoomView
robintown May 2, 2022
08db726
Factor RoomPreviewCard out of SpaceRoomView
robintown May 2, 2022
c3079e1
Adapt RoomPreviewCard for video rooms
robintown May 2, 2022
45d48e8
Merge branch 'develop' into video-rooms-beta
robintown May 2, 2022
fe83642
"New video room" → "Video room"
robintown May 2, 2022
d44b85a
Merge branch 'develop' into video-rooms-beta
robintown May 3, 2022
3c17f22
Add comment about unused cases in RoomPreviewCard
robintown May 3, 2022
7db10bc
Add types
robintown May 3, 2022
6ee692c
Clarify !important comments
robintown May 3, 2022
c4ace54
Add a reload warning
robintown May 3, 2022
78ecca0
Fix the reload warning being the wrong way around
robintown May 3, 2022
a5476ab
Fix lints
robintown May 3, 2022
0a71a80
Merge branch 'develop' into video-rooms-beta
robintown May 4, 2022
d294866
Make widgets in video rooms mutable again to de-risk future upgrades
robintown May 4, 2022
9e914c1
Ensure that the video channel exists when mounting VideoRoomView
robintown May 4, 2022
4dc7c06
Fix lint
robintown May 4, 2022
2190ffd
Iterate beta reload warning
robintown May 4, 2022
315ed63
Merge branch 'develop' into video-rooms-beta
robintown May 4, 2022
6dc1afd
Merge branch 'develop' into video-rooms-beta
robintown May 6, 2022
ca262d0
Merge branch 'develop' into video-rooms-beta
robintown May 6, 2022
5fcbf71
Merge branch 'develop' into video-rooms-beta
robintown Jun 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions res/css/views/beta/_BetaCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ limitations under the License.
background-color: $system;
border-radius: 8px;
box-sizing: border-box;
color: $secondary-content;

.mx_BetaCard_columns {
display: flex;
Expand All @@ -45,14 +46,13 @@ limitations under the License.
.mx_BetaCard_caption {
font-size: $font-15px;
line-height: $font-20px;
color: $secondary-content;
}

.mx_BetaCard_buttons {
display: flex;
flex-wrap: wrap-reverse;
gap: 12px;
margin: 20px auto;
gap: $spacing-12;
margin: $spacing-20 auto 0;

.mx_AccessibleButton {
padding: 7px 40px;
Expand All @@ -66,10 +66,16 @@ limitations under the License.
}
}

.mx_BetaCard_refreshWarning {
margin-top: $spacing-8;
font-size: $font-10px;
text-align: center;
}

.mx_BetaCard_faq {
margin-top: $spacing-20;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-content;

> h4 {
margin: 12px 0 0;
Expand Down Expand Up @@ -105,7 +111,6 @@ limitations under the License.
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-content;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions res/css/views/rooms/_RoomPreviewCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

.mx_RoomPreviewCard {
padding: $spacing-32 $spacing-24 !important; // override default padding from above
padding: $spacing-32 $spacing-24 !important; // Override SpaceRoomView's default padding
margin: auto;
flex-grow: 1;
max-width: 480px;
Expand Down Expand Up @@ -115,7 +115,7 @@ limitations under the License.
}

h1.mx_RoomPreviewCard_name {
margin: $spacing-16 0 !important; // override default margin from above
margin: $spacing-16 0 !important; // Override SpaceRoomView's default margins
}

.mx_RoomPreviewCard_topic {
Expand Down
37 changes: 28 additions & 9 deletions src/components/structures/VideoRoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,47 @@ import { Room } from "matrix-js-sdk/src/models/room";

import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useEventEmitter } from "../../hooks/useEventEmitter";
import { getVideoChannel } from "../../utils/VideoChannelUtils";
import WidgetStore from "../../stores/WidgetStore";
import WidgetUtils from "../../utils/WidgetUtils";
import { addVideoChannel, getVideoChannel } from "../../utils/VideoChannelUtils";
import WidgetStore, { IApp } from "../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
import AppTile from "../views/elements/AppTile";
import VideoLobby from "../views/voip/VideoLobby";

const VideoRoomView: FC<{ room: Room, resizing: boolean }> = ({ room, resizing }) => {
interface IProps {
room: Room;
resizing: boolean;
}

const VideoRoomView: FC<IProps> = ({ room, resizing }) => {
const cli = useContext(MatrixClientContext);
const store = VideoChannelStore.instance;

// In case we mount before the WidgetStore knows about our Jitsi widget
const [widgetStoreReady, setWidgetStoreReady] = useState(Boolean(WidgetStore.instance.matrixClient));
const [widgetLoaded, setWidgetLoaded] = useState(false);
useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => {
if (roomId === null || roomId === room.roomId) setWidgetLoaded(true);
if (roomId === null) setWidgetStoreReady(true);
if (roomId === null || roomId === room.roomId) {
setWidgetLoaded(Boolean(getVideoChannel(room.roomId)));
}
});

const app = useMemo(() => {
const app = getVideoChannel(room.roomId);
if (!app) logger.warn(`No video channel for room ${room.roomId}`);
return app;
}, [room, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
const app: IApp = useMemo(() => {
if (widgetStoreReady) {
const app = getVideoChannel(room.roomId);
if (!app) {
logger.warn(`No video channel for room ${room.roomId}`);
// Since widgets in video rooms are mutable, we'll take this opportunity to
// reinstate the Jitsi widget in case another client removed it
if (WidgetUtils.canUserModifyWidgets(room.roomId)) {
addVideoChannel(room.roomId, room.name);
}
}
return app;
}
}, [room, widgetStoreReady, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps

const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId);
useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(store.roomId === room.roomId));
Expand Down
11 changes: 11 additions & 0 deletions src/components/views/beta/BetaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
</AccessibleButton>;
}

let refreshWarning: string;
if (requiresRefresh) {
robintown marked this conversation as resolved.
Show resolved Hide resolved
const brand = SdkConfig.get().brand;
refreshWarning = value
? _t("Leaving the beta will reload %(brand)s.", { brand })
: _t("Joining the beta will reload %(brand)s.", { brand });
}

let content: ReactNode;
if (busy) {
content = <InlineSpinner />;
Expand Down Expand Up @@ -138,6 +146,9 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
{ content }
</AccessibleButton>
</div>
{ refreshWarning && <div className="mx_BetaCard_refreshWarning">
{ refreshWarning }
</div> }
{ faq && <div className="mx_BetaCard_faq">
{ faq(value) }
</div> }
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/rooms/RoomInfoLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
const membership = useMyRoomMembership(room);
const memberCount = useRoomMemberCount(room);

let iconClass;
let roomType;
let iconClass: string;
let roomType: string;
if (room.isElementVideoRoom()) {
iconClass = "mx_RoomInfoLine_video";
roomType = _t("Video room");
Expand All @@ -57,7 +57,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
}

let members;
let members: JSX.Element;
if (membership === "invite" && summary) {
// Don't trust local state and instead use the summary API
members = <span className="mx_RoomInfoLine_members">
Expand Down
4 changes: 4 additions & 0 deletions src/components/views/rooms/RoomPreviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ interface IProps {
onRejectButtonClicked: () => void;
}

// XXX This component is currently only used for spaces and video rooms, though
// surely we should expand its use to all rooms for consistency? This already
// handles the text room case, though we would need to add support for ignoring
// and viewing invite reasons to achieve parity with the default invite screen.
const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext);
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
Expand Down
11 changes: 0 additions & 11 deletions src/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
events: {
// Allow all users to send video member updates
[VIDEO_CHANNEL_MEMBER]: 0,
// Make widgets immutable, even to admins
"im.vector.modular.widgets": 200,
// Annoyingly, we have to reiterate all the defaults here
[EventType.RoomName]: 50,
[EventType.RoomAvatar]: 50,
Expand All @@ -144,10 +142,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
[EventType.RoomServerAcl]: 100,
[EventType.RoomEncryption]: 100,
},
users: {
// Temporarily give ourselves the power to set up a widget
[client.getUserId()]: 200,
},
};
}
}
Expand Down Expand Up @@ -270,11 +264,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
if (opts.roomType === RoomType.ElementVideo) {
// Set up video rooms with a Jitsi widget
await addVideoChannel(roomId, createOpts.name);

// Reset our power level back to admin so that the widget becomes immutable
const room = client.getRoom(roomId);
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent);
}
}).then(function() {
// NB createRoom doesn't block on the client seeing the echo that the
Expand Down
6 changes: 4 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,8 @@
"Experimental": "Experimental",
"Developer": "Developer",
"Video rooms": "Video rooms",
"A new way to chat over voice and video in Element.": "A new way to chat over voice and video in Element.",
"Video rooms are always-on VoIP channels embedded within a room in Element.": "Video rooms are always-on VoIP channels embedded within a room in Element.",
"A new way to chat over voice and video in %(brand)s.": "A new way to chat over voice and video in %(brand)s.",
"Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.",
"How can I create a video room?": "How can I create a video room?",
"Use the “+” button in the room section of the left panel.": "Use the “+” button in the room section of the left panel.",
"Can I use text chat alongside the video call?": "Can I use text chat alongside the video call?",
Expand Down Expand Up @@ -2954,6 +2954,8 @@
"This is a beta feature": "This is a beta feature",
"Click for more info": "Click for more info",
"Beta": "Beta",
"Leaving the beta will reload %(brand)s.": "Leaving the beta will reload %(brand)s.",
"Joining the beta will reload %(brand)s.": "Joining the beta will reload %(brand)s.",
"Join the beta": "Join the beta",
"Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s",
"Live until %(expiryTime)s": "Live until %(expiryTime)s",
Expand Down
12 changes: 10 additions & 2 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,16 @@ export const SETTINGS: {[setting: string]: ISetting} = {
betaInfo: {
title: _td("Video rooms"),
caption: () => <>
<p>{ _t("A new way to chat over voice and video in Element.") }</p>
<p>{ _t("Video rooms are always-on VoIP channels embedded within a room in Element.") }</p>
<p>
{ _t("A new way to chat over voice and video in %(brand)s.", {
brand: SdkConfig.get().brand,
}) }
</p>
<p>
{ _t("Video rooms are always-on VoIP channels embedded within a room in %(brand)s.", {
brand: SdkConfig.get().brand,
}) }
</p>
</>,
faq: () =>
SdkConfig.get().bug_report_endpoint_url && <>
Expand Down
29 changes: 18 additions & 11 deletions test/components/structures/VideoRoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ limitations under the License.
import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixWidgetType } from "matrix-widget-api";

import { stubClient, stubVideoChannelStore, mkRoom, wrapInMatrixClientContext } from "../../test-utils";
import {
stubClient,
stubVideoChannelStore,
StubVideoChannelStore,
mkRoom,
wrapInMatrixClientContext,
} from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { VIDEO_CHANNEL } from "../../../src/utils/VideoChannelUtils";
import WidgetStore from "../../../src/stores/WidgetStore";
Expand All @@ -30,7 +38,6 @@ import AppTile from "../../../src/components/views/elements/AppTile";
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);

describe("VideoRoomView", () => {
stubClient();
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
id: VIDEO_CHANNEL,
eventId: "$1:example.org",
Expand All @@ -45,22 +52,22 @@ describe("VideoRoomView", () => {
value: { enumerateDevices: () => [] },
});

const cli = MatrixClientPeg.get();
const room = mkRoom(cli, "!1:example.org");
let cli: MatrixClient;
let room: Room;
let store: StubVideoChannelStore;

let store;
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.get();
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
store = stubVideoChannelStore();
});

afterEach(() => {
jest.clearAllMocks();
room = mkRoom(cli, "!1:example.org");
});

it("shows lobby and keeps widget loaded when disconnected", async () => {
const view = mount(<VideoRoomView room={room} resizing={false} />);
// Wait for state to settle
await act(async () => Promise.resolve());
await act(() => Promise.resolve());

expect(view.find(VideoLobby).exists()).toEqual(true);
expect(view.find(AppTile).exists()).toEqual(true);
Expand All @@ -70,7 +77,7 @@ describe("VideoRoomView", () => {
store.connect("!1:example.org");
const view = mount(<VideoRoomView room={room} resizing={false} />);
// Wait for state to settle
await act(async () => Promise.resolve());
await act(() => Promise.resolve());

expect(view.find(VideoLobby).exists()).toEqual(false);
expect(view.find(AppTile).exists()).toEqual(true);
Expand Down
20 changes: 3 additions & 17 deletions test/createRoom-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,35 +37,21 @@ describe("createRoom", () => {
setupAsyncStoreWithClient(WidgetStore.instance, client);
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();

const userId = client.getUserId();
const roomId = await createRoom({ roomType: RoomType.ElementVideo });

const [[{
power_level_content_override: {
users: {
[userId]: userPower,
},
events: {
"im.vector.modular.widgets": widgetPower,
[VIDEO_CHANNEL_MEMBER]: videoMemberPower,
},
events: { [VIDEO_CHANNEL_MEMBER]: videoMemberPower },
},
}]] = mocked(client.createRoom).mock.calls as any;
}]] = mocked(client.createRoom).mock.calls as any; // no good type
const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls;

// We should have had enough power to be able to set up the Jitsi widget
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
// and should have actually set it up
// We should have set up the Jitsi widget
expect(widgetRoomId).toEqual(roomId);
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
expect(widgetId).toEqual(VIDEO_CHANNEL);

// All members should be able to update their connected devices
expect(videoMemberPower).toEqual(0);
// Jitsi widget should be immutable for admins
expect(widgetPower).toBeGreaterThan(100);
// and we should have been reset back to admin
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100, undefined);
});
});

Expand Down