Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17021
Conflicts: src/components/structures/SpaceRoomView.tsx src/i18n/strings/en_EN.json
This commit is contained in:
commit
a8c4aabb52
39 changed files with 791 additions and 423 deletions
|
@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
|||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
|
@ -119,6 +122,7 @@ interface IState {
|
|||
usageLimitEventContent?: IUsageLimit;
|
||||
usageLimitEventTs?: number;
|
||||
useCompactLayout: boolean;
|
||||
activeCalls: Array<MatrixCall>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
usageLimitDismissed: false,
|
||||
activeCalls: [],
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
|
@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
|
@ -206,6 +213,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
this.resizer.detach();
|
||||
}
|
||||
|
||||
private onCallsChanged = () => {
|
||||
this.setState({
|
||||
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
|
||||
});
|
||||
};
|
||||
|
||||
// Child components assume that the client peg will not be null, so give them some
|
||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||
//
|
||||
|
@ -661,6 +674,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||
return (
|
||||
<AudioFeedArrayForCall call={call} key={call.callId} />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
|
@ -685,6 +704,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
{audioFeedArraysForCalls}
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1096,7 +1096,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private leaveRoomWarnings(roomId: string) {
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||
// Show a warning if there are additional complications.
|
||||
const warnings = [];
|
||||
|
||||
|
@ -1135,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const warnings = this.leaveRoomWarnings(roomId);
|
||||
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
|
||||
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
||||
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
||||
description: (
|
||||
|
|
|
@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
|
|||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
export default class RightPanel extends React.Component {
|
||||
|
@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
|
|||
return RightPanelPhases.GroupMemberList;
|
||||
}
|
||||
return rps.groupPanelPhase;
|
||||
} else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
|
||||
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
|
||||
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
|
||||
) {
|
||||
return RightPanelPhases.SpaceMemberList;
|
||||
} else if (userForPanel) {
|
||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||
|
|
|
@ -1750,7 +1750,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
|
||||
if (myMembership === "invite"
|
||||
// SpaceRoomView handles invites itself
|
||||
&& (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
|
||||
) {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
@ -1892,7 +1895,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
|
||||
if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
{ previewBar }
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useMemo, useState} from "react";
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
|
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
|
|||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
|
@ -40,11 +40,13 @@ import InfoTooltip from "../views/elements/InfoTooltip";
|
|||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
refreshToken?: any;
|
||||
additionalButtons?: ReactNode;
|
||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -107,8 +109,16 @@ const Tile: React.FC<ITileProps> = ({
|
|||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
const onPreviewClick = () => onViewRoomClick(false);
|
||||
const onJoinClick = () => onViewRoomClick(true);
|
||||
const onPreviewClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(false);
|
||||
}
|
||||
const onJoinClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
onViewRoomClick(true);
|
||||
}
|
||||
|
||||
let button;
|
||||
if (myMembership === "join") {
|
||||
|
@ -355,6 +365,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
initialText = "",
|
||||
showRoom,
|
||||
refreshToken,
|
||||
additionalButtons,
|
||||
children,
|
||||
}) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -420,78 +431,83 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||
}
|
||||
|
||||
let editSection;
|
||||
let manageButtons;
|
||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||
});
|
||||
|
||||
let buttons;
|
||||
if (selectedRelations.length) {
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||
});
|
||||
|
||||
const disabled = removing || saving;
|
||||
const disabled = !selectedRelations.length || removing || saving;
|
||||
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
let props = {};
|
||||
if (!selectedRelations.length) {
|
||||
Button = AccessibleTooltipButton;
|
||||
props = {
|
||||
tooltip: _t("Select a room below first"),
|
||||
yOffset: -40,
|
||||
};
|
||||
}
|
||||
|
||||
editSection = <span>
|
||||
{ buttons }
|
||||
</span>;
|
||||
manageButtons = <>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setRemoving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
}
|
||||
setRemoving(false);
|
||||
}}
|
||||
kind="danger_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ removing ? _t("Removing...") : _t("Remove") }
|
||||
</Button>
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
const suggested = !selectionAllSuggested;
|
||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||
|
||||
const content = {
|
||||
...existingContent,
|
||||
suggested: !selectionAllSuggested,
|
||||
};
|
||||
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||
|
||||
parentChildMap.get(parentId).get(childId).content = content;
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{ saving
|
||||
? _t("Saving...")
|
||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||
}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
let results;
|
||||
|
@ -537,7 +553,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
content = <>
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ countsStr }
|
||||
{ editSection }
|
||||
<span>
|
||||
{ additionalButtons }
|
||||
{ manageButtons }
|
||||
</span>
|
||||
</div>
|
||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||
{ error }
|
||||
|
|
|
@ -54,6 +54,12 @@ import FacePile from "../views/elements/FacePile";
|
|||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||
import {sleep} from "../../utils/promise";
|
||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {BetaPill} from "../views/beta/BetaCard";
|
||||
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -262,6 +268,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</div>;
|
||||
};
|
||||
|
||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const rect = handle.current.getBoundingClientRect();
|
||||
contextMenu = <IconizedContextMenu
|
||||
left={rect.left + window.pageXOffset + 0}
|
||||
top={rect.bottom + window.pageYOffset + 8}
|
||||
chevronFace={ChevronFace.None}
|
||||
onFinished={closeMenu}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
if (await showCreateNewRoom(cli, space)) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
if (added) {
|
||||
onNewRoomAdded();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuButton
|
||||
kind="primary"
|
||||
inputRef={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("Add")}
|
||||
>
|
||||
{ _t("Add") }
|
||||
</ContextMenuButton>
|
||||
{ contextMenu }
|
||||
</>;
|
||||
};
|
||||
|
||||
const SpaceLanding = ({ space }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const myMembership = useMyRoomMembership(space);
|
||||
|
@ -286,32 +353,20 @@ const SpaceLanding = ({ space }) => {
|
|||
|
||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||
|
||||
let addRoomButtons;
|
||||
let addRoomButton;
|
||||
if (canAddRooms) {
|
||||
addRoomButtons = <React.Fragment>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
if (added) {
|
||||
forceUpdate();
|
||||
}
|
||||
}}>
|
||||
{ _t("Add existing rooms & spaces") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
|
||||
showCreateNewRoom(cli, space);
|
||||
}}>
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
|
||||
}
|
||||
|
||||
let settingsButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) {
|
||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}>
|
||||
{ _t("Settings") }
|
||||
</AccessibleButton>;
|
||||
settingsButton = <AccessibleTooltipButton
|
||||
className="mx_SpaceRoomView_landing_settingsButton"
|
||||
onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}
|
||||
title={_t("Settings")}
|
||||
/>;
|
||||
}
|
||||
|
||||
const onMembersClick = () => {
|
||||
|
@ -338,17 +393,19 @@ const SpaceLanding = ({ space }) => {
|
|||
<SpaceInfo space={space} />
|
||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomView_landing_topic">
|
||||
<RoomTopic room={space} />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
||||
{ addRoomButtons }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
|
||||
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
|
||||
<SpaceHierarchy
|
||||
space={space}
|
||||
showRoom={showRoom}
|
||||
refreshToken={refreshToken}
|
||||
additionalButtons={addRoomButton}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
|
|
|
@ -440,7 +440,7 @@ const UserOptionsSection: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const warnSelfDemote = async (isSpace) => {
|
||||
const warnSelfDemote = async (isSpace: boolean) => {
|
||||
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
|
@ -727,7 +727,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
|
|||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
|
@ -816,7 +816,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
||||
if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
|
@ -1095,7 +1095,7 @@ const PowerLevelEditor: React.FC<{
|
|||
} else if (myUserId === target) {
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
try {
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
|
@ -1325,10 +1325,10 @@ const BasicUserInfo: React.FC<{
|
|||
if (!isRoomEncrypted) {
|
||||
if (!cryptoEnabled) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room && !room.isSpaceRoom()) {
|
||||
} else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
}
|
||||
} else if (!room.isSpaceRoom()) {
|
||||
} else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
|
@ -1405,7 +1405,7 @@ const BasicUserInfo: React.FC<{
|
|||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
@ -1567,7 +1567,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = previousPhase = room.isSpaceRoom()
|
||||
previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()
|
||||
? RightPanelPhases.SpaceMemberList
|
||||
: RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
@ -1616,7 +1616,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
|
|
|
@ -30,6 +30,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -460,7 +461,7 @@ export default class MemberList extends React.Component {
|
|||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
inviteButtonText = _t("Invite to this community");
|
||||
} else if (room.isSpaceRoom()) {
|
||||
} else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
|
@ -492,7 +493,7 @@ export default class MemberList extends React.Component {
|
|||
let previousPhase = RightPanelPhases.RoomSummary;
|
||||
// We have no previousPhase for when viewing a MemberList from a Space
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||
previousPhase = undefined;
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
|
|
|
@ -26,6 +26,7 @@ import {isValid3pidInvite} from "../../../RoomInvite";
|
|||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -135,7 +136,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
if (SettingsStore.getValue("feature_spaces") && this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
|
|
97
src/components/views/voip/AudioFeed.tsx
Normal file
97
src/components/views/voip/AudioFeed.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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, {createRef} from 'react';
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
|
||||
interface IProps {
|
||||
feed: CallFeed,
|
||||
}
|
||||
|
||||
export default class AudioFeed extends React.Component<IProps> {
|
||||
private element = createRef<HTMLAudioElement>();
|
||||
|
||||
componentDidMount() {
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
const audioOutput = CallMediaHandler.getAudioOutput();
|
||||
|
||||
if (audioOutput) {
|
||||
try {
|
||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||
// it fails.
|
||||
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
|
||||
// back to the default after the call is over - Dave
|
||||
element.setSinkId(audioOutput);
|
||||
} catch (e) {
|
||||
console.error("Couldn't set requested audio output device: using default", e);
|
||||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
}
|
||||
|
||||
element.muted = false;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
|
||||
try {
|
||||
// A note on calling methods on media elements:
|
||||
// We used to have queues per media element to serialise all calls on those elements.
|
||||
// The reason given for this was that load() and play() were racing. However, we now
|
||||
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||
// operation was causing bugs where video would not resume because some play command
|
||||
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||
// them with another load() which will cancel the pending one, but since we don't call
|
||||
// load() explicitly, it shouldn't be a problem. - Dave
|
||||
element.play()
|
||||
} catch (e) {
|
||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||
}
|
||||
}
|
||||
|
||||
private stopMedia() {
|
||||
const element = this.element.current;
|
||||
|
||||
element.pause();
|
||||
element.src = null;
|
||||
|
||||
// As per comment in componentDidMount, setting the sink ID back to the
|
||||
// default once the call is over makes setSinkId work reliably. - Dave
|
||||
// Since we are not using the same element anymore, the above doesn't
|
||||
// seem to be necessary - Šimon
|
||||
}
|
||||
|
||||
private onNewStream = () => {
|
||||
this.playMedia();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<audio ref={this.element} />
|
||||
);
|
||||
}
|
||||
}
|
60
src/components/views/voip/AudioFeedArrayForCall.tsx
Normal file
60
src/components/views/voip/AudioFeedArrayForCall.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 from "react";
|
||||
import AudioFeed from "./AudioFeed"
|
||||
import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
feeds: Array<CallFeed>;
|
||||
}
|
||||
|
||||
export default class AudioFeedArrayForCall extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
feeds: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
|
||||
onFeedsChanged = () => {
|
||||
this.setState({
|
||||
feeds: this.props.call.getRemoteFeeds(),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.feeds.map((feed, i) => {
|
||||
return (
|
||||
<AudioFeed feed={feed} key={i} />
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
|
@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -110,12 +109,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
|
@ -143,21 +144,24 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case Action.CallChangeRoom:
|
||||
case 'call_state': {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
this.updateCalls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateCalls = () => {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private onCallRemoteHold = () => {
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||
|
|
|
@ -20,10 +20,9 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
||||
import VideoFeed from './VideoFeed';
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||
|
@ -31,6 +30,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
|
|||
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
|
@ -40,11 +40,11 @@ interface IProps {
|
|||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall,
|
||||
|
||||
// a callback which is called when the content in the callview changes
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
|
||||
// Whether this call view is for picture-in-pictue mode
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
|
@ -60,6 +60,7 @@ interface IState {
|
|||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
showDialpad: boolean,
|
||||
feeds: CallFeed[],
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
|
@ -115,6 +116,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
controlsVisible: true,
|
||||
showMoreMenu: false,
|
||||
showDialpad: false,
|
||||
feeds: this.props.call.getFeeds(),
|
||||
}
|
||||
|
||||
this.updateCallListeners(null, this.props.call);
|
||||
|
@ -172,11 +174,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
oldCall.removeListener(CallEvent.State, this.onCallState);
|
||||
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
oldCall.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
if (newCall) {
|
||||
newCall.on(CallEvent.State, this.onCallState);
|
||||
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||
newCall.on(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +190,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
|
||||
this.setState({feeds: newFeeds});
|
||||
};
|
||||
|
||||
private onCallLocalHoldUnhold = () => {
|
||||
this.setState({
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
|
@ -304,7 +312,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
// Note that this assumes we always have a callview on screen at any given time
|
||||
// Note that this assumes we always have a CallView on screen at any given time
|
||||
// CallHandler would probably be a better place for this
|
||||
private onNativeKeyDown = ev => {
|
||||
let handled = false;
|
||||
|
@ -474,6 +482,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
{contextMenuButton}
|
||||
</div>;
|
||||
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
|
||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||
// for voice calls (fills the bg)
|
||||
let contentView: React.ReactNode;
|
||||
|
@ -524,41 +534,85 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
</div>;
|
||||
}
|
||||
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
let localVideoFeed = null;
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_video: true,
|
||||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
if (isOnHold) {
|
||||
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
|
||||
if (isOnHold || transfereeCall) {
|
||||
if (this.props.call.type === CallType.Video) {
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_video: true,
|
||||
mx_CallView_video_hold: isOnHold,
|
||||
});
|
||||
let onHoldBackground = null;
|
||||
const backgroundStyle: CSSProperties = {};
|
||||
const backgroundAvatarUrl = avatarUrlForMember(
|
||||
// is it worth getting the size of the div to pass here?
|
||||
// is it worth getting the size of the div to pass here?
|
||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||
);
|
||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||
}
|
||||
if (!this.state.vidMuted) {
|
||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
||||
}
|
||||
|
||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{onHoldBackground}
|
||||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize} />
|
||||
{localVideoFeed}
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
contentView = (
|
||||
<div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{onHoldBackground}
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const classes = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
contentView =(
|
||||
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div
|
||||
className="mx_CallView_voice_avatarContainer"
|
||||
style={{width: avatarSize, height: avatarSize}}
|
||||
>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{holdTransferContent}
|
||||
{callControls}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (this.props.call.noIncomingFeeds()) {
|
||||
// Here we're reusing the css classes from voice on hold, because
|
||||
// I am lazy. If this gets merged, the CallView might be subject
|
||||
// to change anyway - I might take an axe to this file in order to
|
||||
// try to get other things working
|
||||
const classes = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_voice: true,
|
||||
mx_CallView_voice_hold: isOnHold,
|
||||
});
|
||||
|
||||
const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
|
||||
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||
// as before. But once again this might be subject to change
|
||||
if (feed.isVideoMuted()) return;
|
||||
return (
|
||||
<VideoFeed
|
||||
key={i}
|
||||
feed={feed}
|
||||
call={this.props.call}
|
||||
pipMode={this.props.pipMode}
|
||||
onResize={this.props.onResize}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Saying "Connecting" here isn't really true, but the best thing
|
||||
// I can come up with, but this might be subject to change as well
|
||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||
{feeds}
|
||||
<div className="mx_CallView_voice_avatarsContainer">
|
||||
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||
<RoomAvatar
|
||||
|
@ -568,7 +622,35 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{holdTransferContent}
|
||||
<div className="mx_CallView_holdTransferContent">{_t("Connecting")}</div>
|
||||
{callControls}
|
||||
</div>;
|
||||
} else {
|
||||
const containerClasses = classNames({
|
||||
mx_CallView_content: true,
|
||||
mx_CallView_video: true,
|
||||
});
|
||||
|
||||
// TODO: Later the CallView should probably be reworked to support
|
||||
// any number of feeds but now we can always expect there to be two
|
||||
// feeds. This is because the js-sdk ignores any new incoming streams
|
||||
const feeds = this.state.feeds.map((feed, i) => {
|
||||
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||
// as before. But once again this might be subject to change
|
||||
if (feed.isVideoMuted() && feed.isLocal()) return;
|
||||
return (
|
||||
<VideoFeed
|
||||
key={i}
|
||||
feed={feed}
|
||||
call={this.props.call}
|
||||
pipMode={this.props.pipMode}
|
||||
onResize={this.props.onResize}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||
{feeds}
|
||||
{callControls}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -16,13 +16,12 @@ limitations under the License.
|
|||
|
||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import React from 'react';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import CallView from './CallView';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {Resizable} from "re-resizable";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
interface IProps {
|
||||
// What room we should display the call for
|
||||
|
@ -55,25 +54,30 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
switch (payload.action) {
|
||||
case Action.CallChangeRoom:
|
||||
case 'call_state': {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({call: newCall});
|
||||
}
|
||||
this.updateCall();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateCall = () => {
|
||||
const newCall = this.getCall();
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({call: newCall});
|
||||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall {
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId);
|
||||
|
||||
|
|
|
@ -18,52 +18,102 @@ import classnames from 'classnames';
|
|||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import React, {createRef} from 'react';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import MemberAvatar from "../avatars/MemberAvatar"
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum VideoFeedType {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall,
|
||||
|
||||
type: VideoFeedType,
|
||||
feed: CallFeed,
|
||||
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
// This is sort of a proxy for a number of things but we currently have no
|
||||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize?: (e: Event) => void,
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voip.VideoFeed")
|
||||
export default class VideoFeed extends React.Component<IProps> {
|
||||
private vid = createRef<HTMLVideoElement>();
|
||||
interface IState {
|
||||
audioMuted: boolean;
|
||||
videoMuted: boolean;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.vid.current.addEventListener('resize', this.onResize);
|
||||
this.setVideoElement();
|
||||
@replaceableComponent("views.voip.VideoFeed")
|
||||
export default class VideoFeed extends React.Component<IProps, IState> {
|
||||
private element = createRef<HTMLVideoElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioMuted: this.props.feed.isAudioMuted(),
|
||||
videoMuted: this.props.feed.isVideoMuted(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.call !== prevProps.call) {
|
||||
this.setVideoElement();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.vid.current.removeEventListener('resize', this.onResize);
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.element.current?.removeEventListener('resize', this.onResize);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private setVideoElement() {
|
||||
if (this.props.type === VideoFeedType.Local) {
|
||||
this.props.call.setLocalVideoElement(this.vid.current);
|
||||
} else {
|
||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
if (!element) return;
|
||||
// We play audio in AudioFeed, not here
|
||||
element.muted = true;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
try {
|
||||
// A note on calling methods on media elements:
|
||||
// We used to have queues per media element to serialise all calls on those elements.
|
||||
// The reason given for this was that load() and play() were racing. However, we now
|
||||
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||
// operation was causing bugs where video would not resume because some play command
|
||||
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||
// them with another load() which will cancel the pending one, but since we don't call
|
||||
// load() explicitly, it shouldn't be a problem. - Dave
|
||||
element.play()
|
||||
} catch (e) {
|
||||
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||
}
|
||||
}
|
||||
|
||||
onResize = (e) => {
|
||||
if (this.props.onResize) {
|
||||
private stopMedia() {
|
||||
const element = this.element.current;
|
||||
if (!element) return;
|
||||
|
||||
element.pause();
|
||||
element.src = null;
|
||||
|
||||
// As per comment in componentDidMount, setting the sink ID back to the
|
||||
// default once the call is over makes setSinkId work reliably. - Dave
|
||||
// Since we are not using the same element anymore, the above doesn't
|
||||
// seem to be necessary - Šimon
|
||||
}
|
||||
|
||||
private onNewStream = () => {
|
||||
this.setState({
|
||||
audioMuted: this.props.feed.isAudioMuted(),
|
||||
videoMuted: this.props.feed.isVideoMuted(),
|
||||
});
|
||||
this.playMedia();
|
||||
};
|
||||
|
||||
private onResize = (e) => {
|
||||
if (this.props.onResize && !this.props.feed.isLocal()) {
|
||||
this.props.onResize(e);
|
||||
}
|
||||
};
|
||||
|
@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component<IProps> {
|
|||
render() {
|
||||
const videoClasses = {
|
||||
mx_VideoFeed: true,
|
||||
mx_VideoFeed_local: this.props.type === VideoFeedType.Local,
|
||||
mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote,
|
||||
mx_VideoFeed_local: this.props.feed.isLocal(),
|
||||
mx_VideoFeed_remote: !this.props.feed.isLocal(),
|
||||
mx_VideoFeed_voice: this.state.videoMuted,
|
||||
mx_VideoFeed_video: !this.state.videoMuted,
|
||||
mx_VideoFeed_mirror: (
|
||||
this.props.type === VideoFeedType.Local &&
|
||||
this.props.feed.isLocal() &&
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||
),
|
||||
};
|
||||
|
||||
return <video className={classnames(videoClasses)} ref={this.vid} />;
|
||||
if (this.state.videoMuted) {
|
||||
const member = this.props.feed.getMember();
|
||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||
|
||||
return (
|
||||
<div className={classnames(videoClasses)} >
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
height={avatarSize}
|
||||
width={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<video className={classnames(videoClasses)} ref={this.element} />
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue