Merge remote-tracking branch 'origin/develop' into dbkr/call_hold
This commit is contained in:
commit
7796621e8d
52 changed files with 1995 additions and 144 deletions
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
|
@ -45,6 +46,8 @@ export default class CaptchaForm extends React.Component {
|
|||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -99,10 +102,12 @@ export default class CaptchaForm extends React.Component {
|
|||
console.log("Loaded recaptcha script.");
|
||||
try {
|
||||
this._renderRecaptcha(DIV_ID);
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
errorText: e.toString(),
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_error", { error: e.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -189,6 +190,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
_onCaptchaResponse = response => {
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
response: response,
|
||||
|
@ -297,6 +299,8 @@ export class TermsAuthEntry extends React.Component {
|
|||
toggledPolicies: initToggles,
|
||||
policies: pickedPolicies,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_terms_begin");
|
||||
}
|
||||
|
||||
|
||||
|
@ -326,8 +330,12 @@ export class TermsAuthEntry extends React.Component {
|
|||
allChecked = allChecked && checked;
|
||||
}
|
||||
|
||||
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
if (allChecked) {
|
||||
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
||||
} else {
|
||||
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -150,7 +151,20 @@ export default class PasswordLogin extends React.Component {
|
|||
this.props.onUsernameChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onUsernameFocus() {
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_focus");
|
||||
}
|
||||
}
|
||||
|
||||
onUsernameBlur(ev) {
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
||||
CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
|
||||
} else {
|
||||
CountlyAnalytics.instance.track("onboarding_login_email_blur");
|
||||
}
|
||||
this.props.onUsernameBlur(ev.target.value);
|
||||
}
|
||||
|
||||
|
@ -161,6 +175,7 @@ export default class PasswordLogin extends React.Component {
|
|||
loginType: loginType,
|
||||
username: "", // Reset because email and username use the same state
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
|
||||
}
|
||||
|
||||
onPhoneCountryChanged(country) {
|
||||
|
@ -176,8 +191,13 @@ export default class PasswordLogin extends React.Component {
|
|||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onPhoneNumberFocus() {
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
|
||||
}
|
||||
|
||||
onPhoneNumberBlur(ev) {
|
||||
this.props.onPhoneNumberBlur(ev.target.value);
|
||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
|
||||
}
|
||||
|
||||
onPasswordChanged(ev) {
|
||||
|
@ -202,6 +222,7 @@ export default class PasswordLogin extends React.Component {
|
|||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
|
@ -216,6 +237,7 @@ export default class PasswordLogin extends React.Component {
|
|||
label={_t("Username")}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onFocus={this.onUsernameFocus}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
|
@ -240,6 +262,7 @@ export default class PasswordLogin extends React.Component {
|
|||
value={this.state.phoneNumber}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onFocus={this.onPhoneNumberFocus}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocus}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
|||
import withValidation from '../elements/Validation';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -77,6 +78,8 @@ export default class RegistrationForm extends React.Component {
|
|||
passwordConfirm: this.props.defaultPassword || "",
|
||||
passwordComplexity: null,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_registration_begin");
|
||||
}
|
||||
|
||||
onSubmit = async ev => {
|
||||
|
@ -86,6 +89,7 @@ export default class RegistrationForm extends React.Component {
|
|||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -110,6 +114,8 @@ export default class RegistrationForm extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
|
@ -128,6 +134,11 @@ export default class RegistrationForm extends React.Component {
|
|||
|
||||
_doSubmit(ev) {
|
||||
const email = this.state.email.trim();
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_ok", {
|
||||
email: !!email,
|
||||
});
|
||||
|
||||
const promise = this.props.onRegisterClick({
|
||||
username: this.state.username.trim(),
|
||||
password: this.state.password.trim(),
|
||||
|
@ -422,6 +433,8 @@ export default class RegistrationForm extends React.Component {
|
|||
value={this.state.email}
|
||||
onChange={this.onEmailChange}
|
||||
onValidate={this.onEmailValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -433,6 +446,8 @@ export default class RegistrationForm extends React.Component {
|
|||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
onValidate={this.onPasswordValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_password_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_password_blur")}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -447,6 +462,8 @@ export default class RegistrationForm extends React.Component {
|
|||
value={this.state.passwordConfirm}
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
onValidate={this.onPasswordConfirmValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_passwordConfirm_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_passwordConfirm_blur")}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -487,6 +504,8 @@ export default class RegistrationForm extends React.Component {
|
|||
value={this.state.username}
|
||||
onChange={this.onUsernameChange}
|
||||
onValidate={this.onUsernameValidate}
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_username_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_username_blur")}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
|||
import SdkConfig from "../../../SdkConfig";
|
||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||
import classNames from 'classnames';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
|
@ -70,6 +71,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
isUrl: props.serverConfig.isUrl,
|
||||
showIdentityServer: false,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_custom_server");
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
|
|
|
@ -23,11 +23,18 @@ import AuthPage from "./AuthPage";
|
|||
import {_td} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
||||
export default class Welcome extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_welcome");
|
||||
}
|
||||
|
||||
render() {
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
|
||||
|
|
138
src/components/views/dialogs/FeedbackDialog.js
Normal file
138
src/components/views/dialogs/FeedbackDialog.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2018 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.
|
||||
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, {useState} from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Modal from "../../../Modal";
|
||||
import BugReportDialog from "./BugReportDialog";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
|
||||
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
|
||||
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
|
||||
|
||||
|
||||
export default (props) => {
|
||||
const [rating, setRating] = useState("");
|
||||
const [comment, setComment] = useState("");
|
||||
|
||||
const onDebugLogsLinkClick = () => {
|
||||
props.onFinished();
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
const hasFeedback = CountlyAnalytics.instance.canEnable();
|
||||
const onFinished = (sendFeedback) => {
|
||||
if (hasFeedback && sendFeedback) {
|
||||
CountlyAnalytics.instance.reportFeedback(parseInt(rating, 10), comment);
|
||||
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
|
||||
title: _t('Feedback sent'),
|
||||
description: _t('Thank you!'),
|
||||
});
|
||||
props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let countlyFeedbackSection;
|
||||
if (hasFeedback) {
|
||||
countlyFeedbackSection = <React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
|
||||
<h3>{_t("Rate %(brand)s", { brand })}</h3>
|
||||
|
||||
<p>{_t("Tell us below how you feel about %(brand)s so far.", { brand })}</p>
|
||||
<p>{_t("Please go into as much detail as you like, so we can track down the problem.")}</p>
|
||||
|
||||
<StyledRadioGroup
|
||||
name="feedbackRating"
|
||||
value={rating}
|
||||
onChange={setRating}
|
||||
definitions={[
|
||||
{ value: "1", label: "😠" },
|
||||
{ value: "2", label: "😞" },
|
||||
{ value: "3", label: "😑" },
|
||||
{ value: "4", label: "😄" },
|
||||
{ value: "5", label: "😍" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Field
|
||||
id="feedbackComment"
|
||||
label={_t("Add comment")}
|
||||
placeholder={_t("Comment")}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={comment}
|
||||
element="textarea"
|
||||
onChange={(ev) => {
|
||||
setComment(ev.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
let subheading;
|
||||
if (hasFeedback) {
|
||||
subheading = (
|
||||
<h2>{_t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand })}</h2>
|
||||
);
|
||||
}
|
||||
|
||||
return (<QuestionDialog
|
||||
className="mx_FeedbackDialog"
|
||||
hasCancelButton={!!hasFeedback}
|
||||
title={_t("Feedback")}
|
||||
description={<React.Fragment>
|
||||
{ subheading }
|
||||
|
||||
<div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
|
||||
<h3>{_t("Report a bug")}</h3>
|
||||
<p>{
|
||||
_t("Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. " +
|
||||
"No match? <newIssueLink>Start a new one</newIssueLink>.", {}, {
|
||||
existingIssuesLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={existingIssuesUrl}>{ sub }</a>;
|
||||
},
|
||||
newIssueLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
|
||||
},
|
||||
})
|
||||
}</p>
|
||||
<p>{
|
||||
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
|
||||
"to help us track down the problem.", {}, {
|
||||
debugLogsLink: sub => (
|
||||
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
|
||||
),
|
||||
})
|
||||
}</p>
|
||||
</div>
|
||||
{ countlyFeedbackSection }
|
||||
</React.Fragment>}
|
||||
button={hasFeedback ? _t("Send feedback") : _t("Go back")}
|
||||
buttonDisabled={hasFeedback && rating === ""}
|
||||
onFinished={onFinished}
|
||||
/>);
|
||||
};
|
|
@ -40,6 +40,7 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
|
|||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -325,6 +326,8 @@ export default class InviteDialog extends React.PureComponent {
|
|||
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
|
||||
// add banned users, so we don't try to invite them
|
||||
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
|
||||
|
||||
CountlyAnalytics.instance.trackBeginInvite(props.roomId);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -627,6 +630,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
};
|
||||
|
||||
_inviteUsers = () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
this.setState({busy: true});
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
|
@ -643,6 +647,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => {
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
|
165
src/components/views/dialogs/ModalWidgetDialog.tsx
Normal file
165
src/components/views/dialogs/ModalWidgetDialog.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
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 * as React from 'react';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {
|
||||
ClientWidgetApi,
|
||||
IModalWidgetCloseRequest,
|
||||
IModalWidgetOpenRequestData,
|
||||
IModalWidgetReturnData,
|
||||
ModalButtonKind,
|
||||
Widget,
|
||||
WidgetApiFromWidgetAction,
|
||||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
sourceWidgetId: string;
|
||||
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
messaging?: ClientWidgetApi;
|
||||
}
|
||||
|
||||
const MAX_BUTTONS = 3;
|
||||
|
||||
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
|
||||
private readonly widget: Widget;
|
||||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
|
||||
state: IState = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.widget = new Widget({
|
||||
...this.props.widgetDefinition,
|
||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||
id: `modal_${this.props.sourceWidgetId}`,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const driver = new StopGapWidgetDriver( []);
|
||||
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
|
||||
this.setState({messaging});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.messaging.off("ready", this.onReady);
|
||||
this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
|
||||
this.state.messaging.stop();
|
||||
}
|
||||
|
||||
private onReady = () => {
|
||||
this.state.messaging.sendWidgetConfig(this.props.widgetDefinition);
|
||||
};
|
||||
|
||||
private onLoad = () => {
|
||||
this.state.messaging.once("ready", this.onReady);
|
||||
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
|
||||
};
|
||||
|
||||
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
|
||||
this.props.onFinished(true, ev.detail.data);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const templated = this.widget.getCompleteUrl({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
});
|
||||
|
||||
const parsed = new URL(templated);
|
||||
|
||||
// Add in some legacy support sprinkles (for non-popout widgets)
|
||||
// TODO: Replace these with proper widget params
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||
parsed.searchParams.set('widgetId', this.widget.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.
|
||||
const widgetUrl = parsed.toString().replace(/%24/g, '$');
|
||||
|
||||
let buttons;
|
||||
if (this.props.widgetDefinition.buttons) {
|
||||
// show first button rightmost for a more natural specification
|
||||
buttons = this.props.widgetDefinition.buttons.slice(0, MAX_BUTTONS).reverse().map(def => {
|
||||
let kind = "secondary";
|
||||
switch (def.kind) {
|
||||
case ModalButtonKind.Primary:
|
||||
kind = "primary";
|
||||
break;
|
||||
case ModalButtonKind.Secondary:
|
||||
kind = "primary_outline";
|
||||
break
|
||||
case ModalButtonKind.Danger:
|
||||
kind = "danger";
|
||||
break;
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
|
||||
};
|
||||
|
||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
|
||||
{ def.label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={this.props.widgetDefinition.name || _t("Modal Widget")}
|
||||
className="mx_ModalWidgetDialog"
|
||||
contentId="mx_Dialog_content"
|
||||
onFinished={this.props.onFinished}
|
||||
>
|
||||
<div className="mx_ModalWidgetDialog_warning">
|
||||
<img
|
||||
src={require("../../../../res/img/element-icons/warning-badge.svg")}
|
||||
height="16"
|
||||
width="16"
|
||||
alt=""
|
||||
/>
|
||||
{_t("Data on this screen is shared with %(widgetDomain)s", {
|
||||
widgetDomain: parsed.hostname,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<iframe
|
||||
ref={this.appFrame}
|
||||
sandbox="allow-forms allow-scripts allow-same-origin"
|
||||
src={widgetUrl}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_ModalWidgetDialog_buttons">
|
||||
{ buttons }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
@ -26,12 +28,14 @@ export default class QuestionDialog extends React.Component {
|
|||
description: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
buttonDisabled: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -61,7 +65,7 @@ export default class QuestionDialog extends React.Component {
|
|||
}
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_QuestionDialog"
|
||||
className={classNames("mx_QuestionDialog", this.props.className)}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
|
@ -74,6 +78,7 @@ export default class QuestionDialog extends React.Component {
|
|||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
primaryDisabled={this.props.buttonDisabled}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 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.
|
||||
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 QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default (props) => {
|
||||
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
|
||||
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
|
||||
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
|
||||
|
||||
const description1 =
|
||||
_t("If you run into any bugs or have feedback you'd like to share, " +
|
||||
"please let us know on GitHub.");
|
||||
const description2 = _t("To help avoid duplicate issues, " +
|
||||
"please <existingIssuesLink>view existing issues</existingIssuesLink> " +
|
||||
"first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> " +
|
||||
"if you can't find it.", {},
|
||||
{
|
||||
existingIssuesLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={existingIssuesUrl}>{ sub }</a>;
|
||||
},
|
||||
newIssueLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
|
||||
},
|
||||
});
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Report bugs & give feedback")}
|
||||
description={<div><p>{description1}</p><p>{description2}</p></div>}
|
||||
button={_t("Go back")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
|
@ -85,6 +85,7 @@ export default class MImageBody extends React.Component {
|
|||
showImage() {
|
||||
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
|
||||
this.setState({showImage: true});
|
||||
this._downloadImage();
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
|
@ -253,10 +254,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
|
||||
_downloadImage() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
|
@ -289,9 +287,18 @@ export default class MImageBody extends React.Component {
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remember that the user wanted to show this particular image
|
||||
if (!this.state.showImage && localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true") {
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
|
||||
const showImage = this.state.showImage ||
|
||||
localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
|
||||
|
||||
if (showImage) {
|
||||
// Don't download anything becaue we don't want to display anything.
|
||||
this._downloadImage();
|
||||
this.setState({showImage: true});
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MFileBody from './MFileBody';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
|
@ -24,23 +23,34 @@ import { _t } from '../../../languageHandler';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
|
||||
export default class MVideoBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: any;
|
||||
/* called when the video has loaded */
|
||||
onHeightChanged: () => void;
|
||||
}
|
||||
|
||||
/* called when the video has loaded */
|
||||
onHeightChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
interface IState {
|
||||
decryptedUrl: string|null,
|
||||
decryptedThumbnailUrl: string|null,
|
||||
decryptedBlob: Blob|null,
|
||||
error: any|null,
|
||||
fetchingData: boolean,
|
||||
}
|
||||
|
||||
state = {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
};
|
||||
export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fetchingData: false,
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
thumbScale(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
||||
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||
if (!fullWidth || !fullHeight) {
|
||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||
// log this because it's spammy
|
||||
|
@ -61,7 +71,7 @@ export default class MVideoBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
_getContentUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
|
@ -70,7 +80,7 @@ export default class MVideoBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getThumbUrl() {
|
||||
_getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
|
@ -81,7 +91,8 @@ export default class MVideoBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
|
@ -92,26 +103,33 @@ export default class MVideoBody extends React.Component {
|
|||
return URL.createObjectURL(blob);
|
||||
});
|
||||
}
|
||||
let decryptedBlob;
|
||||
thumbnailPromise.then((thumbnailUrl) => {
|
||||
return decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return URL.createObjectURL(blob);
|
||||
}).then((contentUrl) => {
|
||||
try {
|
||||
const thumbnailUrl = await thumbnailPromise;
|
||||
if (autoplay) {
|
||||
console.log("Preloading video");
|
||||
const decryptedBlob = await decryptFile(content.file);
|
||||
const contentUrl = URL.createObjectURL(decryptedBlob);
|
||||
this.setState({
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedBlob: decryptedBlob,
|
||||
});
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
}).catch((err) => {
|
||||
} else {
|
||||
console.log("NOT preloading video");
|
||||
this.setState({
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedBlob: null,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.setState({
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,8 +142,35 @@ export default class MVideoBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async _videoOnPlay() {
|
||||
if (this._getContentUrl() || this.state.fetchingData || this.state.error) {
|
||||
// We have the file, we are fetching the file, or there is an error.
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
// To stop subsequent download attempts
|
||||
fetchingData: true,
|
||||
});
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (!content.file) {
|
||||
this.setState({
|
||||
error: "No file given in content",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const decryptedBlob = await decryptFile(content.file);
|
||||
const contentUrl = URL.createObjectURL(decryptedBlob);
|
||||
this.setState({
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedBlob: decryptedBlob,
|
||||
fetchingData: false,
|
||||
});
|
||||
this.props.onHeightChanged();
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
|
@ -136,7 +181,8 @@ export default class MVideoBody extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Important: If we aren't autoplaying and we haven't decrypred it yet, show a video with a poster.
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
|
@ -151,7 +197,6 @@ export default class MVideoBody extends React.Component {
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
|
@ -170,9 +215,9 @@ export default class MVideoBody extends React.Component {
|
|||
}
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<video className="mx_MVideoBody" src={contentUrl} alt={content.body}
|
||||
<video className="mx_MVideoBody" src={contentUrl} title={content.body}
|
||||
controls preload={preload} muted={autoplay} autoPlay={autoplay}
|
||||
height={height} width={width} poster={poster}>
|
||||
height={height} width={width} poster={poster} onPlay={this._videoOnPlay.bind(this)}>
|
||||
</video>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
|
@ -32,6 +32,7 @@ import BasicMessageComposer from "./BasicMessageComposer";
|
|||
import {Key} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -182,6 +183,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
@ -190,8 +192,9 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
|
|
@ -23,7 +23,6 @@ import CallHandler from '../../../CallHandler';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
@ -254,7 +253,6 @@ export default class MessageComposer extends React.Component {
|
|||
super(props);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
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);
|
||||
|
@ -262,7 +260,6 @@ export default class MessageComposer extends React.Component {
|
|||
this._dispatcherRef = null;
|
||||
|
||||
this.state = {
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
|
||||
|
@ -294,7 +291,6 @@ export default class MessageComposer extends React.Component {
|
|||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
}
|
||||
|
||||
|
@ -318,9 +314,6 @@ export default class MessageComposer extends React.Component {
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -341,12 +334,6 @@ export default class MessageComposer extends React.Component {
|
|||
return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate() {
|
||||
const replyToEvent = RoomViewStore.getQuotingEvent();
|
||||
if (this.state.replyToEvent === replyToEvent) return;
|
||||
this.setState({ replyToEvent });
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
// Merge the new input state with old to support partial updates
|
||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||
|
@ -371,6 +358,7 @@ export default class MessageComposer extends React.Component {
|
|||
event_id: createEventId,
|
||||
room_id: replacementRoomId,
|
||||
auto_join: true,
|
||||
_type: "tombstone", // instrumentation
|
||||
|
||||
// Try to join via the server that sent the event. This converts @something:example.org
|
||||
// into a server domain by splitting on colons and ignoring the first entry ("@something").
|
||||
|
@ -383,7 +371,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
renderPlaceholderText() {
|
||||
if (this.state.replyToEvent) {
|
||||
if (this.props.replyToEvent) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
|
@ -429,7 +417,7 @@ export default class MessageComposer extends React.Component {
|
|||
placeholder={this.renderPlaceholderText()}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
/>,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
|
|
|
@ -42,6 +42,7 @@ import {Key} from "../../../Keyboard";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -304,9 +305,10 @@ export default class SendMessageComposer extends React.Component {
|
|||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
if (shouldSend) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
this.context.sendMessage(roomId, content);
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -316,6 +318,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
||||
|
|
|
@ -25,6 +25,7 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
|
|||
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
|
||||
import {SettingLevel} from "../../../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
export default class SecurityRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -350,6 +351,16 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
let historySection = (<>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{this._renderHistory()}
|
||||
</div>
|
||||
</>);
|
||||
if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
|
||||
historySection = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
|
@ -371,10 +382,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
{this._renderRoomAccess()}
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{this._renderHistory()}
|
||||
</div>
|
||||
{historySection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import SecureBackupPanel from "../../SecureBackupPanel";
|
|||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
|
||||
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -102,6 +103,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
_updateAnalytics = (checked) => {
|
||||
checked ? Analytics.enable() : Analytics.disable();
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
|
@ -339,7 +341,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
let privacySection;
|
||||
if (Analytics.canEnable()) {
|
||||
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
|
||||
privacySection = <React.Fragment>
|
||||
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue