@@ -488,6 +489,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
value={CREATE_STORAGE_OPTION_PASSPHRASE}
name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
+ onChange={this._onKeyPassphraseChange}
outlined
>
@@ -509,7 +511,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
)}
-
+
{optionKey}
{optionPassphrase}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index f7665fea8a..f37da03e47 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -29,6 +29,7 @@ import 'focus-visible';
import 'what-input';
import Analytics from "../../Analytics";
+import CountlyAnalytics from "../../CountlyAnalytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg";
@@ -109,7 +110,8 @@ export enum Views {
// flow to setup SSSS / cross-signing on this account
E2E_SETUP = 7,
- // we are logged in with an active matrix client.
+ // we are logged in with an active matrix client. The logged_in state also
+ // includes guests users as they too are logged in at the client level.
LOGGED_IN = 8,
// We are logged out (invalid token) but have our local state again. The user
@@ -349,6 +351,7 @@ export default class MatrixChat extends React.PureComponent
{
if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable();
}
+ CountlyAnalytics.instance.enable(/* anonymous = */ true);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
@@ -363,6 +366,7 @@ export default class MatrixChat extends React.PureComponent {
if (this.shouldTrackPageChange(prevState, this.state)) {
const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs);
+ CountlyAnalytics.instance.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.fire(Action.FocusComposer);
@@ -415,6 +419,8 @@ export default class MatrixChat extends React.PureComponent {
} else {
dis.dispatch({action: "view_welcome_page"});
}
+ } else if (SettingsStore.getValue("analyticsOptIn")) {
+ CountlyAnalytics.instance.enable(/* anonymous = */ false);
}
});
// Note we don't catch errors from this: we catch everything within
@@ -750,7 +756,12 @@ export default class MatrixChat extends React.PureComponent {
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
- Analytics.enable();
+ if (Analytics.canEnable()) {
+ Analytics.enable();
+ }
+ if (CountlyAnalytics.instance.canEnable()) {
+ CountlyAnalytics.instance.enable(/* anonymous = */ false);
+ }
break;
case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
@@ -1200,7 +1211,9 @@ export default class MatrixChat extends React.PureComponent {
StorageManager.tryPersistStorage();
- if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
+ if (SettingsStore.getValue("showCookieBar") &&
+ (Analytics.canEnable() || CountlyAnalytics.instance.canEnable())
+ ) {
showAnalyticsToast(this.props.config.piwik?.policyUrl);
}
}
@@ -1581,6 +1594,9 @@ export default class MatrixChat extends React.PureComponent {
action: 'require_registration',
});
} else if (screen === 'directory') {
+ if (this.state.view === Views.WELCOME) {
+ CountlyAnalytics.instance.track("onboarding_room_directory");
+ }
dis.fire(Action.ViewRoomDirectory);
} else if (screen === "start_sso" || screen === "start_cas") {
// TODO if logged in, skip SSO
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 97e1f82a77..e5c828b442 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -33,6 +33,7 @@ import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
+import CountlyAnalytics from "../../CountlyAnalytics";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
@@ -49,6 +50,8 @@ export default class RoomDirectory extends React.Component {
constructor(props) {
super(props);
+ this.startTime = CountlyAnalytics.getTimestamp();
+
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
this.state = {
publicRooms: [],
@@ -198,6 +201,11 @@ export default class RoomDirectory extends React.Component {
return;
}
+ if (this.state.filterString) {
+ const count = data.total_room_count_estimate || data.chunk.length;
+ CountlyAnalytics.instance.trackRoomDirectorySearch(count, this.state.filterString);
+ }
+
this.nextBatch = data.next_batch;
this.setState((s) => {
s.publicRooms.push(...(data.chunk || []));
@@ -407,7 +415,7 @@ export default class RoomDirectory extends React.Component {
};
onCreateRoomClick = room => {
- this.props.onFinished();
+ this.onFinished();
dis.dispatch({
action: 'view_create_room',
public: true,
@@ -419,11 +427,12 @@ export default class RoomDirectory extends React.Component {
}
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
- this.props.onFinished();
+ this.onFinished();
const payload = {
action: 'view_room',
auto_join: autoJoin,
should_peek: shouldPeek,
+ _type: "room_directory", // instrumentation
};
if (room) {
// Don't let the user view a room they won't be able to either
@@ -575,6 +584,11 @@ export default class RoomDirectory extends React.Component {
}
};
+ onFinished = () => {
+ CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
+ this.props.onFinished();
+ };
+
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@@ -693,7 +707,7 @@ export default class RoomDirectory extends React.Component {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 372186ff38..e29daadd8e 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -129,6 +129,7 @@ export interface IState {
initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
+ replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
@@ -315,6 +316,7 @@ export default class RoomView extends React.Component {
joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
+ replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
@@ -1111,6 +1113,7 @@ export default class RoomView extends React.Component {
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
+ _type: "unknown", // TODO: instrumentation
});
return Promise.resolve();
});
@@ -1899,6 +1902,7 @@ export default class RoomView extends React.Component {
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier}
+ replyToEvent={this.state.replyToEvent}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
/>;
}
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 64ee94628e..4847d41fa8 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -23,7 +23,7 @@ import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
-import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
+import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
@@ -186,7 +186,7 @@ export default class UserMenu extends React.Component {
ev.preventDefault();
ev.stopPropagation();
- Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
+ Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
this.setState({contextMenuPosition: null}); // also close the menu
};
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index 3fa2713a35..54d4b5de83 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -26,6 +26,7 @@ import PasswordReset from "../../../PasswordReset";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage";
+import CountlyAnalytics from "../../../CountlyAnalytics";
// Phases
// Show controls to configure server details
@@ -64,6 +65,12 @@ export default class ForgotPassword extends React.Component {
serverRequiresIdServer: null,
};
+ constructor(props) {
+ super(props);
+
+ CountlyAnalytics.instance.track("onboarding_forgot_password_begin");
+ }
+
componentDidMount() {
this.reset = null;
this._checkServerLiveliness(this.props.serverConfig);
@@ -299,6 +306,8 @@ export default class ForgotPassword extends React.Component {
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
autoFocus
+ onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
+ onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
/>
@@ -308,6 +317,8 @@ export default class ForgotPassword extends React.Component {
label={_t('Password')}
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
+ onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
+ onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
/>
CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
+ onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
/>
{_t(
diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js
index 118eed59e3..c3cbac0442 100644
--- a/src/components/structures/auth/Login.js
+++ b/src/components/structures/auth/Login.js
@@ -30,6 +30,7 @@ import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
+import CountlyAnalytics from "../../../CountlyAnalytics";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@@ -126,6 +127,8 @@ export default class LoginComponent extends React.Component {
'm.login.cas': () => this._renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"),
};
+
+ CountlyAnalytics.instance.track("onboarding_login_begin");
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js
index 783d519621..5cce93f0b8 100644
--- a/src/components/views/auth/CaptchaForm.js
+++ b/src/components/views/auth/CaptchaForm.js
@@ -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() });
}
}
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 47263c1e21..f49e6959fb 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -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() {
diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js
index 3bd9b557bc..405f9051b9 100644
--- a/src/components/views/auth/PasswordLogin.js
+++ b/src/components/views/auth/PasswordLogin.js
@@ -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}
diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js
index c07486d3bd..db7d1df994 100644
--- a/src/components/views/auth/RegistrationForm.js
+++ b/src/components/views/auth/RegistrationForm.js
@@ -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")}
/>;
}
diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js
index ee6f57a521..e04bf9e25a 100644
--- a/src/components/views/auth/ServerConfig.js
+++ b/src/components/views/auth/ServerConfig.js
@@ -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
diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js
index 21032f4f1a..0205f4e0b9 100644
--- a/src/components/views/auth/Welcome.js
+++ b/src/components/views/auth/Welcome.js
@@ -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');
diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js
new file mode 100644
index 0000000000..2515377709
--- /dev/null
+++ b/src/components/views/dialogs/FeedbackDialog.js
@@ -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 =
+
+
+
{_t("Rate %(brand)s", { brand })}
+
+
{_t("Tell us below how you feel about %(brand)s so far.", { brand })}
+
{_t("Please go into as much detail as you like, so we can track down the problem.")}
+
+
+
+
+ ;
+ }
+
+ let subheading;
+ if (hasFeedback) {
+ subheading = (
+ {_t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand })}
+ );
+ }
+
+ return (
+ { subheading }
+
+
+
{_t("Report a bug")}
+
{
+ _t("Please view existing bugs on Github first. " +
+ "No match? Start a new one.", {}, {
+ existingIssuesLink: (sub) => {
+ return { sub };
+ },
+ newIssueLink: (sub) => {
+ return { sub };
+ },
+ })
+ }
+
{
+ _t("PRO TIP: If you start a bug, please submit debug logs " +
+ "to help us track down the problem.", {}, {
+ debugLogsLink: sub => (
+ {sub}
+ ),
+ })
+ }
+
+ { countlyFeedbackSection }
+ }
+ button={hasFeedback ? _t("Send feedback") : _t("Go back")}
+ buttonDisabled={hasFeedback && rating === ""}
+ onFinished={onFinished}
+ />);
+};
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 73101056f3..fc3245aa18 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -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();
}
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
new file mode 100644
index 0000000000..6ce3230a7a
--- /dev/null
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -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 {
+ private readonly widget: Widget;
+ private appFrame: React.RefObject = 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) => {
+ 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
+ { def.label }
+ ;
+ });
+ }
+
+ return
+
+
+ {_t("Data on this screen is shared with %(widgetDomain)s", {
+ widgetDomain: parsed.hostname,
+ })}
+
+
+
+
+
+ { buttons }
+
+ ;
+ }
+}
diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js
index d6de60195f..3d90236b08 100644
--- a/src/components/views/dialogs/QuestionDialog.js
+++ b/src/components/views/dialogs/QuestionDialog.js
@@ -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 (
{
- 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 view existing issues " +
- "first (and add a +1) or create a new issue " +
- "if you can't find it.", {},
- {
- existingIssuesLink: (sub) => {
- return { sub };
- },
- newIssueLink: (sub) => {
- return { sub };
- },
- });
-
- return ({description1}
{description2}
}
- button={_t("Go back")}
- onFinished={props.onFinished}
- />);
-};
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index c92ae475bf..a8cdc17abf 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -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});
}
diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.tsx
similarity index 67%
rename from src/components/views/messages/MVideoBody.js
rename to src/components/views/messages/MVideoBody.tsx
index 86bf41699b..fb987a4f0d 100644
--- a/src/components/views/messages/MVideoBody.js
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -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
{
+ 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 (
-
diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js
index 78c7de887d..6fc1d3e1ad 100644
--- a/src/components/views/rooms/EditMessageComposer.js
+++ b/src/components/views/rooms/EditMessageComposer.js
@@ -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
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 40eaf77272..4ddff8f4b0 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -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}
/>,
,
,
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 4828277d8a..9438cceef5 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -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);
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
index 0a0c693158..f72e78fa3f 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
@@ -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 = (<>
+ {_t("Who can read history?")}
+
+ {this._renderHistory()}
+
+ >);
+ if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
+ historySection = null;
+ }
+
return (
{_t("Security & Privacy")}
@@ -371,10 +382,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
{this._renderRoomAccess()}
- {_t("Who can read history?")}
-
- {this._renderHistory()}
-
+ {historySection}
);
}
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index 61402e8881..a0d9016ce2 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -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 =