Merge remote-tracking branch 'origin/develop' into feat/matrix-wysisyg-integration
This commit is contained in:
commit
5bdac78fc7
150 changed files with 3632 additions and 980 deletions
|
@ -493,7 +493,6 @@ export class EmailIdentityAuthEntry extends
|
|||
? _t("Resent!")
|
||||
: _t("Resend")}
|
||||
alignment={Alignment.Right}
|
||||
tooltipClassName="mx_Tooltip_noMargin"
|
||||
onHideTooltip={this.state.requested
|
||||
? () => this.setState({ requested: false })
|
||||
: undefined}
|
||||
|
|
|
@ -32,8 +32,10 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
||||
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||
|
@ -96,6 +98,14 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
|
|||
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
|
||||
"RoomSettingsGeneral",
|
||||
));
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
tabs.push(new Tab(
|
||||
ROOM_VOIP_TAB,
|
||||
_td("Voice & Video"),
|
||||
"mx_RoomSettingsDialog_voiceIcon",
|
||||
<VoipRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
ROOM_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import Tooltip, { Alignment } from './Tooltip';
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
title?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
label?: string;
|
||||
tooltipClassName?: string;
|
||||
|
@ -78,7 +78,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
|
||||
...props } = this.props;
|
||||
|
||||
const tip = this.state.hover && <Tooltip
|
||||
const tip = this.state.hover && (title || tooltip) && <Tooltip
|
||||
tooltipClassName={tooltipClassName}
|
||||
label={tooltip || title}
|
||||
alignment={alignment}
|
||||
|
@ -86,11 +86,11 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
onMouseOver={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.hideTooltip}
|
||||
aria-label={title}
|
||||
onMouseOver={this.showTooltip || props.onMouseOver}
|
||||
onMouseLeave={this.hideTooltip || props.onMouseLeave}
|
||||
onFocus={this.onFocus || props.onFocus}
|
||||
onBlur={this.hideTooltip || props.onBlur}
|
||||
aria-label={title || props["aria-label"]}
|
||||
>
|
||||
{ children }
|
||||
{ this.props.label }
|
||||
|
|
|
@ -27,6 +27,8 @@ interface IProps {
|
|||
label: string;
|
||||
// The translated caption for the switch
|
||||
caption?: string;
|
||||
// Tooltip to display
|
||||
tooltip?: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
|
@ -53,7 +55,8 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
|||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
aria-label={this.props.label}
|
||||
title={this.props.label}
|
||||
tooltip={this.props.tooltip}
|
||||
/>;
|
||||
|
||||
if (this.props.toggleInFront) {
|
||||
|
@ -66,7 +69,7 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
|||
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
|
||||
});
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div data-testid={this.props["data-testid"]} className={classes}>
|
||||
{ firstPart }
|
||||
{ secondPart }
|
||||
</div>
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,21 +18,27 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
checked: boolean;
|
||||
|
||||
// Title to use
|
||||
title?: string;
|
||||
|
||||
// Whether or not the user can interact with the switch
|
||||
disabled?: boolean;
|
||||
|
||||
// Tooltip to show
|
||||
tooltip?: string;
|
||||
|
||||
// Called when the checked state changes. First argument will be the new state.
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
||||
export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps) => {
|
||||
const _onClick = () => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
|
@ -45,14 +51,16 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props}
|
||||
<AccessibleTooltipButton {...props}
|
||||
className={classes}
|
||||
onClick={_onClick}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
title={title}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
<div className="mx_ToggleSwitch_ball" />
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -149,18 +149,24 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
break;
|
||||
case Alignment.Top:
|
||||
style.top = baseTop - spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%, -100%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))), -100%)`;
|
||||
break;
|
||||
case Alignment.Bottom:
|
||||
style.top = baseTop + parentBox.height + spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.InnerBottom:
|
||||
style.top = baseTop + parentBox.height - 50;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.TopRight:
|
||||
style.top = baseTop - spacing;
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Call, ConnectionState } from "../../../models/Call";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -28,9 +28,9 @@ import type { ButtonEvent } from "../elements/AccessibleButton";
|
|||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { CallDuration, CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
const MAX_FACES = 8;
|
||||
|
||||
|
@ -39,6 +39,8 @@ interface ActiveCallEventProps {
|
|||
participants: Set<RoomMember>;
|
||||
buttonText: string;
|
||||
buttonKind: string;
|
||||
buttonTooltip?: string;
|
||||
buttonDisabled?: boolean;
|
||||
onButtonClick: ((ev: ButtonEvent) => void) | null;
|
||||
}
|
||||
|
||||
|
@ -49,6 +51,8 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
|||
participants,
|
||||
buttonText,
|
||||
buttonKind,
|
||||
buttonDisabled,
|
||||
buttonTooltip,
|
||||
onButtonClick,
|
||||
},
|
||||
ref,
|
||||
|
@ -80,14 +84,15 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
|||
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
|
||||
</div>
|
||||
<CallDurationFromEvent mxEvent={mxEvent} />
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallEvent_button"
|
||||
kind={buttonKind}
|
||||
disabled={onButtonClick === null}
|
||||
disabled={onButtonClick === null || buttonDisabled}
|
||||
onClick={onButtonClick}
|
||||
tooltip={buttonTooltip}
|
||||
>
|
||||
{ buttonText }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -101,6 +106,7 @@ interface ActiveLoadedCallEventProps {
|
|||
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
|
||||
const connectionState = useConnectionState(call);
|
||||
const participants = useParticipants(call);
|
||||
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
const connect = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -132,6 +138,8 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
|
|||
participants={participants}
|
||||
buttonText={buttonText}
|
||||
buttonKind={buttonKind}
|
||||
buttonDisabled={Boolean(joinCallButtonDisabledTooltip)}
|
||||
buttonTooltip={joinCallButtonDisabledTooltip}
|
||||
onButtonClick={onButtonClick}
|
||||
/>;
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ import classNames from 'classnames';
|
|||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
|
@ -54,6 +55,7 @@ export default class HeaderButton extends React.Component<IProps> {
|
|||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={title}
|
||||
alignment={Alignment.Bottom}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
/>;
|
||||
|
|
|
@ -282,7 +282,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
title={_t('Room info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
/>,
|
||||
|
|
|
@ -31,7 +31,6 @@ import Resizer from "../../../resizer/resizer";
|
|||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
@ -330,13 +329,8 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{ height: Math.min(height, maxHeight), width: undefined }}
|
||||
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
|
@ -346,7 +340,15 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||
resizeNotifier.notifyTimelineHeightChanged();
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
setHeight(height + d.height);
|
||||
let newHeight = defaultHeight + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
|
||||
WidgetLayoutStore.instance.setContainerHeight(
|
||||
room,
|
||||
Container.Top,
|
||||
newHeight,
|
||||
);
|
||||
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
handleWrapperClass={handleWrapperClass}
|
||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
||||
export enum E2EState {
|
||||
|
@ -49,10 +49,20 @@ interface IProps {
|
|||
size?: number;
|
||||
onClick?: () => void;
|
||||
hideTooltip?: boolean;
|
||||
tooltipAlignment?: Alignment;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const E2EIcon: React.FC<IProps> = ({
|
||||
isUser,
|
||||
status,
|
||||
className,
|
||||
size,
|
||||
onClick,
|
||||
hideTooltip,
|
||||
tooltipAlignment,
|
||||
bordered,
|
||||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -80,7 +90,7 @@ const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, h
|
|||
|
||||
let tip;
|
||||
if (hover && !hideTooltip) {
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
|
|
|
@ -18,6 +18,8 @@ import React, { FC } from "react";
|
|||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Call } from "../../../models/Call";
|
||||
import { useParticipants } from "../../../hooks/useCall";
|
||||
|
||||
export enum LiveContentType {
|
||||
Video,
|
||||
|
@ -55,3 +57,18 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
|||
</> }
|
||||
</span>
|
||||
);
|
||||
|
||||
interface LiveContentSummaryWithCallProps {
|
||||
call: Call;
|
||||
}
|
||||
|
||||
export function LiveContentSummaryWithCall({ call }: LiveContentSummaryWithCallProps) {
|
||||
const participants = useParticipants(call);
|
||||
|
||||
return <LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("Video")}
|
||||
active={false}
|
||||
participantCount={participants.size}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
|
@ -32,7 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
@ -53,18 +52,21 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|||
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
|
||||
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import SdkConfig, { DEFAULTS } from "../../../SdkConfig";
|
||||
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
import { useCall, useLayout } from "../../../hooks/useCall";
|
||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { ElementCall } from "../../../models/Call";
|
||||
import { Call, ElementCall, Layout } from "../../../models/Call";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
class DisabledWithReason {
|
||||
constructor(public readonly reason: string) { }
|
||||
|
@ -107,6 +109,7 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>;
|
||||
};
|
||||
|
@ -192,10 +195,11 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (Element Call)")} onClick={onElementClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (%(brand)s)", { brand })} onClick={onElementClick} />
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
@ -207,6 +211,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{ menu }
|
||||
|
@ -225,7 +230,9 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, room]);
|
||||
const useElementCallExclusively = useMemo(() => SdkConfig.get("element_call").use_exclusively, []);
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
return SdkConfig.get("element_call").use_exclusively ?? DEFAULTS.element_call.use_exclusively;
|
||||
}, []);
|
||||
|
||||
const hasLegacyCall = useEventEmitterState(
|
||||
LegacyCallHandler.instance,
|
||||
|
@ -318,6 +325,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
}
|
||||
};
|
||||
|
||||
interface CallLayoutSelectorProps {
|
||||
call: ElementCall;
|
||||
}
|
||||
|
||||
const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
||||
const layout = useLayout(call);
|
||||
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
const onClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
openMenu();
|
||||
}, [openMenu]);
|
||||
|
||||
const onFreedomClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Tile);
|
||||
}, [closeMenu, call]);
|
||||
|
||||
const onSpotlightClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Spotlight);
|
||||
}, [closeMenu, call]);
|
||||
|
||||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
menu = <IconizedContextMenu
|
||||
className="mx_RoomHeader_layoutMenu"
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_freedomIcon"
|
||||
label={_t("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_RoomHeader_button", {
|
||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Layout type")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{ menu }
|
||||
</>;
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
searchScope: SearchScope;
|
||||
|
@ -338,6 +411,8 @@ export interface IProps {
|
|||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
viewingCall: boolean;
|
||||
activeCall: Call | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -356,6 +431,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
private readonly client = this.props.room.client;
|
||||
|
||||
constructor(props: IProps, context: IState) {
|
||||
super(props, context);
|
||||
|
@ -367,14 +443,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
|
@ -401,7 +475,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private onContextMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onContextMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -412,56 +486,98 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: undefined });
|
||||
};
|
||||
|
||||
private renderButtons(): JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
private onHideCallClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
view_call: false,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.props.inRoom && !this.context.tombstone) {
|
||||
buttons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||
private renderButtons(isVideoRoom: boolean): React.ReactNode {
|
||||
const startButtons: JSX.Element[] = [];
|
||||
|
||||
if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) {
|
||||
startButtons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (this.props.onForgetClick) {
|
||||
const forgetButton = <AccessibleTooltipButton
|
||||
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
|
||||
startButtons.push(<CallLayoutSelector call={this.props.activeCall} />);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onForgetClick) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>;
|
||||
buttons.push(forgetButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onAppsClick) {
|
||||
const appsButton = <AccessibleTooltipButton
|
||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>;
|
||||
buttons.push(appsButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
const searchButton = <AccessibleTooltipButton
|
||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>;
|
||||
buttons.push(searchButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && this.props.inRoom) {
|
||||
const inviteButton = <AccessibleTooltipButton
|
||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="invite"
|
||||
/>;
|
||||
buttons.push(inviteButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
const endButtons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
if (this.props.activeCall === null) {
|
||||
endButtons.push(<AccessibleButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("Close call")}
|
||||
key="close"
|
||||
/>);
|
||||
} else {
|
||||
endButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{ startButtons }
|
||||
<RoomHeaderButtons
|
||||
room={this.props.room}
|
||||
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
{ endButtons }
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderName(oobName: string) {
|
||||
|
@ -480,7 +596,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
|
@ -505,6 +621,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
alignment={Alignment.Bottom}
|
||||
>
|
||||
{ roomName }
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
|
@ -519,6 +636,57 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
const icon = this.props.viewingCall
|
||||
? <div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
||||
: this.props.e2eStatus
|
||||
? <E2EIcon
|
||||
className="mx_RoomHeader_icon"
|
||||
status={this.props.e2eStatus}
|
||||
tooltipAlignment={Alignment.Bottom}
|
||||
/>
|
||||
// If we're expecting an E2EE status to come in, but it hasn't
|
||||
// yet been loaded, insert a blank div to reserve space
|
||||
: this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled()
|
||||
? <div className="mx_RoomHeader_icon" />
|
||||
: null;
|
||||
|
||||
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
return (
|
||||
<header className="mx_RoomHeader light-panel">
|
||||
<div
|
||||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ icon }
|
||||
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
|
||||
{ _t("Video call") }
|
||||
</div>
|
||||
{ this.props.activeCall instanceof ElementCall && (
|
||||
<CallDurationFromEvent mxEvent={this.props.activeCall.groupCall} />
|
||||
) }
|
||||
{ /* Empty topic element to fill out space */ }
|
||||
<div className="mx_RoomHeader_topic" />
|
||||
{ buttons }
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
let searchStatus: JSX.Element | null = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
|
@ -543,29 +711,6 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
let buttons: JSX.Element | null = null;
|
||||
if (this.props.showButtons) {
|
||||
buttons = <React.Fragment>
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
const viewLabs = () => defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
|
@ -581,7 +726,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ icon }
|
||||
{ name }
|
||||
{ searchStatus }
|
||||
{ topicElement }
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
call: CallStore.instance.get(this.props.room.roomId),
|
||||
call: CallStore.instance.getCall(this.props.room.roomId),
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
};
|
||||
|
@ -159,7 +159,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Recalculate the call for this room, since it could've changed between
|
||||
// construction and mounting
|
||||
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
|
||||
this.setState({ call: CallStore.instance.getCall(this.props.room.roomId) });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi
|
|||
import LogoutDialog from '../dialogs/LogoutDialog';
|
||||
import DeviceTile from './devices/DeviceTile';
|
||||
import SelectableDeviceTile from './devices/SelectableDeviceTile';
|
||||
import { DeviceType } from '../../../utils/device/parseUserAgent';
|
||||
|
||||
interface IProps {
|
||||
device: IMyDevice;
|
||||
|
@ -153,9 +154,10 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
|
||||
const deviceWithVerification = {
|
||||
const extendedDevice = {
|
||||
...this.props.device,
|
||||
isVerified: this.props.verified,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
|
||||
if (this.props.isOwnDevice) {
|
||||
|
@ -163,7 +165,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
<div className="mx_DevicesPanel_deviceTrust">
|
||||
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
|
||||
</div>
|
||||
<DeviceTile device={deviceWithVerification}>
|
||||
<DeviceTile device={extendedDevice}>
|
||||
{ buttons }
|
||||
</DeviceTile>
|
||||
</div>;
|
||||
|
@ -171,7 +173,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<SelectableDeviceTile device={deviceWithVerification} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
<SelectableDeviceTile device={extendedDevice} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
{ buttons }
|
||||
</SelectableDeviceTile>
|
||||
</div>
|
||||
|
|
|
@ -24,10 +24,10 @@ import DeviceDetails from './DeviceDetails';
|
|||
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
|
||||
import DeviceTile from './DeviceTile';
|
||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||
import { DeviceWithVerification } from './types';
|
||||
import { ExtendedDevice } from './types';
|
||||
|
||||
interface Props {
|
||||
device?: DeviceWithVerification;
|
||||
device?: ExtendedDevice;
|
||||
isLoading: boolean;
|
||||
isSigningOut: boolean;
|
||||
localNotificationSettings?: LocalNotificationSettings | undefined;
|
||||
|
|
|
@ -22,10 +22,10 @@ import Field from '../../elements/Field';
|
|||
import Spinner from '../../elements/Spinner';
|
||||
import { Caption } from '../../typography/Caption';
|
||||
import Heading from '../../typography/Heading';
|
||||
import { DeviceWithVerification } from './types';
|
||||
import { ExtendedDevice } from './types';
|
||||
|
||||
interface Props {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'session',
|
||||
values: [
|
||||
{ label: _t('Session ID'), value: device.device_id },
|
||||
{ label: _t('Client'), value: device.client },
|
||||
{
|
||||
label: _t('Last activity'),
|
||||
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
|
||||
|
@ -72,8 +73,8 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'application',
|
||||
heading: _t('Application'),
|
||||
values: [
|
||||
{ label: _t('Name'), value: device.clientName },
|
||||
{ label: _t('Version'), value: device.clientVersion },
|
||||
{ label: _t('Name'), value: device.appName },
|
||||
{ label: _t('Version'), value: device.appVersion },
|
||||
{ label: _t('URL'), value: device.url },
|
||||
],
|
||||
},
|
||||
|
@ -81,6 +82,8 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'device',
|
||||
heading: _t('Device'),
|
||||
values: [
|
||||
{ label: _t('Model'), value: device.deviceModel },
|
||||
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
|
||||
{ label: _t('IP address'), value: device.last_seen_ip },
|
||||
],
|
||||
},
|
||||
|
@ -150,7 +153,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={checked => setPushNotifications?.(device.device_id, checked)}
|
||||
aria-label={_t("Toggle push notifications on this session.")}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
data-testid='device-detail-push-notification-checkbox'
|
||||
/>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>
|
||||
|
|
|
@ -21,16 +21,16 @@ import { _t } from "../../../../languageHandler";
|
|||
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
|
||||
import Heading from "../../typography/Heading";
|
||||
import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "./filter";
|
||||
import { DeviceWithVerification } from "./types";
|
||||
import { DeviceType } from "./DeviceType";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { DeviceTypeIcon } from "./DeviceTypeIcon";
|
||||
export interface DeviceTileProps {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
isSelected?: boolean;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }) => {
|
||||
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
|
||||
return <Heading size='h4'>
|
||||
{ device.display_name || device.device_id }
|
||||
</Heading>;
|
||||
|
@ -48,7 +48,7 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri
|
|||
return formatRelativeTime(new Date(timestamp));
|
||||
};
|
||||
|
||||
const getInactiveMetadata = (device: DeviceWithVerification): { id: string, value: React.ReactNode } | undefined => {
|
||||
const getInactiveMetadata = (device: ExtendedDevice): { id: string, value: React.ReactNode } | undefined => {
|
||||
const isInactive = isDeviceInactive(device);
|
||||
|
||||
if (!isInactive) {
|
||||
|
@ -89,7 +89,11 @@ const DeviceTile: React.FC<DeviceTileProps> = ({
|
|||
];
|
||||
|
||||
return <div className="mx_DeviceTile" data-testid={`device-tile-${device.device_id}`}>
|
||||
<DeviceType isVerified={device.isVerified} isSelected={isSelected} />
|
||||
<DeviceTypeIcon
|
||||
isVerified={device.isVerified}
|
||||
isSelected={isSelected}
|
||||
deviceType={device.deviceType}
|
||||
/>
|
||||
<div className="mx_DeviceTile_info" onClick={onClick}>
|
||||
<DeviceTileName device={device} />
|
||||
<div className="mx_DeviceTile_metadata">
|
||||
|
|
|
@ -1,56 +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 from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { DeviceWithVerification } from './types';
|
||||
|
||||
interface Props {
|
||||
isVerified?: DeviceWithVerification['isVerified'];
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export const DeviceType: React.FC<Props> = ({ isVerified, isSelected }) => (
|
||||
<div className={classNames('mx_DeviceType', {
|
||||
mx_DeviceType_selected: isSelected,
|
||||
})}
|
||||
>
|
||||
{ /* TODO(kerrya) all devices have an unknown type until PSG-650 */ }
|
||||
<UnknownDeviceIcon
|
||||
className='mx_DeviceType_deviceIcon'
|
||||
role='img'
|
||||
aria-label={_t('Unknown device type')}
|
||||
/>
|
||||
{
|
||||
isVerified
|
||||
? <VerifiedIcon
|
||||
className={classNames('mx_DeviceType_verificationIcon', 'verified')}
|
||||
role='img'
|
||||
aria-label={_t('Verified')}
|
||||
/>
|
||||
: <UnverifiedIcon
|
||||
className={classNames('mx_DeviceType_verificationIcon', 'unverified')}
|
||||
role='img'
|
||||
aria-label={_t('Unverified')}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
|
83
src/components/views/settings/devices/DeviceTypeIcon.tsx
Normal file
83
src/components/views/settings/devices/DeviceTypeIcon.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
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 from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
|
||||
import { Icon as DesktopIcon } from '../../../../../res/img/element-icons/settings/desktop.svg';
|
||||
import { Icon as WebIcon } from '../../../../../res/img/element-icons/settings/web.svg';
|
||||
import { Icon as MobileIcon } from '../../../../../res/img/element-icons/settings/mobile.svg';
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { DeviceType } from '../../../../utils/device/parseUserAgent';
|
||||
|
||||
interface Props {
|
||||
isVerified?: ExtendedDevice['isVerified'];
|
||||
isSelected?: boolean;
|
||||
deviceType?: DeviceType;
|
||||
}
|
||||
|
||||
const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
[DeviceType.Desktop]: DesktopIcon,
|
||||
[DeviceType.Mobile]: MobileIcon,
|
||||
[DeviceType.Web]: WebIcon,
|
||||
[DeviceType.Unknown]: UnknownDeviceIcon,
|
||||
};
|
||||
const deviceTypeLabel: Record<DeviceType, string> = {
|
||||
[DeviceType.Desktop]: _t('Desktop session'),
|
||||
[DeviceType.Mobile]: _t('Mobile session'),
|
||||
[DeviceType.Web]: _t('Web session'),
|
||||
[DeviceType.Unknown]: _t('Unknown session type'),
|
||||
};
|
||||
|
||||
export const DeviceTypeIcon: React.FC<Props> = ({
|
||||
isVerified,
|
||||
isSelected,
|
||||
deviceType,
|
||||
}) => {
|
||||
const Icon = deviceTypeIcon[deviceType] || deviceTypeIcon[DeviceType.Unknown];
|
||||
const label = deviceTypeLabel[deviceType] || deviceTypeLabel[DeviceType.Unknown];
|
||||
return (
|
||||
<div className={classNames('mx_DeviceTypeIcon', {
|
||||
mx_DeviceTypeIcon_selected: isSelected,
|
||||
})}
|
||||
>
|
||||
<div className='mx_DeviceTypeIcon_deviceIconWrapper'>
|
||||
<Icon
|
||||
className='mx_DeviceTypeIcon_deviceIcon'
|
||||
role='img'
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isVerified
|
||||
? <VerifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'verified')}
|
||||
role='img'
|
||||
aria-label={_t('Verified')}
|
||||
/>
|
||||
: <UnverifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'unverified')}
|
||||
role='img'
|
||||
aria-label={_t('Unverified')}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
};
|
||||
|
|
@ -21,11 +21,11 @@ import AccessibleButton from '../../elements/AccessibleButton';
|
|||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
|
||||
interface Props {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
onVerifyDevice?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ import SelectableDeviceTile from './SelectableDeviceTile';
|
|||
import {
|
||||
DevicesDictionary,
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
import { DevicesState } from './useOwnDevices';
|
||||
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
|
||||
|
@ -42,27 +42,27 @@ interface Props {
|
|||
devices: DevicesDictionary;
|
||||
pushers: IPusher[];
|
||||
localNotificationSettings: Map<string, LocalNotificationSettings>;
|
||||
expandedDeviceIds: DeviceWithVerification['device_id'][];
|
||||
signingOutDeviceIds: DeviceWithVerification['device_id'][];
|
||||
selectedDeviceIds: DeviceWithVerification['device_id'][];
|
||||
expandedDeviceIds: ExtendedDevice['device_id'][];
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][];
|
||||
filter?: DeviceSecurityVariation;
|
||||
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
|
||||
onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
|
||||
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
saveDeviceName: DevicesState['saveDeviceName'];
|
||||
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
|
||||
onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
setSelectedDeviceIds: (deviceIds: DeviceWithVerification['device_id'][]) => void;
|
||||
setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
}
|
||||
|
||||
const isDeviceSelected = (
|
||||
deviceId: DeviceWithVerification['device_id'],
|
||||
selectedDeviceIds: DeviceWithVerification['device_id'][],
|
||||
deviceId: ExtendedDevice['device_id'],
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][],
|
||||
) => selectedDeviceIds.includes(deviceId);
|
||||
|
||||
// devices without timestamp metadata should be sorted last
|
||||
const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) =>
|
||||
const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
|
||||
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
||||
|
@ -149,7 +149,7 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
|||
</div>;
|
||||
|
||||
const DeviceListItem: React.FC<{
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
pusher?: IPusher | undefined;
|
||||
localNotificationSettings?: LocalNotificationSettings | undefined;
|
||||
isExpanded: boolean;
|
||||
|
@ -227,11 +227,11 @@ export const FilteredDeviceList =
|
|||
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
||||
|
||||
function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined {
|
||||
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
|
||||
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||
}
|
||||
|
||||
const toggleSelection = (deviceId: DeviceWithVerification['device_id']): void => {
|
||||
const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
|
||||
// remove from selection
|
||||
setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId));
|
||||
|
|
|
@ -23,13 +23,13 @@ import DeviceSecurityCard from './DeviceSecurityCard';
|
|||
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
DevicesDictionary,
|
||||
} from './types';
|
||||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
currentDeviceId: DeviceWithVerification['device_id'];
|
||||
currentDeviceId: ExtendedDevice['device_id'];
|
||||
goToFilteredList: (filter: DeviceSecurityVariation) => void;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ const SecurityRecommendations: React.FC<Props> = ({
|
|||
currentDeviceId,
|
||||
goToFilteredList,
|
||||
}) => {
|
||||
const devicesArray = Object.values<DeviceWithVerification>(devices);
|
||||
const devicesArray = Object.values<ExtendedDevice>(devices);
|
||||
|
||||
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
|
||||
devicesArray,
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeviceWithVerification, DeviceSecurityVariation } from "./types";
|
||||
import { ExtendedDevice, DeviceSecurityVariation } from "./types";
|
||||
|
||||
type DeviceFilterCondition = (device: DeviceWithVerification) => boolean;
|
||||
type DeviceFilterCondition = (device: ExtendedDevice) => boolean;
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||
|
@ -32,7 +32,7 @@ const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
|||
};
|
||||
|
||||
export const filterDevicesBySecurityRecommendation = (
|
||||
devices: DeviceWithVerification[],
|
||||
devices: ExtendedDevice[],
|
||||
securityVariations: DeviceSecurityVariation[],
|
||||
) => {
|
||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||
|
|
|
@ -16,14 +16,17 @@ limitations under the License.
|
|||
|
||||
import { IMyDevice } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { ExtendedDeviceInformation } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
|
||||
export type ExtendedDeviceInfo = {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
export type ExtendedDeviceAppInfo = {
|
||||
// eg Element Web
|
||||
appName?: string;
|
||||
appVersion?: string;
|
||||
url?: string;
|
||||
};
|
||||
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceInfo;
|
||||
export type DevicesDictionary = Record<DeviceWithVerification['device_id'], ExtendedDevice>;
|
||||
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation;
|
||||
export type DevicesDictionary = Record<ExtendedDevice['device_id'], ExtendedDevice>;
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
MatrixEvent,
|
||||
PUSHER_DEVICE_ID,
|
||||
PUSHER_ENABLED,
|
||||
UNSTABLE_MSC3852_LAST_SEEN_UA,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
@ -34,8 +35,9 @@ import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifi
|
|||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
|
||||
import { DevicesDictionary, DeviceWithVerification, ExtendedDeviceInfo } from "./types";
|
||||
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
||||
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
const isDeviceVerified = (
|
||||
matrixClient: MatrixClient,
|
||||
|
@ -63,12 +65,12 @@ const isDeviceVerified = (
|
|||
}
|
||||
};
|
||||
|
||||
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceInfo => {
|
||||
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => {
|
||||
const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id);
|
||||
|
||||
return {
|
||||
clientName: name,
|
||||
clientVersion: version,
|
||||
appName: name,
|
||||
appVersion: version,
|
||||
url,
|
||||
};
|
||||
};
|
||||
|
@ -87,6 +89,7 @@ const fetchDevicesWithVerification = async (
|
|||
...device,
|
||||
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
|
||||
...parseDeviceExtendedInformation(matrixClient, device),
|
||||
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
|
||||
},
|
||||
}), {});
|
||||
|
||||
|
@ -104,10 +107,10 @@ export type DevicesState = {
|
|||
currentDeviceId: string;
|
||||
isLoadingDeviceList: boolean;
|
||||
// not provided when current session cannot request verification
|
||||
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
|
||||
requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise<VerificationRequest>;
|
||||
refreshDevices: () => Promise<void>;
|
||||
saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise<void>;
|
||||
saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise<void>;
|
||||
error?: OwnDevicesError;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
};
|
||||
|
@ -189,7 +192,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified;
|
||||
|
||||
const requestDeviceVerification = isCurrentDeviceVerified && userId
|
||||
? async (deviceId: DeviceWithVerification['device_id']) => {
|
||||
? async (deviceId: ExtendedDevice['device_id']) => {
|
||||
return await matrixClient.requestVerification(
|
||||
userId,
|
||||
[deviceId],
|
||||
|
@ -198,7 +201,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
: undefined;
|
||||
|
||||
const saveDeviceName = useCallback(
|
||||
async (deviceId: DeviceWithVerification['device_id'], deviceName: string): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise<void> => {
|
||||
const device = devices[deviceId];
|
||||
|
||||
// no change
|
||||
|
@ -219,7 +222,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
}, [matrixClient, devices, refreshDevices]);
|
||||
|
||||
const setPushNotifications = useCallback(
|
||||
async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise<void> => {
|
||||
try {
|
||||
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
|
||||
if (pusher) {
|
||||
|
|
|
@ -31,6 +31,8 @@ import PowerSelector from "../../../elements/PowerSelector";
|
|||
import SettingsFieldset from '../../SettingsFieldset';
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
||||
|
||||
interface IEventShowOpts {
|
||||
isState?: boolean;
|
||||
|
@ -60,6 +62,10 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
|
|||
[EventType.Reaction]: { isState: false, hideForSpace: true },
|
||||
[EventType.RoomRedaction]: { isState: false, hideForSpace: true },
|
||||
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
||||
[VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true },
|
||||
|
@ -252,6 +258,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events");
|
||||
}
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start %(brand)s calls");
|
||||
plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join %(brand)s calls");
|
||||
}
|
||||
|
||||
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
|
||||
"users_default": {
|
||||
|
@ -435,7 +446,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
let label = plEventsToLabels[eventType];
|
||||
if (label) {
|
||||
label = _t(label);
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
label = _t(label, { brand });
|
||||
} else {
|
||||
label = _t("Send %(eventType)s events", { eventType });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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, { useCallback, useMemo, useState } from 'react';
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import { useRoomState } from "../../../../../hooks/useRoomState";
|
||||
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
||||
|
||||
interface ElementCallSwitchProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
|
||||
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]);
|
||||
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
|
||||
const [content, events, maySend] = useRoomState(room, useCallback((state) => {
|
||||
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
return [
|
||||
content ?? {},
|
||||
content?.["events"] ?? {},
|
||||
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()),
|
||||
];
|
||||
}, []));
|
||||
|
||||
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
|
||||
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
|
||||
});
|
||||
|
||||
const onChange = useCallback((enabled: boolean): void => {
|
||||
setElementCallEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
|
||||
const moderatorLevel = content.kick ?? 50;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
|
||||
} else {
|
||||
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, {
|
||||
"events": events,
|
||||
...content,
|
||||
});
|
||||
}, [roomId, content, events, isPublic]);
|
||||
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
|
||||
return <LabelledToggleSwitch
|
||||
data-testid="element-call-switch"
|
||||
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
|
||||
caption={_t(
|
||||
"%(brand)s is end-to-end encrypted, " +
|
||||
"but is currently limited to smaller numbers of users.",
|
||||
{ brand },
|
||||
)}
|
||||
value={elementCallEnabled}
|
||||
onChange={onChange}
|
||||
disabled={!maySend}
|
||||
tooltip={_t("You do not have sufficient permissions to change this.")}
|
||||
/>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const VoipRoomSettingsTab: React.FC<Props> = ({ roomId }) => {
|
||||
return <SettingsTab heading={_t("Voice & Video")}>
|
||||
<SettingsSubsection heading={_t("Call type")}>
|
||||
<ElementCallSwitch roomId={roomId} />
|
||||
</SettingsSubsection>
|
||||
</SettingsTab>;
|
||||
};
|
|
@ -214,7 +214,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
{ _t("Share your activity and status with others.") }
|
||||
</span>
|
||||
<SettingsFlag
|
||||
disabled={!this.state.disablingReadReceiptsSupported}
|
||||
disabled={
|
||||
!this.state.disablingReadReceiptsSupported
|
||||
&& SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
|
||||
}
|
||||
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
|
||||
name="sendReadReceipts"
|
||||
level={SettingLevel.ACCOUNT}
|
||||
|
|
|
@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
|
|||
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
|
||||
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
|
||||
import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
||||
import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types';
|
||||
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
|
||||
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
|
||||
|
@ -38,10 +38,10 @@ const useSignOut = (
|
|||
onSignoutResolvedCallback: () => Promise<void>,
|
||||
): {
|
||||
onSignOutCurrentDevice: () => void;
|
||||
onSignOutOtherDevices: (deviceIds: DeviceWithVerification['device_id'][]) => Promise<void>;
|
||||
signingOutDeviceIds: DeviceWithVerification['device_id'][];
|
||||
onSignOutOtherDevices: (deviceIds: ExtendedDevice['device_id'][]) => Promise<void>;
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
} => {
|
||||
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
|
||||
const onSignOutCurrentDevice = () => {
|
||||
Modal.createDialog(
|
||||
|
@ -53,7 +53,7 @@ const useSignOut = (
|
|||
);
|
||||
};
|
||||
|
||||
const onSignOutOtherDevices = async (deviceIds: DeviceWithVerification['device_id'][]) => {
|
||||
const onSignOutOtherDevices = async (deviceIds: ExtendedDevice['device_id'][]) => {
|
||||
if (!deviceIds.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -96,8 +96,8 @@ const SessionManagerTab: React.FC = () => {
|
|||
supportsMSC3881,
|
||||
} = useOwnDevices();
|
||||
const [filter, setFilter] = useState<DeviceSecurityVariation>();
|
||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
|
@ -105,7 +105,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
const userId = matrixClient.getUserId();
|
||||
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
|
||||
|
||||
const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => {
|
||||
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
if (expandedDeviceIds.includes(deviceId)) {
|
||||
setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId));
|
||||
} else {
|
||||
|
@ -136,7 +136,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const onTriggerDeviceVerification = useCallback((deviceId: DeviceWithVerification['device_id']) => {
|
||||
const onTriggerDeviceVerification = useCallback((deviceId: ExtendedDevice['device_id']) => {
|
||||
if (!requestDeviceVerification) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
|||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { ConnectionState } from "../../../models/Call";
|
||||
import { Call, CallEvent, ElementCall, isConnected } from "../../../models/Call";
|
||||
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -35,7 +35,7 @@ import IconizedContextMenu, {
|
|||
} from "../context_menus/IconizedContextMenu";
|
||||
import { aboveLeftOf, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
|
@ -110,10 +110,11 @@ const MAX_FACES = 8;
|
|||
interface LobbyProps {
|
||||
room: Room;
|
||||
connect: () => Promise<void>;
|
||||
joinCallButtonDisabledTooltip?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
|
||||
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, connect, children }) => {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
@ -233,14 +234,15 @@ export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallView_connectButton"
|
||||
kind="primary"
|
||||
disabled={connecting}
|
||||
disabled={connecting || Boolean(joinCallButtonDisabledTooltip)}
|
||||
onClick={onConnectClick}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
title={_t("Join")}
|
||||
label={_t("Join")}
|
||||
tooltip={connecting ? _t("Connecting") : joinCallButtonDisabledTooltip}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -321,6 +323,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
|||
const cli = useContext(MatrixClientContext);
|
||||
const connected = isConnected(useConnectionState(call));
|
||||
const participants = useParticipants(call);
|
||||
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
// Disconnect from any other active calls first, since we don't yet support holding
|
||||
|
@ -344,7 +347,13 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
|||
</div>;
|
||||
}
|
||||
|
||||
lobby = <Lobby room={room} connect={connect}>{ facePile }</Lobby>;
|
||||
lobby = <Lobby
|
||||
room={room}
|
||||
connect={connect}
|
||||
joinCallButtonDisabledTooltip={joinCallButtonDisabledTooltip}
|
||||
>
|
||||
{ facePile }
|
||||
</Lobby>;
|
||||
}
|
||||
|
||||
return <div className="mx_CallView">
|
||||
|
|
|
@ -32,7 +32,7 @@ const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExp
|
|||
{ onMaximize && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||
onClick={onMaximize}
|
||||
title={_t("Fill Screen")}
|
||||
title={_t("Fill screen")}
|
||||
/> }
|
||||
{ onPin && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||
|
|
|
@ -201,7 +201,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<aside
|
||||
className={this.props.className}
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
|
@ -211,7 +211,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||
onStartMoving: this.onStartMoving,
|
||||
onResize: this.onResize,
|
||||
}) }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import LegacyCallView from "./LegacyCallView";
|
|||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -35,6 +34,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
|
|||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -116,7 +116,6 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall
|
|||
*/
|
||||
|
||||
export default class PipView extends React.Component<IProps, IState> {
|
||||
private settingsWatcherRef: string;
|
||||
private movePersistedElement = createRef<() => void>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -157,7 +156,6 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
|
@ -278,6 +276,14 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onViewCall = (): void =>
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.persistentRoomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
||||
public updateShowWidgetInPip(
|
||||
persistentWidgetId = this.state.persistentWidgetId,
|
||||
|
@ -323,18 +329,19 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
mx_LegacyCallView_large: !pipMode,
|
||||
});
|
||||
const roomId = this.state.persistentRoomId;
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
|
||||
const viewingCallRoom = this.state.viewedRoomId === roomId;
|
||||
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
|
||||
|
||||
pipContent = ({ onStartMoving, _onResize }) =>
|
||||
pipContent = ({ onStartMoving }) =>
|
||||
<div className={pipViewClasses}>
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||
pipMode={pipMode}
|
||||
callRooms={[roomForWidget]}
|
||||
onExpand={!viewingCallRoom && this.onExpand}
|
||||
onPin={viewingCallRoom && this.onPin}
|
||||
onMaximize={viewingCallRoom && this.onMaximize}
|
||||
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
|
||||
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
|
||||
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
|
||||
/>
|
||||
<PersistentApp
|
||||
persistentWidgetId={this.state.persistentWidgetId}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue