Merge pull request #4624 from matrix-org/t3chguy/toasts3_2

Migrate Banners to Toasts
This commit is contained in:
Michael Telatynski 2020-05-28 18:21:28 +01:00 committed by GitHub
commit 0242b6f3f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 506 additions and 626 deletions

View file

@ -67,7 +67,3 @@ limitations under the License.
.mx_MatrixToolbar_action {
margin-right: 16px;
}
.mx_MatrixToolbar_changelog {
white-space: pre;
}

View file

@ -575,10 +575,12 @@ async function startMatrixClient(startSyncing=true) {
// to work).
dis.dispatch({action: 'will_start_client'}, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
ToastStore.sharedInstance().reset();
Notifier.start();
UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start();

View file

@ -26,6 +26,10 @@ import * as sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import {
showToast as showNotificationsToast,
hideToast as hideNotificationsToast,
} from "./toasts/DesktopNotificationsToast";
/*
* Dispatches:
@ -184,6 +188,10 @@ const Notifier = {
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false;
this.isSyncing = false;
if (this.shouldShowToolbar()) {
showNotificationsToast();
}
},
stop: function() {
@ -278,12 +286,7 @@ const Notifier = {
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled(),
});
hideNotificationsToast();
// update the info to localStorage for persistent settings
if (persistent && global.localStorage) {

View file

@ -43,6 +43,15 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
hideToast as hideSetPasswordToast
} from "../../toasts/SetPasswordToast";
import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -65,10 +74,6 @@ interface IProps {
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
@ -86,10 +91,8 @@ interface IProps {
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
@ -97,8 +100,6 @@ interface IState {
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/**
@ -141,11 +142,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
};
// stash the MatrixClient in case we log out before we are unmounted
@ -182,10 +180,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed
if (
(prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
(prevProps.checkingForUpdate !== this.props.checkingForUpdate)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
@ -220,9 +215,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
};
_setStateFromSessionStore = () => {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
if (this._sessionStore.getCachedPassword()) {
showSetPasswordToast();
} else {
hideSetPasswordToast();
}
};
_createResizer() {
@ -294,6 +291,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(data);
}
};
@ -304,11 +303,24 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}
};
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
} else {
hideServerLimitToast();
}
}
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = [];
const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
@ -318,12 +330,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) pinnedEvents.push(event);
if (event) events.push(event);
}
}
this.setState({
serverNoticeEvents: pinnedEvents,
const usageLimitEvent = events.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
};
_onPaste = (ev) => {
@ -599,12 +617,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
let pageElement;
@ -648,40 +661,9 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
break;
}
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
let topBar;
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
topBar = <ServerLimitBar kind='hard'
adminContact={this.state.syncErrorData.error.data.admin_contact}
limitType={this.state.syncErrorData.error.data.limit_type}
/>;
} else if (usageLimitEvent) {
topBar = <ServerLimitBar kind='soft'
adminContact={usageLimitEvent.getContent().admin_contact}
limitType={usageLimitEvent.getContent().limit_type}
/>;
} else if (this.props.showCookieBar &&
this.props.config.piwik &&
navigator.doNotTrack !== "1"
) {
const policyUrl = this.props.config.piwik.policyUrl || null;
topBar = <CookieBar policyUrl={policyUrl} />;
} else if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (this.props.showNotifierToolbar) {
topBar = <MatrixToolbar />;
}
let bodyClasses = 'mx_MatrixChat';

View file

@ -67,6 +67,10 @@ import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView";
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions";
import {
showToast as showAnalyticsToast,
hideToast as hideAnalyticsToast
} from "../../toasts/AnalyticsToast";
/** constants for MatrixChat.state.view */
export enum Views {
@ -168,12 +172,7 @@ interface IState {
leftDisabled: boolean;
middleDisabled: boolean;
// the right panel's disabled state is tracked in its store.
version?: string;
newVersion?: string;
hasNewVersion: boolean;
newVersionReleaseNotes?: string;
checkingForUpdate?: string; // updateCheckStatusEnum
showCookieBar: boolean;
// Parameters used in the registration dance with the IS
register_client_secret?: string;
register_session_id?: string;
@ -183,7 +182,6 @@ interface IState {
hideToSRUsers: boolean;
syncError?: Error;
resizeNotifier: ResizeNotifier;
showNotifierToolbar: boolean;
serverConfig?: ValidatedServerConfig;
ready: boolean;
thirdPartyInvite?: object;
@ -227,17 +225,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
leftDisabled: false,
middleDisabled: false,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
showCookieBar: false,
hideToSRUsers: false,
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(),
showNotifierToolbar: false,
ready: false,
};
@ -338,12 +331,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
if (SettingsStore.getValue("showCookieBar")) {
this.setState({
showCookieBar: true,
});
}
if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable();
}
@ -685,9 +672,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({action: 'view_my_groups'});
}
break;
case 'notifier_enabled':
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
break;
case 'hide_left_panel':
this.setState({
collapseLhs: true,
@ -735,12 +719,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'client_started':
this.onClientStarted();
break;
case 'new_version':
this.onVersion(
payload.currentVersion, payload.newVersion,
payload.releaseNotes,
);
break;
case 'check_updates':
this.setState({ checkingForUpdate: payload.value });
break;
@ -760,19 +738,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'accept_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
this.setState({
showCookieBar: false,
});
hideAnalyticsToast();
Analytics.enable();
break;
case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
this.setState({
showCookieBar: false,
});
hideAnalyticsToast();
break;
}
};
@ -1261,6 +1233,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
}
}
private showScreenAfterLogin() {
@ -1391,7 +1367,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({action: 'focus_composer'});
this.setState({
ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
});
});
cli.on('Call.incoming', function(call) {
@ -1833,16 +1808,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("settings");
};
onVersion(current: string, latest: string, releaseNotes?: string) {
this.setState({
version: current,
newVersion: latest,
hasNewVersion: current !== latest,
newVersionReleaseNotes: releaseNotes,
checkingForUpdate: null,
});
}
onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get();
if (!cli) {
@ -2037,7 +2002,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
showCookieBar={this.state.showCookieBar}
/>
);
} else {

View file

@ -1,103 +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 PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
export default class CookieBar extends React.Component {
static propTypes = {
policyUrl: PropTypes.string,
}
constructor() {
super();
}
onUsageDataClicked(e) {
e.stopPropagation();
e.preventDefault();
Analytics.showDetailsModal();
}
onAccept() {
dis.dispatch({
action: 'accept_cookies',
});
}
onReject() {
dis.dispatch({
action: 'reject_cookies',
});
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const toolbarClasses = "mx_MatrixToolbar";
return (
<div className={toolbarClasses}>
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{ this.props.policyUrl ? _t(
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie " +
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
// XXX: We need to link to the page that explains our cookies
'PolicyLink': (sub) => <a
className="mx_MatrixToolbar_link"
target="_blank"
href={this.props.policyUrl}
>
{ sub }
</a>
,
},
) : _t(
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie.",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
},
) }
</div>
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
{ _t("Yes, I want to help!") }
</AccessibleButton>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} />
</AccessibleButton>
</div>
);
}
}

View file

@ -1,45 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import Notifier from '../../../Notifier';
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
export default createReactClass({
displayName: 'MatrixToolbar',
hideToolbar: function() {
Notifier.setToolbarHidden(true);
},
onClick: function() {
Notifier.setEnabled(true);
},
render: function() {
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} /></AccessibleButton>
</div>
);
},
});

View file

@ -1,108 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import PlatformPeg from '../../../PlatformPeg';
import { _t } from '../../../languageHandler';
/**
* Check a version string is compatible with the Changelog
* dialog ([vectorversion]-react-[react-sdk-version]-js-[js-sdk-version])
*/
function checkVersion(ver) {
const parts = ver.split('-');
return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js';
}
export default createReactClass({
propTypes: {
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,
releaseNotes: PropTypes.string,
},
displayReleaseNotes: function(releaseNotes) {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
title: _t("What's New"),
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
button: _t("Update"),
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
},
displayChangelog: function() {
const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
version: this.props.version,
newVersion: this.props.newVersion,
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
},
onUpdateClicked: function() {
PlatformPeg.get().installUpdate();
},
render: function() {
let action_button;
// If we have release notes to display, we display them. Otherwise,
// we display the Changelog Dialog which takes two versions and
// automatically tells you what's changed (provided the versions
// are in the right format)
if (this.props.releaseNotes) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.displayReleaseNotes}>
{ _t("What's new?") }
</button>
);
} else if (checkVersion(this.props.version) && checkVersion(this.props.newVersion)) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.displayChangelog}>
{ _t("What's new?") }
</button>
);
} else if (PlatformPeg.get()) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.onUpdateClicked}>
{ _t("Update") }
</button>
);
}
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{_t("A new version of Riot is available.")}
</div>
{action_button}
</div>
);
},
});

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default createReactClass({
onUpdateClicked: function() {
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
},
render: function() {
const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable";
return (
<div className={toolbarClasses} onClick={this.onUpdateClicked}>
<img className="mx_MatrixToolbar_warning"
src={require("../../../../res/img/warning.svg")}
width="24"
height="23"
alt=""
/>
<div className="mx_MatrixToolbar_content">
{ _t(
"To return to your account in future you need to <u>set a password</u>",
{},
{ 'u': (sub) => <u>{ sub }</u> },
) }
</div>
<button className="mx_MatrixToolbar_action">
{ _t("Set Password") }
</button>
</div>
);
},
});

View file

@ -1,99 +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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import { _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
export default createReactClass({
propTypes: {
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
kind: PropTypes.string,
adminContact: PropTypes.string,
// The type of limit that has been hit.
limitType: PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
kind: 'hard',
};
},
render: function() {
const toolbarClasses = {
'mx_MatrixToolbar': true,
};
let adminContact;
let limitError;
if (this.props.kind === 'hard') {
toolbarClasses['mx_MatrixToolbar_error'] = true;
adminContact = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
},
);
limitError = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
'': _td("This homeserver has exceeded one of its resource limits."),
},
);
} else {
toolbarClasses['mx_MatrixToolbar_info'] = true;
adminContact = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'': _td("Please <a>contact your service administrator</a> to get this limit increased."),
},
);
limitError = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'monthly_active_user': _td(
"This homeserver has hit its Monthly Active User limit so " +
"<b>some users will not be able to log in</b>.",
),
'': _td(
"This homeserver has exceeded one of its resource limits so " +
"<b>some users will not be able to log in</b>.",
),
},
{'b': sub => <b>{sub}</b>},
);
}
return (
<div className={classNames(toolbarClasses)}>
<div className="mx_MatrixToolbar_content">
{limitError}
{' '}
{adminContact}
</div>
</div>
);
},
});

View file

@ -1,75 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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 createReactClass from 'create-react-class';
import Notifier from "../../../Notifier";
import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'EnableNotificationsButton',
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "notifier_enabled") {
return;
}
this.forceUpdate();
},
enabled: function() {
return Notifier.isEnabled();
},
onClick: function() {
const self = this;
if (!Notifier.supportsDesktopNotifications()) {
return;
}
if (!Notifier.isEnabled()) {
Notifier.setEnabled(true, function() {
self.forceUpdate();
});
} else {
Notifier.setEnabled(false);
}
this.forceUpdate();
},
render: function() {
if (this.enabled()) {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
{ _t("Disable Notifications") }
</button>
);
} else {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
{ _t("Enable Notifications") }
</button>
);
}
},
});

View file

@ -391,10 +391,26 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Help us improve Riot": "Help us improve Riot",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.",
"I want to help": "I want to help",
"No": "No",
"Review where youre logged in": "Review where youre logged in",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Review": "Review",
"Later": "Later",
"Notifications": "Notifications",
"You are not receiving desktop notifications": "You are not receiving desktop notifications",
"Enable them now": "Enable them now",
"Close": "Close",
"Your homeserver has exceeded its user limit.": "Your homeserver has exceeded its user limit.",
"Your homeserver has exceeded one of its resource limits.": "Your homeserver has exceeded one of its resource limits.",
"Contact your <a>server admin</a>.": "Contact your <a>server admin</a>.",
"Warning": "Warning",
"Ok": "Ok",
"Set password": "Set password",
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
"Set Password": "Set Password",
"Set up encryption": "Set up encryption",
"Encryption upgrade available": "Encryption upgrade available",
"Verify this session": "Verify this session",
@ -405,6 +421,12 @@
"Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"What's new?": "What's new?",
"What's New": "What's New",
"Update": "Update",
"Restart": "Restart",
"Upgrade your Riot": "Upgrade your Riot",
"A new version of Riot is available!": "A new version of Riot is available!",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
@ -643,8 +665,6 @@
"Last seen": "Last seen",
"Failed to set display name": "Failed to set display name",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
" to store messages from ": " to store messages from ",
"rooms.": "rooms.",
@ -776,7 +796,6 @@
"Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account",
"Warning": "Warning",
"General": "General",
"Discovery": "Discovery",
"Deactivate account": "Deactivate account",
@ -815,7 +834,6 @@
"Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s",
"Server rules": "Server rules",
"User rules": "User rules",
"Close": "Close",
"You have not ignored anyone.": "You have not ignored anyone.",
"You are currently ignoring:": "You are currently ignoring:",
"You are not subscribed to any lists": "You are not subscribed to any lists",
@ -836,7 +854,6 @@
"If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.",
"Room ID or address of ban list": "Room ID or address of ban list",
"Subscribe": "Subscribe",
"Notifications": "Notifications",
"Start automatically after system login": "Start automatically after system login",
"Always show the window menu bar": "Always show the window menu bar",
"Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close",
@ -1287,7 +1304,6 @@
"Verify by emoji": "Verify by emoji",
"Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?",
"Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?",
"No": "No",
"Yes": "Yes",
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
"In encrypted rooms, verify all users to ensure its secure.": "In encrypted rooms, verify all users to ensure its secure.",
@ -1381,20 +1397,6 @@
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
"You're not currently a member of any communities.": "You're not currently a member of any communities.",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.",
"Yes, I want to help!": "Yes, I want to help!",
"You are not receiving desktop notifications": "You are not receiving desktop notifications",
"Enable them now": "Enable them now",
"What's New": "What's New",
"Update": "Update",
"What's new?": "What's new?",
"A new version of Riot is available.": "A new version of Riot is available.",
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Set Password": "Set Password",
"Please <a>contact your service administrator</a> to get this limit increased.": "Please <a>contact your service administrator</a> to get this limit increased.",
"This homeserver has hit its Monthly Active User limit so <b>some users will not be able to log in</b>.": "This homeserver has hit its Monthly Active User limit so <b>some users will not be able to log in</b>.",
"This homeserver has exceeded one of its resource limits so <b>some users will not be able to log in</b>.": "This homeserver has exceeded one of its resource limits so <b>some users will not be able to log in</b>.",
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
"Checking for an update...": "Checking for an update...",
"No update available.": "No update available.",

View file

@ -68,13 +68,15 @@ export default class ToastStore extends EventEmitter {
}
dismissToast(key) {
if (this.toasts[0] && this.toasts[0].key === key) {
this.countSeen++;
}
const length = this.toasts.length;
this.toasts = this.toasts.filter(t => t.key !== key);
if (length !== this.toasts.length) {
if (this.toasts.length === 0) {
this.countSeen = 0;
} else {
this.countSeen++;
}
this.emit('update');

View file

@ -0,0 +1,77 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher";
import Analytics from "../Analytics";
import AccessibleButton from "../components/views/elements/AccessibleButton";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
console.log("DEBUG onAccept AnalyticsToast");
dis.dispatch({
action: 'accept_cookies',
});
};
const onReject = () => {
console.log("DEBUG onReject AnalyticsToast");
dis.dispatch({
action: "reject_cookies",
});
};
const onUsageDataClicked = () => {
Analytics.showDetailsModal();
};
const TOAST_KEY = "analytics";
export const showToast = (policyUrl?: string) => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Help us improve Riot"),
props: {
description: _t(
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. " +
"This will use a <PolicyLink>cookie</PolicyLink>.",
{},
{
"UsageDataLink": (sub) => (
<AccessibleButton kind="link" onClick={onUsageDataClicked}>{ sub }</AccessibleButton>
),
// XXX: We need to link to the page that explains our cookies
"PolicyLink": (sub) => policyUrl ? (
<a target="_blank" href={policyUrl}>{ sub }</a>
) : sub,
},
),
acceptLabel: _t("I want to help"),
onAccept,
rejectLabel: _t("No"),
onReject,
},
component: GenericToast,
priority: 10,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,50 @@
/*
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 { _t } from "../languageHandler";
import Notifier from "../Notifier";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
Notifier.setEnabled(true);
};
const onReject = () => {
Notifier.setToolbarHidden(true);
};
const TOAST_KEY = "desktopnotifications";
export const showToast = () => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Notifications"),
props: {
description: _t("You are not receiving desktop notifications"),
acceptLabel: _t("Enable them now"),
onAccept,
rejectLabel: _t("Close"),
onReject,
},
component: GenericToast,
priority: 30,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,50 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { _t, _td } from "../languageHandler";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
import {messageForResourceLimitError} from "../utils/ErrorUtils";
const TOAST_KEY = "serverlimit";
export const showToast = (limitType: string, adminContact?: string, syncError?: boolean) => {
const errorText = messageForResourceLimitError(limitType, adminContact, {
'monthly_active_user': _td("Your homeserver has exceeded its user limit."),
'': _td("Your homeserver has exceeded one of its resource limits."),
});
const contactText = messageForResourceLimitError(limitType, adminContact, {
'': _td("Contact your <a>server admin</a>."),
});
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Warning"),
props: {
description: <React.Fragment>{errorText} {contactText}</React.Fragment>,
acceptLabel: _t("Ok"),
onAccept: hideToast,
},
component: GenericToast,
priority: 70,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,47 @@
/*
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 { _t } from "../languageHandler";
import Modal from "../Modal";
import SetPasswordDialog from "../components/views/dialogs/SetPasswordDialog";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
};
const TOAST_KEY = "setpassword";
export const showToast = () => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Set password"),
props: {
description: _t("To return to your account in future you need to set a password"),
acceptLabel: _t("Set Password"),
onAccept,
rejectLabel: _t("Later"),
onReject: hideToast, // it'll return on reload
},
component: GenericToast,
priority: 60,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,90 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { _t } from "../languageHandler";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
import QuestionDialog from "../components/views/dialogs/QuestionDialog";
import ChangelogDialog from "../components/views/dialogs/ChangelogDialog";
import PlatformPeg from "../PlatformPeg";
import Modal from "../Modal";
const TOAST_KEY = "update";
/*
* Check a version string is compatible with the Changelog
* dialog ([riot-version]-react-[react-sdk-version]-js-[js-sdk-version])
*/
function checkVersion(ver) {
const parts = ver.split('-');
return parts.length === 5 && parts[1] === 'react' && parts[3] === 'js';
}
function installUpdate() {
PlatformPeg.get().installUpdate();
}
export const showToast = (version: string, newVersion: string, releaseNotes?: string) => {
let onAccept;
let acceptLabel = _t("What's new?");
if (releaseNotes) {
onAccept = () => {
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
title: _t("What's New"),
description: <pre>{releaseNotes}</pre>,
button: _t("Update"),
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
};
} else if (checkVersion(version) && checkVersion(newVersion)) {
onAccept = () => {
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
version,
newVersion,
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
};
} else {
onAccept = installUpdate;
acceptLabel = _t("Restart");
}
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Upgrade your Riot"),
props: {
description: _t("A new version of Riot is available!"),
acceptLabel,
onAccept,
},
component: GenericToast,
priority: 20,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -17,6 +17,7 @@ limitations under the License.
const {range} = require('./util');
const signup = require('./usecases/signup');
const toastScenarios = require('./scenarios/toast');
const roomDirectoryScenarios = require('./scenarios/directory');
const lazyLoadingScenarios = require('./scenarios/lazy-loading');
const e2eEncryptionScenarios = require('./scenarios/e2e-encryption');
@ -37,6 +38,7 @@ module.exports = async function scenario(createSession, restCreator) {
const alice = await createUser("alice");
const bob = await createUser("bob");
await toastScenarios(alice, bob);
await roomDirectoryScenarios(alice, bob);
await e2eEncryptionScenarios(alice, bob);
console.log("create REST users:");

View file

@ -0,0 +1,49 @@
/*
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.
*/
const {assertNoToasts, acceptToast, rejectToast} = require("../usecases/toasts");
module.exports = async function toastScenarios(alice, bob) {
console.log(" checking and clearing toasts:");
alice.log.startGroup(`clears toasts`);
alice.log.step(`reject desktop notifications toast`);
await rejectToast(alice, "Notifications");
alice.log.done();
alice.log.step(`accepts analytics toast`);
await acceptToast(alice, "Help us improve Riot");
alice.log.done();
alice.log.step(`checks no remaining toasts`);
await assertNoToasts(alice);
alice.log.done();
alice.log.endGroup();
bob.log.startGroup(`clears toasts`);
bob.log.step(`reject desktop notifications toast`);
await rejectToast(bob, "Notifications");
bob.log.done();
bob.log.step(`reject analytics toast`);
await rejectToast(bob, "Help us improve Riot");
bob.log.done();
bob.log.step(`checks no remaining toasts`);
await assertNoToasts(bob);
bob.log.done();
bob.log.endGroup();
};

View file

@ -122,8 +122,8 @@ module.exports = class RiotSession {
await input.type(text);
}
query(selector, timeout = DEFAULT_TIMEOUT) {
return this.page.waitForSelector(selector, {visible: true, timeout});
query(selector, timeout = DEFAULT_TIMEOUT, hidden = false) {
return this.page.waitForSelector(selector, {visible: true, timeout, hidden});
}
async queryAll(selector) {

View file

@ -20,7 +20,7 @@ const assert = require('assert');
async function assertDialog(session, expectedTitle) {
const titleElement = await session.query(".mx_Dialog .mx_Dialog_title");
const dialogHeader = await session.innerText(titleElement);
assert(dialogHeader, expectedTitle);
assert.equal(dialogHeader, expectedTitle);
}
async function acceptDialog(session, expectedTitle) {

View file

@ -0,0 +1,47 @@
/*
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.
*/
const assert = require('assert');
async function assertNoToasts(session) {
try {
await session.query('.mx_Toast_toast', 1000, true);
} catch (e) {
const h2Element = await session.query('.mx_Toast_title h2', 1000);
const toastTitle = await session.innerText(h2Element);
throw new Error(`"${toastTitle}" toast found when none expected`);
}
}
async function assertToast(session, expectedTitle) {
const h2Element = await session.query('.mx_Toast_title h2');
const toastTitle = await session.innerText(h2Element);
assert.equal(toastTitle, expectedTitle);
}
async function acceptToast(session, expectedTitle) {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary');
await btn.click();
}
async function rejectToast(session, expectedTitle) {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger');
await btn.click();
}
module.exports = {assertNoToasts, assertToast, acceptToast, rejectToast};