Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering
This commit is contained in:
commit
2bb7b6535f
107 changed files with 3477 additions and 2536 deletions
|
@ -25,6 +25,7 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
|
|||
import { _t } from '../../languageHandler';
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
|
@ -222,6 +223,8 @@ class FilePanel extends React.Component {
|
|||
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
|
||||
</div>);
|
||||
|
||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
|
||||
if (this.state.timelineSet) {
|
||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
|
||||
|
@ -232,6 +235,7 @@ class FilePanel extends React.Component {
|
|||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
|
|
|
@ -85,7 +85,6 @@ interface IProps {
|
|||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
currentRoomId: string;
|
||||
ConferenceHandler?: object;
|
||||
collapseLhs: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
|
@ -637,7 +636,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
viaServers={this.props.viaServers}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
break;
|
||||
|
|
|
@ -80,6 +80,7 @@ import { leaveRoomBehaviour } from "../../utils/membership";
|
|||
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
|
||||
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -148,7 +149,6 @@ interface IRoomInfo {
|
|||
interface IProps { // TODO type things better
|
||||
config: Record<string, any>;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ConferenceHandler?: any;
|
||||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||
enableGuest?: boolean;
|
||||
// the queryParams extracted from the [real] query-string of the URI
|
||||
|
@ -1016,6 +1016,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private async createRoom(defaultPublic = false) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
if (communityId) {
|
||||
// double check the user will have permission to associate this room with the community
|
||||
if (!CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
Modal.createTrackedDialog('Pre-failure to create room', '', ErrorDialog, {
|
||||
title: _t("Cannot create rooms in this community"),
|
||||
description: _t("You do not have permission to create rooms in this community."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
|
||||
|
||||
|
@ -1843,7 +1855,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
subtitle = `${this.subTitleStatus} ${subtitle}`;
|
||||
}
|
||||
document.title = `${SdkConfig.get().brand} ${subtitle}`;
|
||||
|
||||
const title = `${SdkConfig.get().brand} ${subtitle}`;
|
||||
|
||||
if (document.title !== title) {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusIndicator(state: string, prevState: string) {
|
||||
|
|
|
@ -69,7 +69,6 @@ import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
|||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import TintableSvg from "../views/elements/TintableSvg";
|
||||
import type * as ConferenceHandler from '../../VectorConferenceHandler';
|
||||
import {XOR} from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
|
||||
|
@ -84,8 +83,6 @@ if (DEBUG) {
|
|||
}
|
||||
|
||||
interface IProps {
|
||||
ConferenceHandler?: ConferenceHandler;
|
||||
|
||||
threepidInvite: IThreepidInvite,
|
||||
|
||||
// Any data about the room that would normally come from the homeserver
|
||||
|
@ -181,7 +178,6 @@ export interface IState {
|
|||
matrixClientIsReady: boolean;
|
||||
showUrlPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
displayConfCallNotification?: boolean;
|
||||
rejecting?: boolean;
|
||||
rejectError?: Error;
|
||||
}
|
||||
|
@ -488,8 +484,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
callState: callState,
|
||||
});
|
||||
|
||||
this.updateConfCallNotification();
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
|
@ -724,10 +718,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
callState = call.call_state;
|
||||
}
|
||||
|
||||
// possibly remove the conf call notification if we're now in
|
||||
// the conf
|
||||
this.updateConfCallNotification();
|
||||
|
||||
this.setState({
|
||||
callState: callState,
|
||||
});
|
||||
|
@ -1018,9 +1008,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
// rate limited because a power level change will emit an event for every member in the room.
|
||||
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
||||
// a member state changed in this room
|
||||
// refresh the conf call notification state
|
||||
this.updateConfCallNotification();
|
||||
this.updateDMState();
|
||||
|
||||
let memberCountInfluence = 0;
|
||||
|
@ -1049,30 +1036,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
||||
}
|
||||
|
||||
private updateConfCallNotification() {
|
||||
const room = this.state.room;
|
||||
if (!room || !this.props.ConferenceHandler) {
|
||||
return;
|
||||
}
|
||||
const confMember = room.getMember(
|
||||
this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
|
||||
);
|
||||
|
||||
if (!confMember) {
|
||||
return;
|
||||
}
|
||||
const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
|
||||
|
||||
// A conf call notification should be displayed if there is an ongoing
|
||||
// conf call but this cilent isn't a part of it.
|
||||
this.setState({
|
||||
displayConfCallNotification: (
|
||||
(!confCall || confCall.call_state === "ended") &&
|
||||
confMember.membership === "join"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private updateDMState() {
|
||||
const room = this.state.room;
|
||||
if (room.getMyMembership() != "join") {
|
||||
|
@ -1681,7 +1644,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!this.state.room) {
|
||||
return null;
|
||||
}
|
||||
return CallHandler.getCallForRoom(this.state.room.roomId);
|
||||
return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
// this has to be a proper method rather than an unnamed function,
|
||||
|
@ -1866,6 +1829,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
searchInProgress={this.state.searchInProgress}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearch={this.onSearch}
|
||||
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
|
||||
/>;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
|
@ -1924,9 +1888,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={this.context.credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
showApps={this.state.showApps}
|
||||
hideAppsDrawer={false}
|
||||
|
|
|
@ -343,6 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
let secondarySection = null;
|
||||
|
||||
if (prototypeCommunityName) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
|
@ -350,24 +351,36 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</span>
|
||||
</div>
|
||||
);
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
let settingsOption;
|
||||
let inviteOption;
|
||||
if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={this.onCommunitySettingsClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{settingsOption}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onCommunityMembersClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
/>
|
||||
{inviteOption}
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
secondarySection = (
|
||||
|
|
|
@ -40,11 +40,7 @@ interface IProps {
|
|||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
complexity: zxcvbn.ZXCVBNResult;
|
||||
}
|
||||
|
||||
class PassphraseField extends PureComponent<IProps, IState> {
|
||||
class PassphraseField extends PureComponent<IProps> {
|
||||
static defaultProps = {
|
||||
label: _td("Password"),
|
||||
labelEnterPassword: _td("Enter password"),
|
||||
|
@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
|||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||
};
|
||||
|
||||
state = { complexity: null };
|
||||
|
||||
public readonly validate = withValidation<this>({
|
||||
description: function() {
|
||||
const complexity = this.state.complexity;
|
||||
public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
|
||||
description: function(complexity) {
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
},
|
||||
deriveData: async ({ value }) => {
|
||||
if (!value) return null;
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
return scorePassword(value);
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
|||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
test: async function({ value }, complexity) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({ complexity });
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
valid: function(complexity) {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (this.state.complexity.score >= this.props.minScore) {
|
||||
if (complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
}
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.complexity;
|
||||
invalid: function(complexity) {
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -117,7 +117,9 @@ export default class SetPasswordDialog extends React.Component {
|
|||
autoFocusNewPasswordInput={true}
|
||||
shouldAskForEmail={true}
|
||||
onError={this._onPasswordChangeError}
|
||||
onFinished={this._onPasswordChanged} />
|
||||
onFinished={this._onPasswordChanged}
|
||||
buttonLabel={_t("Set Password")}
|
||||
/>
|
||||
<div className="error">
|
||||
{ this.state.error }
|
||||
</div>
|
||||
|
|
|
@ -289,7 +289,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
content = <div>
|
||||
<p>{_t("Use your Security Key to continue.")}</p>
|
||||
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
|
||||
<form
|
||||
className="mx_AccessSecretStorageDialog_primaryContainer"
|
||||
onSubmit={this._onRecoveryKeyNext}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
|
||||
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
|
||||
<Field
|
||||
|
@ -298,6 +303,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
forceValidity={this.state.recoveryKeyCorrect}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
|
||||
|
|
|
@ -18,11 +18,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'qs';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -34,37 +32,16 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
/**
|
||||
* Does template substitution on a URL (or any string). Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
function uriFromTemplate(uriTemplate, variables) {
|
||||
let out = uriTemplate;
|
||||
for (const [key, val] of Object.entries(variables)) {
|
||||
out = out.replace(
|
||||
'$' + key, encodeURIComponent(val),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||
import {MatrixCapabilities} from "matrix-widget-api";
|
||||
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -72,11 +49,13 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.app.id;
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onRevokeClicked = this._onRevokeClicked.bind(this);
|
||||
|
@ -89,7 +68,6 @@ export default class AppTile extends React.Component {
|
|||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
|
||||
this._contextMenuButton = createRef();
|
||||
this._appFrame = createRef();
|
||||
this._menu_bar = createRef();
|
||||
}
|
||||
|
||||
|
@ -108,12 +86,10 @@ export default class AppTile extends React.Component {
|
|||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||
};
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.app.url),
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
||||
|
@ -124,43 +100,6 @@ export default class AppTile extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the widget support a given capability
|
||||
* @param {string} capability Capability to check for
|
||||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
try {
|
||||
const parsed = new URL(urlString);
|
||||
|
||||
// TODO: Replace these with proper widget params
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||
parsed.searchParams.set('widgetId', this.props.app.id);
|
||||
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
|
||||
|
||||
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
|
||||
// in HTTP, but URL parsers encode them anyways.
|
||||
return parsed.toString().replace(/%24/g, '$');
|
||||
} catch (e) {
|
||||
console.error("Failed to add widget URL params:", e);
|
||||
return urlString;
|
||||
}
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.app.url);
|
||||
|
@ -176,7 +115,7 @@ export default class AppTile extends React.Component {
|
|||
componentDidMount() {
|
||||
// Only fetch IM token on mount if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
// Widget action listeners
|
||||
|
@ -190,93 +129,44 @@ export default class AppTile extends React.Component {
|
|||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Generify the name of this function. It's not just scalar tokens.
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
* Component initialisation is only complete when this function has resolved
|
||||
*/
|
||||
setScalarToken() {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
||||
console.warn('Widget does not match integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
_resetWidget(newProps) {
|
||||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
}
|
||||
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
console.warn("No integration manager - not setting scalar token", url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Pick the right manager for the widget
|
||||
|
||||
const defaultManager = managers.getPrimaryManager();
|
||||
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
|
||||
console.warn('Unknown integration manager, refusing to set auth token', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the token before loading the iframe as we need it to mangle the URL
|
||||
if (!this._scalarClient) {
|
||||
this._scalarClient = defaultManager.getScalarClient();
|
||||
}
|
||||
this._scalarClient.getScalarToken().then((token) => {
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this._addWurlParams(this.props.app.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: u.format(),
|
||||
initialising: false,
|
||||
});
|
||||
|
||||
// Fetch page title from remote content if not already set
|
||||
if (!this.state.widgetPageTitle && params.url) {
|
||||
this._fetchWidgetTitle(params.url);
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to get scalar_token", err);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
initialising: false,
|
||||
});
|
||||
_startWidget() {
|
||||
this._sgWidget.prepare().then(() => {
|
||||
this.setState({initialising: false});
|
||||
});
|
||||
}
|
||||
|
||||
_iframeRefChange = (ref) => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
this._sgWidget.start(ref);
|
||||
} else {
|
||||
this._resetWidget(this.props);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (nextProps.app.url !== this.props.app.url) {
|
||||
this._getNewState(nextProps);
|
||||
// Fetch IM token for new URL if we're showing and have permission to load
|
||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._resetWidget(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,9 +177,9 @@ export default class AppTile extends React.Component {
|
|||
loading: true,
|
||||
});
|
||||
}
|
||||
// Fetch IM token now that we're showing if we already have permission to load
|
||||
// Start the widget now that we're showing if we already have permission to load
|
||||
if (this.state.hasPermissionToLoad) {
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,7 +209,14 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
_onSnapshotClick() {
|
||||
WidgetUtils.snapshotWidget(this.props.app);
|
||||
this._sgWidget.widgetApi.takeScreenshot().then(data => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: data.screenshot,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error("Failed to take screenshot: ", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -327,35 +224,24 @@ export default class AppTile extends React.Component {
|
|||
* @private
|
||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||
*/
|
||||
_endWidgetActions() {
|
||||
let terminationPromise;
|
||||
|
||||
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
||||
// Wait for widget to terminate within a timeout
|
||||
const timeout = 2000;
|
||||
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
||||
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
||||
} else {
|
||||
terminationPromise = Promise.resolve();
|
||||
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||
if (this.iframe) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Element instance is located.
|
||||
this.iframe.src = 'about:blank';
|
||||
}
|
||||
|
||||
return terminationPromise.finally(() => {
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||
if (this._appFrame.current) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Element instance is located.
|
||||
this._appFrame.current.src = 'about:blank';
|
||||
}
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
});
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
|
@ -409,73 +295,18 @@ export default class AppTile extends React.Component {
|
|||
this._revokeWidgetPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
// Destroy the old widget messaging before starting it back up again. Some widgets
|
||||
// have startup routines that run when they are loaded, so we just need to reinitialize
|
||||
// the messaging for them.
|
||||
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
|
||||
this._setupWidgetMessaging();
|
||||
|
||||
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
|
||||
_onWidgetReady = () => {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(
|
||||
this.props.app.id,
|
||||
this.props.app.url,
|
||||
this._getRenderedUrl(),
|
||||
this.props.userWidget,
|
||||
this._appFrame.current.contentWindow,
|
||||
);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
let requestedWhitelistCapabilies = [];
|
||||
|
||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||
return this.indexOf(e)>=0;
|
||||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
|
||||
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
||||
// using this custom extension to the widget API.
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
widgetMessaging.flagReadyToContinue();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
|
||||
});
|
||||
}
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
}
|
||||
};
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.app.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
|
@ -493,20 +324,6 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_fetchWidgetTitle(url) {
|
||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||
if (widgetPageTitle) {
|
||||
this.setState({widgetPageTitle: widgetPageTitle});
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
}
|
||||
|
||||
_grantWidgetPermission() {
|
||||
const roomId = this.props.room.roomId;
|
||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||
|
@ -516,7 +333,7 @@ export default class AppTile extends React.Component {
|
|||
this.setState({hasPermissionToLoad: true});
|
||||
|
||||
// Fetch a token for the integration manager, now that we're allowed to
|
||||
this.setScalarToken();
|
||||
this._startWidget();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
|
@ -535,6 +352,7 @@ export default class AppTile extends React.Component {
|
|||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
this._sgWidget.stop();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
|
@ -572,40 +390,6 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the widget template variables in a url with their values
|
||||
*
|
||||
* @param {string} u The URL with template variables
|
||||
* @param {string} widgetType The widget's type
|
||||
*
|
||||
* @returns {string} url with temlate variables replaced
|
||||
*/
|
||||
_templatedUrl(u, widgetType: string) {
|
||||
const targetData = {};
|
||||
if (WidgetType.JITSI.matches(widgetType)) {
|
||||
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUser = MatrixClientPeg.get().getUser(myUserId);
|
||||
const vars = Object.assign(targetData, this.props.app.data, {
|
||||
'matrix_user_id': myUserId,
|
||||
'matrix_room_id': this.props.room.roomId,
|
||||
'matrix_display_name': myUser ? myUser.displayName : myUserId,
|
||||
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'theme': SettingsStore.getValue("theme"),
|
||||
});
|
||||
|
||||
if (vars.conferenceId === undefined) {
|
||||
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
|
||||
const parsedUrl = new URL(this.props.app.url);
|
||||
vars.conferenceId = parsedUrl.searchParams.get("confId");
|
||||
}
|
||||
|
||||
return uriFromTemplate(u, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're using a local version of the widget rather than loading the
|
||||
* actual widget URL
|
||||
|
@ -615,67 +399,11 @@ export default class AppTile extends React.Component {
|
|||
return WidgetType.JITSI.matches(this.props.app.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL used in the iframe
|
||||
* In cases where we supply our own UI for a widget, this is an internal
|
||||
* URL different to the one used if the widget is popped out to a separate
|
||||
* tab / browser
|
||||
*
|
||||
* @returns {string} url
|
||||
*/
|
||||
_getRenderedUrl() {
|
||||
let url;
|
||||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||
url = WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: true,
|
||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
||||
});
|
||||
url = this._addWurlParams(url);
|
||||
} else {
|
||||
url = this._getSafeUrl(this.state.widgetUrl);
|
||||
}
|
||||
return this._templatedUrl(url, this.props.app.type);
|
||||
}
|
||||
|
||||
_getPopoutUrl() {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
return this._templatedUrl(
|
||||
WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: false,
|
||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
||||
}),
|
||||
this.props.app.type,
|
||||
);
|
||||
} else {
|
||||
// use app.url, not state.widgetUrl, because we want the one without
|
||||
// the wURL params for the popped-out version.
|
||||
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
|
||||
}
|
||||
}
|
||||
|
||||
_getSafeUrl(u) {
|
||||
const parsedWidgetUrl = url.parse(u, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
|
||||
// We also need the dollar signs in-tact for variable substitution.
|
||||
return safeWidgetUrl.replace(/%24/g, '$');
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle !== this.formatAppTileName()) {
|
||||
title = this.state.widgetPageTitle;
|
||||
}
|
||||
|
||||
|
@ -698,9 +426,9 @@ export default class AppTile extends React.Component {
|
|||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
||||
this._endWidgetActions().then(() => {
|
||||
if (this._appFrame.current) {
|
||||
if (this.iframe) {
|
||||
// Reload iframe
|
||||
this._appFrame.current.src = this._getRenderedUrl();
|
||||
this.iframe.src = this._sgWidget.embedUrl;
|
||||
this.setState({});
|
||||
}
|
||||
});
|
||||
|
@ -708,13 +436,13 @@ export default class AppTile extends React.Component {
|
|||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
|
||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick() {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this._appFrame.current.src = this._appFrame.current.src;
|
||||
this.iframe.src = this.iframe.src;
|
||||
}
|
||||
|
||||
_onContextMenuClick = () => {
|
||||
|
@ -760,7 +488,7 @@ export default class AppTile extends React.Component {
|
|||
<AppPermission
|
||||
roomId={this.props.room.roomId}
|
||||
creatorUserId={this.props.creatorUserId}
|
||||
url={this.state.widgetUrl}
|
||||
url={this._sgWidget.embedUrl}
|
||||
isRoomEncrypted={isEncrypted}
|
||||
onPermissionGranted={this._grantWidgetPermission}
|
||||
/>
|
||||
|
@ -785,11 +513,11 @@ export default class AppTile extends React.Component {
|
|||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
allow={iframeFeatures}
|
||||
ref={this._appFrame}
|
||||
src={this._getRenderedUrl()}
|
||||
ref={this._iframeRefChange}
|
||||
src={this._sgWidget.embedUrl}
|
||||
allowFullScreen={true}
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded} />
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remain on screen, we must put it in
|
||||
|
@ -833,9 +561,10 @@ export default class AppTile extends React.Component {
|
|||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
||||
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
|
||||
&& this.props.show;
|
||||
|
||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||
contextMenu = (
|
||||
|
@ -943,9 +672,6 @@ AppTile.propTypes = {
|
|||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Optional function to be called on widget capability request
|
||||
// Called with an array of the requested capabilities
|
||||
onCapabilityRequest: PropTypes.func,
|
||||
// Is this an instance of a user widget
|
||||
userWidget: PropTypes.bool,
|
||||
};
|
||||
|
|
77
src/components/views/elements/DesktopBuildsNotice.tsx
Normal file
77
src/components/views/elements/DesktopBuildsNotice.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
Copyright 2020 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 EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import React from "react";
|
||||
|
||||
export enum WarningKind {
|
||||
Files,
|
||||
Search,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
isRoomEncrypted: boolean;
|
||||
kind: WarningKind;
|
||||
}
|
||||
|
||||
export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
|
||||
if (!isRoomEncrypted) return null;
|
||||
if (EventIndexPeg.get()) return null;
|
||||
|
||||
const {desktopBuilds, brand} = SdkConfig.get();
|
||||
|
||||
let text = null;
|
||||
let logo = null;
|
||||
if (desktopBuilds.available) {
|
||||
logo = <img src={desktopBuilds.logo} />;
|
||||
switch (kind) {
|
||||
case WarningKind.Files:
|
||||
text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
});
|
||||
break;
|
||||
case WarningKind.Search:
|
||||
text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
|
||||
a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (kind) {
|
||||
case WarningKind.Files:
|
||||
text = _t("This version of %(brand)s does not support viewing some encrypted files", {brand});
|
||||
break;
|
||||
case WarningKind.Search:
|
||||
text = _t("This version of %(brand)s does not support searching encrypted messages", {brand});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// for safety
|
||||
if (!text) {
|
||||
console.warn("Unknown desktop builds warning kind: ", kind);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DesktopBuildsNotice">
|
||||
{logo}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -82,6 +82,7 @@ export default class PersistentApp extends React.Component {
|
|||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
showMenubar={false}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,18 +21,19 @@ import classNames from "classnames";
|
|||
|
||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||
|
||||
interface IRule<T> {
|
||||
interface IRule<T, D = void> {
|
||||
key: string;
|
||||
final?: boolean;
|
||||
skip?(this: T, data: Data): boolean;
|
||||
test(this: T, data: Data): boolean | Promise<boolean>;
|
||||
valid?(this: T): string;
|
||||
invalid?(this: T): string;
|
||||
skip?(this: T, data: Data, derivedData: D): boolean;
|
||||
test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
|
||||
valid?(this: T, derivedData: D): string;
|
||||
invalid?(this: T, derivedData: D): string;
|
||||
}
|
||||
|
||||
interface IArgs<T> {
|
||||
rules: IRule<T>[];
|
||||
description(this: T): React.ReactChild;
|
||||
interface IArgs<T, D = void> {
|
||||
rules: IRule<T, D>[];
|
||||
description(this: T, derivedData: D): React.ReactChild;
|
||||
deriveData?(data: Data): Promise<D>;
|
||||
}
|
||||
|
||||
export interface IFieldState {
|
||||
|
@ -53,6 +54,10 @@ export interface IValidationResult {
|
|||
* @param {Function} description
|
||||
* Function that returns a string summary of the kind of value that will
|
||||
* meet the validation rules. Shown at the top of the validation feedback.
|
||||
* @param {Function} deriveData
|
||||
* Optional function that returns a Promise to an object of generic type D.
|
||||
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
|
||||
* Useful for doing calculations per-value update once rather than in each of the above rule methods.
|
||||
* @param {Object} rules
|
||||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
|
@ -66,7 +71,7 @@ export interface IValidationResult {
|
|||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
*/
|
||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
||||
export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
|
@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
};
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
const derivedData = deriveData ? await deriveData(data) : undefined;
|
||||
|
||||
const results = [];
|
||||
let valid = true;
|
||||
if (rules && rules.length) {
|
||||
|
@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
continue;
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
|
||||
if (rule.skip && rule.skip.call(this, data)) {
|
||||
if (rule.skip && rule.skip.call(this, data, derivedData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, data);
|
||||
const ruleValid = await rule.test.call(this, data, derivedData);
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
// the valid state, show it.
|
||||
const text = rule.valid.call(this);
|
||||
const text = rule.valid.call(this, derivedData);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
} else if (!ruleValid && rule.invalid) {
|
||||
// If the rule's result is invalid and has text to show for
|
||||
// the invalid state, show it.
|
||||
const text = rule.invalid.call(this);
|
||||
const text = rule.invalid.call(this, derivedData);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
|||
if (description) {
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const content = description.call(this);
|
||||
const content = description.call(this, derivedData);
|
||||
summary = <div className="mx_Validation_description">{content}</div>;
|
||||
}
|
||||
|
||||
|
|
76
src/components/views/messages/MJitsiWidgetEvent.tsx
Normal file
76
src/components/views/messages/MJitsiWidgetEvent.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2020 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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const url = this.props.mxEvent.getContent()['url'];
|
||||
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
|
||||
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
|
||||
|
||||
let joinCopy = _t('Join the conference at the top of this room');
|
||||
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) {
|
||||
joinCopy = _t('Join the conference from the room information card on the right');
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
// removed
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t('Video conference ended by %(senderName)s', {senderName})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (prevUrl) {
|
||||
// modified
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t('Video conference updated by %(senderName)s', {senderName})}
|
||||
</div>
|
||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
||||
{joinCopy}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// assume added
|
||||
return (
|
||||
<div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
|
||||
<div className='mx_MJitsiWidgetEvent_title'>
|
||||
{_t("Video conference started by %(senderName)s", {senderName})}
|
||||
</div>
|
||||
<div className='mx_MJitsiWidgetEvent_subtitle'>
|
||||
{joinCopy}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1296,7 +1296,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
|||
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
|
||||
const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe;
|
||||
const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe &&
|
||||
devices && devices.length > 0;
|
||||
|
||||
const setUpdating = (updating) => {
|
||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||
|
|
|
@ -29,16 +29,17 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
||||
import {Capability} from "../../../widgets/WidgetApi";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import classNames from "classnames";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { MatrixCapabilities } from "matrix-widget-api";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -77,9 +78,17 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
let snapshotButton;
|
||||
if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
|
||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
||||
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||
const onSnapshotClick = () => {
|
||||
WidgetUtils.snapshotWidget(app);
|
||||
widgetMessaging.takeScreenshot().then(data => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: data.screenshot,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error("Failed to take screenshot: ", err);
|
||||
});
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
|
|
|
@ -75,6 +75,15 @@ export default class RoomProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -150,7 +159,12 @@ export default class RoomProfileSettings extends React.Component {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
|
@ -169,10 +183,22 @@ export default class RoomProfileSettings extends React.Component {
|
|||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,15 +39,9 @@ export default class AuxPanel extends React.Component {
|
|||
showApps: PropTypes.bool, // Render apps
|
||||
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: PropTypes.object,
|
||||
|
||||
// set to true to show the file drop target
|
||||
draggingFile: PropTypes.bool,
|
||||
|
||||
// set to true to show the 'active conf call' banner
|
||||
displayConfCallNotification: PropTypes.bool,
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: PropTypes.number,
|
||||
|
@ -161,39 +155,9 @@ export default class AuxPanel extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let conferenceCallNotification = null;
|
||||
if (this.props.displayConfCallNotification) {
|
||||
let supportedText = '';
|
||||
let joinNode;
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
supportedText = _t(" (unsupported)");
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{ _t(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
{},
|
||||
{
|
||||
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
||||
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</span>);
|
||||
}
|
||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||
conferenceCallNotification = (
|
||||
<div className="mx_RoomView_ongoingConfCallNotification">
|
||||
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
|
||||
|
||||
{ joinNode }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const callView = (
|
||||
<CallView
|
||||
room={this.props.room}
|
||||
ConferenceHandler={this.props.conferenceHandler}
|
||||
onResize={this.props.onResize}
|
||||
maxVideoHeight={this.props.maxHeight}
|
||||
/>
|
||||
|
@ -276,7 +240,6 @@ export default class AuxPanel extends React.Component {
|
|||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
|
|
|
@ -619,13 +619,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
private onFormatAction = (action: Formatting) => {
|
||||
const range = getRangeForSelection(
|
||||
this.editorRef.current,
|
||||
this.props.model,
|
||||
document.getSelection());
|
||||
const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
|
||||
// trim the range as we want it to exclude leading/trailing spaces
|
||||
range.trim();
|
||||
|
||||
if (range.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this.modifiedFlag = true;
|
||||
switch (action) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import * as ObjectUtils from "../../../ObjectUtils";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {E2E_STATE} from "./E2EIcon";
|
||||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
|
||||
const eventTileTypes = {
|
||||
|
@ -111,6 +112,19 @@ export function getHandlerTile(ev) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
if (type === "im.vector.modular.widgets") {
|
||||
let type = ev.getContent()['type'];
|
||||
if (!type) {
|
||||
// deleted/invalid widget - try the past widget type
|
||||
type = ev.getPrevContent()['type'];
|
||||
}
|
||||
|
||||
if (WidgetType.JITSI.matches(type)) {
|
||||
return "messages.MJitsiWidgetEvent";
|
||||
}
|
||||
}
|
||||
|
||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
||||
}
|
||||
|
||||
|
@ -627,16 +641,18 @@ export default class EventTile extends React.Component {
|
|||
const msgtype = content.msgtype;
|
||||
const eventType = this.props.mxEvent.getType();
|
||||
|
||||
let tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === "m.room.encryption");
|
||||
(eventType === "m.room.encryption") ||
|
||||
(tileHandler === "messages.MJitsiWidgetEvent");
|
||||
let isInfoMessage = (
|
||||
!isBubbleMessage && eventType !== 'm.room.message' &&
|
||||
eventType !== 'm.sticker' && eventType !== 'm.room.create'
|
||||
);
|
||||
|
||||
let tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
// If we're showing hidden events in the timeline, we should use the
|
||||
// source tile when there's no regular tile for an event and also for
|
||||
// replace relations (which otherwise would display as a confusing
|
||||
|
|
|
@ -24,7 +24,6 @@ import {isValid3pidInvite} from "../../../RoomInvite";
|
|||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
|
@ -233,15 +232,10 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
roomMembers() {
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
m.membership === 'join' || m.membership === 'invite'
|
||||
) && (
|
||||
!ConferenceHandler ||
|
||||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
|
||||
);
|
||||
});
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -32,6 +33,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -85,9 +90,16 @@ VideoCallButton.propTypes = {
|
|||
};
|
||||
|
||||
function HangupButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onHangupClick = () => {
|
||||
const call = CallHandler.getCallForRoom(props.roomId);
|
||||
if (props.isConference) {
|
||||
dis.dispatch({
|
||||
action: props.canEndConference ? 'end_conference' : 'hangup_conference',
|
||||
room_id: props.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
@ -98,14 +110,28 @@ function HangupButton(props) {
|
|||
room_id: call.roomId,
|
||||
});
|
||||
};
|
||||
return (<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
|
||||
|
||||
let tooltip = _t("Hangup");
|
||||
if (props.isConference && props.canEndConference) {
|
||||
tooltip = _t("End conference");
|
||||
}
|
||||
|
||||
const canLeaveConference = !props.isConference ? true : props.isInConference;
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_hangup"
|
||||
onClick={onHangupClick}
|
||||
title={_t('Hangup')}
|
||||
/>);
|
||||
title={tooltip}
|
||||
disabled={!canLeaveConference}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
HangupButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
isConference: PropTypes.bool.isRequired,
|
||||
canEndConference: PropTypes.bool,
|
||||
isInConference: PropTypes.bool,
|
||||
};
|
||||
|
||||
const EmojiButton = ({addEmoji}) => {
|
||||
|
@ -226,12 +252,17 @@ export default class MessageComposer extends React.Component {
|
|||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
|
||||
this._dispatcherRef = null;
|
||||
|
||||
this.state = {
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
|
||||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -247,6 +278,14 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onWidgetUpdate = () => {
|
||||
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
|
||||
};
|
||||
|
||||
_onActiveWidgetUpdate = () => {
|
||||
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
|
@ -277,6 +316,8 @@ export default class MessageComposer extends React.Component {
|
|||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
|
@ -392,9 +433,19 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
if (this.state.showCallButtons) {
|
||||
if (callInProgress) {
|
||||
if (this.state.hasConference) {
|
||||
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
controls.push(
|
||||
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
|
||||
<HangupButton
|
||||
roomId={this.props.room.roomId}
|
||||
isConference={true}
|
||||
canEndConference={canEndConf}
|
||||
isInConference={this.state.joinedConference}
|
||||
/>,
|
||||
);
|
||||
} else if (callInProgress) {
|
||||
controls.push(
|
||||
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018-2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,6 +28,11 @@ export default class RoomUpgradeWarningBar extends React.Component {
|
|||
recommendation: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
|
||||
|
@ -35,6 +40,13 @@ export default class RoomUpgradeWarningBar extends React.Component {
|
|||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
_onStateEvents = (event, state) => {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -19,6 +20,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import classNames from "classnames";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
|
||||
|
||||
export default class SearchBar extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -72,21 +74,24 @@ export default class SearchBar extends React.Component {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="mx_SearchBar">
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
<>
|
||||
<div className="mx_SearchBar">
|
||||
<div className="mx_SearchBar_buttons" role="radiogroup">
|
||||
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
|
||||
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
|
||||
</div>
|
||||
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
|
||||
</div>
|
||||
<DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import * as sdk from '../../../index';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -30,6 +29,7 @@ import {ContextMenu} from "../../structures/ContextMenu";
|
|||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||
|
||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||
// We sit in a context menu, so this should be given to the context menu.
|
||||
|
@ -212,9 +212,11 @@ export default class Stickerpicker extends React.Component {
|
|||
|
||||
_sendVisibilityToWidget(visible) {
|
||||
if (!this.state.stickerpickerWidget) return;
|
||||
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
|
||||
if (widgetMessaging && visible !== this._prevSentVisibility) {
|
||||
widgetMessaging.sendVisibility(visible);
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
|
||||
if (messaging && visible !== this._prevSentVisibility) {
|
||||
messaging.updateVisibility(visible).catch(err => {
|
||||
console.error("Error updating widget visibility: ", err);
|
||||
});
|
||||
this._prevSentVisibility = visible;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback} from "react";
|
||||
import React, {useState} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const hoveringProps = {
|
||||
onMouseEnter: () => setIsHovering(true),
|
||||
onMouseLeave: () => setIsHovering(false),
|
||||
};
|
||||
|
||||
const openImageView = useCallback(() => {
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
Modal.createDialog(ImageView, {
|
||||
src: avatarUrl,
|
||||
name: avatarName,
|
||||
}, "mx_Dialog_lightbox");
|
||||
}, [avatarUrl, avatarName]);
|
||||
|
||||
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
|
||||
let avatarElement = <AccessibleButton
|
||||
element="div"
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_avatarPlaceholder"
|
||||
{...hoveringProps}
|
||||
/>;
|
||||
if (avatarUrl) {
|
||||
avatarElement = (
|
||||
<AccessibleButton
|
||||
|
@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
src={avatarUrl}
|
||||
alt={avatarAltText}
|
||||
aria-label={avatarAltText}
|
||||
onClick={openImageView} />
|
||||
onClick={uploadAvatar}
|
||||
{...hoveringProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let uploadAvatarBtn;
|
||||
if (uploadAvatar) {
|
||||
// insert an empty div to be the host for a css mask containing the upload.svg
|
||||
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
|
||||
{_t("Upload")}
|
||||
</AccessibleButton>;
|
||||
uploadAvatarBtn = <AccessibleButton
|
||||
onClick={uploadAvatar}
|
||||
className='mx_AvatarSetting_uploadButton'
|
||||
{...hoveringProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
let removeAvatarBtn;
|
||||
|
@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_AvatarSetting_avatar">
|
||||
{ avatarElement }
|
||||
{ uploadAvatarBtn }
|
||||
{ removeAvatarBtn }
|
||||
const avatarClasses = classNames({
|
||||
"mx_AvatarSetting_avatar": true,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
<div className="mx_AvatarSetting_hover">
|
||||
<div className="mx_AvatarSetting_hoverBg" />
|
||||
<span>{_t("Upload")}</span>
|
||||
</div>
|
||||
{uploadAvatarBtn}
|
||||
{removeAvatarBtn}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class ChangePassword extends React.Component {
|
|||
rowClassName: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
buttonKind: PropTypes.string,
|
||||
buttonLabel: PropTypes.string,
|
||||
confirm: PropTypes.bool,
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput: PropTypes.bool,
|
||||
|
@ -271,7 +272,7 @@ export default class ChangePassword extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
|
||||
{ _t('Change Password') }
|
||||
{ this.props.buttonLabel || _t('Change Password') }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -65,6 +65,15 @@ export default class ProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -144,18 +153,27 @@ export default class ProfileSettings extends React.Component {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<Field
|
||||
label={_t("Display Name")}
|
||||
type="text" value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{this.state.userId}
|
||||
{hostingSignup}
|
||||
</p>
|
||||
<Field label={_t("Display Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} />
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
|
@ -164,10 +182,22 @@ export default class ProfileSettings extends React.Component {
|
|||
uploadAvatar={this._uploadAvatar}
|
||||
removeAvatar={this._removeAvatar} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -221,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
_renderProfileSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<ProfileSettings />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import IncomingCallBox from './IncomingCallBox';
|
||||
import CallPreview from './CallPreview';
|
||||
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
|
||||
|
||||
interface IProps {
|
||||
|
||||
|
@ -31,7 +30,7 @@ export default class CallContainer extends React.PureComponent<IProps, IState> {
|
|||
public render() {
|
||||
return <div className="mx_CallContainer">
|
||||
<IncomingCallBox />
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<CallPreview />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,6 @@ import PersistentApp from "../elements/PersistentApp";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
// A Conference Handler implementation
|
||||
// Must have a function signature:
|
||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||
ConferenceHandler: any;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -47,7 +43,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,14 +73,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state':
|
||||
this.setState({
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onCallViewClick = () => {
|
||||
const call = CallHandler.getAnyActiveCall();
|
||||
const call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||
if (call) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -94,7 +90,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
||||
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
||||
const showCall = (
|
||||
this.state.activeCall &&
|
||||
this.state.activeCall.call_state === 'connected' &&
|
||||
|
@ -106,7 +102,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
|||
<CallView
|
||||
className="mx_CallPreview"
|
||||
onClick={this.onCallViewClick}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
showHangup={true}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -31,11 +31,6 @@ interface IProps {
|
|||
// room; if not, we will show any active call.
|
||||
room?: Room;
|
||||
|
||||
// A Conference Handler implementation
|
||||
// Must have a function signature:
|
||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||
ConferenceHandler?: any;
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
|
@ -96,14 +91,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
|
||||
if (this.props.room) {
|
||||
const roomId = this.props.room.roomId;
|
||||
call = CallHandler.getCallForRoom(roomId) ||
|
||||
(this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : null);
|
||||
call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||
|
||||
if (this.call) {
|
||||
this.setState({ call: call });
|
||||
}
|
||||
} else {
|
||||
call = CallHandler.getAnyActiveCall();
|
||||
call = CallHandler.sharedInstance().getAnyActiveCall();
|
||||
// Ignore calls if we can't get the room associated with them.
|
||||
// I think the underlying problem is that the js-sdk sends events
|
||||
// for calls before it has made the rooms available in the store,
|
||||
|
@ -115,20 +109,19 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (call) {
|
||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||
// always use a separate element for audio stream playback.
|
||||
// this is to let us move CallView around the DOM without interrupting remote audio
|
||||
// during playback, by having the audio rendered by a top-level <audio/> element.
|
||||
// rather than being rendered by the main remoteVideo <video/> element.
|
||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||
if (this.getVideoView()) {
|
||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||
|
||||
// always use a separate element for audio stream playback.
|
||||
// this is to let us move CallView around the DOM without interrupting remote audio
|
||||
// during playback, by having the audio rendered by a top-level <audio/> element.
|
||||
// rather than being rendered by the main remoteVideo <video/> element.
|
||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||
}
|
||||
}
|
||||
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||
// if this call is a conf call, don't display local video as the
|
||||
// conference will have us in it
|
||||
this.getVideoView().getLocalVideoElement().style.display = (
|
||||
call.confUserId ? "none" : "block"
|
||||
);
|
||||
this.getVideoView().getLocalVideoElement().style.display = "block";
|
||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||
} else {
|
||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
|||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'call_state': {
|
||||
const call = CallHandler.getCall(payload.room_id);
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
||||
if (call && call.call_state === 'ringing') {
|
||||
this.setState({
|
||||
incomingCall: call,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue