Merge branches 'develop' and 't3chguy/e2eedefault' of github.com:matrix-org/matrix-react-sdk into t3chguy/e2eedefault

 Conflicts:
	src/components/views/dialogs/InviteDialog.js
This commit is contained in:
Michael Telatynski 2020-06-01 21:42:31 +01:00
commit 8848a2ea64
305 changed files with 9256 additions and 4200 deletions

View file

@ -18,7 +18,7 @@ import React from 'react';
import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
import AutoHideScrollbar from './AutoHideScrollbar';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils';

View file

@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
import request from 'browser-request';
import { _t } from '../../languageHandler';
import sanitizeHtml from 'sanitize-html';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";

View file

@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import { getHostingLink } from '../../utils/HostingLink';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td } from '../../languageHandler';
@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this summary?"),
placeholder: _t("Room name or alias"),
placeholder: _t("Room name or address"),
button: _t("Add to summary"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],

View file

@ -21,7 +21,7 @@ import { getHomePageUrl } from "../../utils/pages";
import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});

View file

@ -21,11 +21,12 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Key } from '../../Keyboard';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
const LeftPanel = createReactClass({
@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
let roomList = null;
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
roomList = <RoomList2
onKeyDown={this._onKeyDown}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ref={this.collectRoomList}
onFocus={this._onFocus}
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
return (
<div className={containerClasses}>
{ tagPanelContainer }
@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
{ exploreButton }
{ searchBox }
</div>
<RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />
{roomList}
</aside>
</div>
);

View file

@ -27,11 +27,10 @@ import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@ -42,6 +41,17 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
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.
@ -56,7 +66,7 @@ function canElementReceiveInput(el) {
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
@ -64,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;
@ -75,7 +81,6 @@ interface IProps {
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
@ -85,10 +90,8 @@ interface IProps {
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
@ -96,8 +99,6 @@ interface IState {
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/**
@ -140,11 +141,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
@ -178,18 +176,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this._loadResizerPreferences();
}
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)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData);
@ -219,9 +205,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
};
_setStateFromSessionStore = () => {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
if (this._sessionStore.getCachedPassword()) {
showSetPasswordToast();
} else {
hideSetPasswordToast();
}
};
_createResizer() {
@ -293,22 +281,37 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(data);
}
};
onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
};
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
const pinnedEvents = [];
for (const room of roomLists['m.server_notice']) {
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
} else {
hideServerLimitToast();
}
}
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return [];
const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -317,12 +320,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) => {
@ -598,12 +607,6 @@ 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;
@ -647,46 +650,7 @@ 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) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (this.props.showNotifierToolbar) {
topBar = <MatrixToolbar />;
}
let bodyClasses = 'mx_MatrixChat';
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
@ -701,7 +665,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp}
>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>

View file

@ -17,12 +17,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import {InvalidStoreError} from "matrix-js-sdk/src/errors";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import React, { createRef } from 'react';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible';
// what-input helps improve keyboard accessibility
@ -30,17 +29,17 @@ import 'what-input';
import Analytics from "../../Analytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig";
import * as RoomListSorter from "../../RoomListSorter";
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import * as sdk from '../../index';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
@ -50,23 +49,29 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme";
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import {defer, IDeferred} from "../../utils/promise";
import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";
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";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
/** constants for MatrixChat.state.view */
export enum Views {
@ -107,7 +112,7 @@ export enum Views {
// re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future.
const ONBOARDING_FLOW_STARTERS = [
'view_user_settings',
Action.ViewUserSettings,
'view_create_chat',
'view_create_room',
'view_create_group',
@ -168,12 +173,6 @@ 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;
@ -216,6 +214,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: any;
private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
constructor(props, context) {
super(props, context);
@ -226,17 +225,10 @@ 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,
};
@ -283,8 +275,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.accountPasswordTimer = null;
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start();
this.fontWatcher.start();
this.focusComposer = false;
@ -334,12 +329,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
if (SettingsStore.getValue("showCookieBar")) {
this.setState({
showCookieBar: true,
});
}
if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable();
}
@ -367,6 +356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
this.fontWatcher.stop();
window.removeEventListener('resize', this.handleResize);
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
@ -613,7 +603,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'view_indexed_room':
this.viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings': {
case Action.ViewUserSettings: {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -680,9 +670,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,
@ -730,15 +717,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;
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
@ -755,19 +733,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;
}
};
@ -926,9 +898,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private viewGroup(payload) {
private async viewGroup(payload) {
const groupId = payload.group_id;
// Wait for the first sync to complete
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
console.warn('Cannot view a group before first sync. group_id:', groupId);
return;
}
await this.firstSyncPromise.promise;
}
this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});
@ -1245,6 +1228,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() {
@ -1372,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true;
this.firstSyncPromise.resolve();
if (Notifier.shouldShowToolbar()) {
showNotificationsToast();
}
dis.dispatch({action: 'focus_composer'});
this.setState({
ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
});
});
cli.on('Call.incoming', function(call) {
@ -1454,16 +1444,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("Session.logged_out", () => dft.stop());
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
const krh = new KeyRequestHandler(cli);
cli.on("crypto.roomKeyRequest", (req) => {
krh.handleKeyRequest(req);
});
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
@ -1553,7 +1533,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification",
props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"),
priority: ToastStore.PRIORITY_REALTIME,
priority: 90,
});
}
});
@ -1621,9 +1601,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
action: 'view_create_room',
});
} else if (screen === 'settings') {
dis.dispatch({
action: 'view_user_settings',
});
dis.fire(Action.ViewUserSettings);
} else if (screen === 'welcome') {
dis.dispatch({
action: 'view_welcome_page',
@ -1755,8 +1733,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const member = new RoomMember(null, userId);
if (!member) { return; }
dis.dispatch({
action: 'view_user',
dis.dispatch<ViewUserPayload>({
action: Action.ViewUser,
member: member,
});
}
@ -1829,16 +1807,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) {
@ -2033,7 +2001,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

@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@ -107,10 +108,14 @@ export default class MessagePanel extends React.Component {
// whether to show reactions for an event
showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
};
constructor() {
super();
// Force props to be loaded for useIRCLayout
constructor(props) {
super(props);
this.state = {
// previous positions the read marker has been in, so we can
@ -597,6 +602,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/>
</TileErrorBoundary>
</li>,
@ -792,6 +798,15 @@ export default class MessagePanel extends React.Component {
);
}
let ircResizer = null;
if (this.props.useIRCLayout) {
ircResizer = <IRCTimelineProfileResizer
minWidth={20}
maxWidth={600}
roomId={this.props.room ? this.props.roomroomId : null}
/>;
}
return (
<ErrorBoundary>
<ScrollPanel
@ -804,6 +819,7 @@ export default class MessagePanel extends React.Component {
style={style}
stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier}
fixedChildren={ircResizer}
>
{ topSpinner }
{ this._getEventTiles() }

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../index';
import { _t } from '../../languageHandler';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";

View file

@ -22,7 +22,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
@ -30,6 +30,7 @@ import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
export default class RightPanel extends React.Component {
static get propTypes() {
@ -237,7 +238,7 @@ export default class RightPanel extends React.Component {
// within a room, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: "view_user",
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null,
});
@ -266,7 +267,7 @@ export default class RightPanel extends React.Component {
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
action: Action.ViewUser,
member: null,
});
};

View file

@ -20,7 +20,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import PropTypes from 'prop-types';
@ -199,7 +199,7 @@ export default createReactClass({
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
} else {
desc = _t('Remove %(name)s from the directory?', {name: name});
}
@ -216,7 +216,7 @@ export default createReactClass({
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
step = _t('delete the alias.');
step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias);
}).then(() => {
modal.close();

View file

@ -25,7 +25,7 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
const STATUS_BAR_HIDDEN = 0;

View file

@ -20,7 +20,7 @@ limitations under the License.
import React, {createRef} from 'react';
import classNames from 'classnames';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import * as Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils';
@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
import toRem from "../../utils/rem";
import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore
const debug = false;
@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = toRem(height);
this._subList.current.style.height = toPx(height);
}
this._updateLazyRenderHeight(height);
};

View file

@ -34,7 +34,7 @@ import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler from '../../CallHandler';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils';
@ -164,6 +164,10 @@ export default createReactClass({
canReact: false,
canReply: false,
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
};
},
@ -193,6 +197,8 @@ export default createReactClass({
this._roomView = createRef();
this._searchResultsPanel = createRef();
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
},
_onReadReceiptsChange: function() {
@ -232,7 +238,8 @@ export default createReactClass({
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
};
@ -532,6 +539,14 @@ export default createReactClass({
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this._layoutWatcherRef);
},
onLayoutChange: function() {
this.setState({
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
});
},
_onRightPanelStoreUpdate: function() {
@ -681,6 +696,16 @@ export default createReactClass({
});
}
break;
case 'sync_state':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this._onRoomViewStoreUpdate(true);
});
}
break;
}
},
@ -1663,14 +1688,16 @@ export default createReactClass({
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading;
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
if (loading) {
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
previewLoading={this.state.peekLoading}
previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
@ -1695,7 +1722,8 @@ export default createReactClass({
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
@ -1980,6 +2008,13 @@ export default createReactClass({
highlightedEventId = this.state.initialEventId;
}
const messagePanelClassNames = classNames(
"mx_RoomView_messagePanel",
{
"mx_IRCLayout": this.state.useIRCLayout,
"mx_GroupLayout": !this.state.useIRCLayout,
});
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
<TimelinePanel
@ -1995,11 +2030,12 @@ export default createReactClass({
onScroll={this.onMessageListScroll}
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview}
className="mx_RoomView_messagePanel"
className={messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
useIRCLayout={this.state.useIRCLayout}
/>);
let topUnreadMessagesBar = null;

View file

@ -144,6 +144,11 @@ export default createReactClass({
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier: PropTypes.object,
/* fixedChildren: allows for children to be passed which are rendered outside
* of the wrapper
*/
fixedChildren: PropTypes.node,
},
getDefaultProps: function() {
@ -881,6 +886,7 @@ export default createReactClass({
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }

View file

@ -19,7 +19,7 @@ import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';

View file

@ -22,7 +22,7 @@ import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import dis from '../../dispatcher/dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';

View file

@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as ObjectUtils from "../../ObjectUtils";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import * as sdk from "../../index";
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
// whether to show reactions for an event
showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
},
statics: {
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
getRelationsForEvent={this.getRelationsForEvent}
editState={this.state.editState}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/>
);
},

View file

@ -15,14 +15,21 @@ limitations under the License.
*/
import * as React from "react";
import { _t } from '../../languageHandler';
import ToastStore from "../../stores/ToastStore";
import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames";
export default class ToastContainer extends React.Component {
constructor() {
super();
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
interface IState {
toasts: IToast<any>[];
countSeen: number;
}
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
this.state = {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
// Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
}
_onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
};
render() {
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
});
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
let countIndicator;
if (isStacked || this.state.countSeen > 0) {
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
}
const toastProps = Object.assign({}, props, {
key,
toastKey: key,
});
toast = (<div className={toastClasses}>
<h2>{title}{countIndicator}</h2>
<div className="mx_Toast_title">
<h2>{title}</h2>
<span>{countIndicator}</span>
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
}

View file

@ -22,7 +22,7 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
const AVATAR_SIZE = 28;

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher";
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';

View file

@ -32,7 +32,7 @@ import * as Lifecycle from '../../../Lifecycle';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
// Phases
// Show controls to configure server details

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal';
import {MatrixClientPeg} from "../../../MatrixClientPeg";

View file

@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
allChecked = allChecked && checked;
checkboxes.push(
// XXX: replace with StyledCheckbox
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
@ -538,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
type: MsisdnAuthEntry.LOGIN_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
});

View file

@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?

View file

@ -36,7 +36,7 @@ interface IProps {
labelStrongPassword?: string;
labelAllowedButUnsafe?: string;
onChange(ev: KeyboardEvent);
onChange(ev: React.FormEvent<HTMLElement>);
onValidate(result: IValidationResult);
}

View file

@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
type="text"
label={_t("Phone")}
value={this.state.phoneNumber}
prefix={phoneCountry}
prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit}

View file

@ -473,7 +473,7 @@ export default createReactClass({
type="text"
label={phoneLabel}
value={this.state.phoneNumber}
prefix={phoneCountry}
prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate}
/>;

View file

@ -22,7 +22,8 @@ import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
export const FREE = 'Free';
export const PREMIUM = 'Premium';

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -17,220 +17,182 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import toRem from "../../../utils/rem";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
export default createReactClass({
displayName: 'BaseAvatar',
const useImageUrl = ({url, urls}) => {
const [imageUrls, setUrls] = useState([]);
const [urlsIndex, setIndex] = useState();
propTypes: {
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
},
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
statics: {
contextType: MatrixClientContext,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop',
defaultToInitialLetter: true,
};
},
getInitialState: function() {
return this._getState(this.props);
},
componentDidMount() {
this.unmounted = false;
this.context.on('sync', this.onClientSync);
},
componentWillUnmount() {
this.unmounted = true;
this.context.removeListener('sync', this.onClientSync);
},
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
const newImageUrls = newState.imageUrls;
const oldImageUrls = this.state.imageUrls;
if (newImageUrls.length !== oldImageUrls.length) {
this.setState(newState); // detected a new entry
} else {
// check each one to see if they are the same
for (let i = 0; i < newImageUrls.length; i++) {
if (oldImageUrls[i] !== newImageUrls[i]) {
this.setState(newState); // detected a diff
break;
}
}
}
},
onClientSync: function(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected &&
// Did we fall back?
this.state.urlsIndex > 0
) {
// Start from the highest priority URL again
this.setState({
urlsIndex: 0,
});
}
},
_getState: function(props) {
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]
// imageUrls: [ props.url, ...props.urls ]
let urls = [];
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || [];
_urls = memoizedUrls || [];
if (props.url) {
urls.unshift(props.url); // put in urls[0]
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
let defaultImageUrl = null;
if (props.defaultToInitialLetter) {
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
props.idName || props.name,
);
urls.push(defaultImageUrl); // lowest priority
}
// deduplicate URLs
urls = Array.from(new Set(urls));
_urls = Array.from(new Set(_urls));
return {
imageUrls: urls,
defaultImageUrl: defaultImageUrl,
urlsIndex: 0,
};
},
setIndex(0);
setUrls(_urls);
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
onError: function(ev) {
const nextIndex = this.state.urlsIndex + 1;
if (nextIndex < this.state.imageUrls.length) {
// try the next one
this.setState({
urlsIndex: nextIndex,
});
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
setIndex(0);
}
},
}, []);
useEventEmitter(cli, "sync", onClientSync);
render: function() {
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const imageUrl = imageUrls[urlsIndex];
return [imageUrl, onError];
};
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter, onClick, inputRef,
...otherProps
} = this.props;
const BaseAvatar = (props) => {
const {
name,
idName,
title,
url,
urls,
width=40,
height=40,
resizeMethod="crop", // eslint-disable-line no-unused-vars
defaultToInitialLetter=true,
onClick,
inputRef,
...otherProps
} = props;
const [imageUrl, onError] = useImageUrl({url, urls});
if (!imageUrl && defaultToInitialLetter) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span
className="mx_BaseAvatar_initial"
aria-hidden="true"
style={{
fontSize: toPx(width * 0.65),
width: toPx(width),
lineHeight: toPx(height),
}}
>
{ initialLetter }
</span>
);
const imgNode = (
<img
className="mx_BaseAvatar_image"
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
alt=""
title={title}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
aria-hidden="true" />
);
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{
fontSize: toRem(width * 0.65),
width: toRem(width),
lineHeight: toRem(height),
}}
>
{ initialLetter }
</span>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
aria-hidden="true"
style={{
width: toRem(width),
height: toRem(height)
}} />
);
if (onClick != null) {
return (
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} inputRef={inputRef} {...otherProps}
>
{ textNode }
{ imgNode }
</AccessibleButton>
);
} else {
return (
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
{ textNode }
{ imgNode }
</span>
);
}
}
if (onClick != null) {
return (
<AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
{...otherProps}
element="span"
className="mx_BaseAvatar"
onClick={onClick}
onError={this.onError}
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
inputRef={inputRef}
{...otherProps} />
>
{ textNode }
{ imgNode }
</AccessibleButton>
);
} else {
return (
<img
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={this.onError}
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
ref={inputRef}
{...otherProps} />
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
{ textNode }
{ imgNode }
</span>
);
}
},
});
}
if (onClick != null) {
return (
<AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
onClick={onClick}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
title={title} alt=""
inputRef={inputRef}
{...otherProps} />
);
} else {
return (
<img
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={onError}
style={{
width: toPx(width),
height: toPx(height),
}}
title={title} alt=""
ref={inputRef}
{...otherProps} />
);
}
};
BaseAvatar.displayName = "BaseAvatar";
BaseAvatar.propTypes = {
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
onClick: PropTypes.func,
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
};
export default BaseAvatar;

View file

@ -18,9 +18,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar';
import * as sdk from "../../../index";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
export default createReactClass({
displayName: 'MemberAvatar',
@ -33,7 +34,7 @@ export default createReactClass({
resizeMethod: PropTypes.string,
// The onClick to give the avatar
onClick: PropTypes.func,
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
viewUserOnClick: PropTypes.bool,
title: PropTypes.string,
},
@ -61,10 +62,14 @@ export default createReactClass({
return {
name: props.member.name,
title: props.title || props.member.userId,
imageUrl: Avatar.avatarUrlForMember(props.member,
props.width,
props.height,
props.resizeMethod),
imageUrl: props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
};
} else if (props.fallbackUserId) {
return {
@ -85,7 +90,7 @@ export default createReactClass({
if (viewUserOnClick) {
onClick = () => {
dis.dispatch({
action: 'view_user',
action: Action.ViewUser,
member: this.props.member,
});
};

View file

@ -23,7 +23,7 @@ import createReactClass from 'create-react-class';
import {EventStatus} from 'matrix-js-sdk';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
@ -116,11 +116,6 @@ export default createReactClass({
this.closeMenu();
},
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
@ -465,15 +460,6 @@ export default createReactClass({
);
}
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</MenuItem>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
@ -500,7 +486,6 @@ export default createReactClass({
{ quoteButton }
{ externalURLButton }
{ collapseReplyThread }
{ e2eInfo }
{ reportEventButton }
</div>
);

View file

@ -24,7 +24,7 @@ import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import * as sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu";

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
@ -27,6 +27,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export default class TopLeftMenu extends React.Component {
static propTypes = {
@ -134,7 +135,7 @@ export default class TopLeftMenu extends React.Component {
}
openSettings() {
dis.dispatch({action: 'view_user_settings'});
dis.fire(Action.ViewUserSettings);
this.closeMenu();
}

View file

@ -98,7 +98,7 @@ export default createReactClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);

View file

@ -24,7 +24,7 @@ import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -615,7 +616,7 @@ export default createReactClass({
onManageSettingsClick(e) {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
dis.fire(Action.ViewUserSettings);
this.onCancel();
},

View file

@ -144,6 +144,7 @@ export default createReactClass({
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
'mx_Dialog_headerWithCancel': !!cancelButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{headerImage}

View file

@ -18,7 +18,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -182,7 +182,7 @@ export default createReactClass({
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
@ -42,11 +42,9 @@ export default (props) => {
};
const description =
_t("You've previously used a newer version of Riot on %(host)s. " +
_t("You've previously used a newer version of Riot with this session. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ",
{host: props.host},
);
"need to sign out and back in again.");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -25,6 +25,7 @@ import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
@ -209,21 +210,18 @@ export default class DeactivateAccountDialog extends React.Component {
<div className="mx_DeactivateAccountDialog_input_section">
<p>
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
<input
id="mx_DeactivateAccountDialog_erase_account_input"
type="checkbox"
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
/>
{ _t(
<StyledCheckbox
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
>
{_t(
"Please forget all messages I have sent when my account is deactivated " +
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
"of conversations)",
{},
{ b: (sub) => <b>{ sub }</b> },
) }
</label>
)}
</StyledCheckbox>
</p>
{error}

View file

@ -25,7 +25,7 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";

View file

@ -18,7 +18,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import {Action} from "../../../dispatcher/actions";
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
_onOpenSettingsClick = () => {
this.props.onFinished();
dis.dispatch({action: "view_user_settings"});
dis.fire(Action.ViewUserSettings);
};
render() {

View file

@ -27,14 +27,17 @@ import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
import SettingsStore from '../../../settings/SettingsStore';
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models";
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
@ -342,10 +345,10 @@ export default class InviteDialog extends React.PureComponent {
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
const taggedRooms = RoomListStore.getRoomLists();
const dmTaggedRooms = taggedRooms[TAG_DM];
const taggedRooms = RoomListStoreTempProxy.getRoomLists();
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
@ -901,7 +904,7 @@ export default class InviteDialog extends React.PureComponent {
_onManageSettingsClick = (e) => {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
dis.fire(Action.ViewUserSettings);
this.props.onFinished();
};

View file

@ -1,178 +0,0 @@
/*
Copyright 2017 Vector Creations 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 Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for session ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new session '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified session '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{ _t('Ignore request') }
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading session info...') }</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
},
});

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -27,7 +27,7 @@ import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsT
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
export default class RoomSettingsDialog extends React.Component {

View file

@ -29,6 +29,7 @@ import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../..
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
const socials = [
{
@ -168,13 +169,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const events = this.props.target.getLiveTimeline().getEvents();
if (events.length > 0) {
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to most recent message') }
</label>
</StyledCheckbox>
</div>;
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
@ -184,13 +184,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
onClick={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to selected message') }
</label>
</StyledCheckbox>
</div>;
}

View file

@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab(
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",

View file

@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback },
);
});

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';

View file

@ -31,7 +31,7 @@ import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import MessageSpinner from './MessageSpinner';
import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";

View file

@ -0,0 +1,81 @@
/*
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';
interface IProps {
className: string,
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
onMouseUp: (event: MouseEvent) => void,
}
interface IState {
onMouseMove: (event: MouseEvent) => void,
onMouseUp: (event: MouseEvent) => void,
location: ILocationState,
}
export interface ILocationState {
currentX: number,
currentY: number,
}
export default class Draggable extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
onMouseMove: this.onMouseMove.bind(this),
onMouseUp: this.onMouseUp.bind(this),
location: {
currentX: 0,
currentY: 0,
},
};
}
private onMouseDown = (event: MouseEvent): void => {
this.setState({
location: {
currentX: event.clientX,
currentY: event.clientY,
},
});
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
}
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
}
private onMouseMove(event: MouseEvent): void {
const newLocation = this.props.dragFunc(this.state.location, event);
this.setState({
location: newLocation,
});
}
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />
}
}

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { debounce } from 'lodash';
import {IFieldState, IValidationResult} from "../elements/Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@ -29,58 +29,93 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
export default class Field extends React.PureComponent {
static propTypes = {
// The field's ID, which binds the input and label together. Immutable.
id: PropTypes.string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element: PropTypes.oneOf(["input", "select", "textarea"]),
// The field's type (when used as an <input>). Defaults to "text".
type: PropTypes.string,
// id of a <datalist> element for suggestions
list: PropTypes.string,
// The field's label string.
label: PropTypes.string,
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
// The field's value.
// This is a controlled component, so the value is required.
value: PropTypes.string.isRequired,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// Optional component to include inside the field after the input.
postfix: PropTypes.node,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate: PropTypes.func,
// If specified, overrides the value returned by onValidate.
flagInvalid: PropTypes.bool,
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
tooltipContent: PropTypes.node,
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName: PropTypes.string,
// If specified, an additional class name to apply to the field container
className: PropTypes.string,
// All other props pass through to the <input>.
};
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
// The field's ID, which binds the input and label together. Immutable.
id?: string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element?: "input" | "select" | "textarea",
// The field's type (when used as an <input>). Defaults to "text".
type?: string,
// id of a <datalist> element for suggestions
list?: string,
// The field's label string.
label?: string,
// The field's placeholder string. Defaults to the label.
placeholder?: string,
// The field's value.
// This is a controlled component, so the value is required.
value: string,
// Optional component to include inside the field before the input.
prefixComponent?: React.ReactNode,
// Optional component to include inside the field after the input.
postfixComponent?: React.ReactNode,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
// If specified, overrides the value returned by onValidate.
flagInvalid?: boolean,
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
tooltipContent?: React.ReactNode,
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName?: string,
// If specified, an additional class name to apply to the field container
className?: string,
// All other props pass through to the <input>.
}
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
}
export default class Field extends React.PureComponent<IProps, IState> {
private id: string;
private input: HTMLInputElement;
private static defaultProps = {
element: "input",
type: "text",
}
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
private validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
constructor(props) {
super(props);
this.state = {
valid: undefined,
feedback: undefined,
feedbackVisible: false,
focused: false,
};
this.id = this.props.id || getId();
}
onFocus = (ev) => {
public focus() {
this.input.focus();
}
private onFocus = (ev) => {
this.setState({
focused: true,
});
@ -93,7 +128,7 @@ export default class Field extends React.PureComponent {
}
};
onChange = (ev) => {
private onChange = (ev) => {
this.validateOnChange();
// Parent component may have supplied its own `onChange` as well
if (this.props.onChange) {
@ -101,7 +136,7 @@ export default class Field extends React.PureComponent {
}
};
onBlur = (ev) => {
private onBlur = (ev) => {
this.setState({
focused: false,
});
@ -114,11 +149,7 @@ export default class Field extends React.PureComponent {
}
};
focus() {
this.input.focus();
}
async validate({ focused, allowEmpty = true }) {
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
if (!this.props.onValidate) {
return;
}
@ -149,56 +180,42 @@ export default class Field extends React.PureComponent {
}
}
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
render() {
public render() {
const {
element, prefix, postfix, className, onValidate, children,
element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
const inputElement = element || "input";
// Set some defaults for the <input> element
inputProps.type = inputProps.type || "text";
inputProps.ref = input => this.input = input;
const ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur;
inputProps.list = list;
const fieldInput = React.createElement(inputElement, inputProps, children);
// Appease typescript's inference
const inputProps_ = {...inputProps, ref, list};
const fieldInput = React.createElement(this.props.element, inputProps_, children);
let prefixContainer = null;
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
if (prefixComponent) {
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
}
let postfixContainer = null;
if (postfix) {
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
if (postfixComponent) {
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefix,
mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? flagInvalid

View file

@ -19,7 +19,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -0,0 +1,88 @@
/*
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 SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Draggable, {ILocationState} from './Draggable';
interface IProps {
// Current room
roomId: string,
minWidth: number,
maxWidth: number,
};
interface IState {
width: number,
IRCLayoutRoot: HTMLElement,
};
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
IRCLayoutRoot: null,
}
};
componentDidMount() {
this.setState({
IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
}, () => this.updateCSSWidth(this.state.width))
}
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
const offset = event.clientX - location.currentX;
const newWidth = this.state.width + offset;
console.log({offset})
// If we're trying to go smaller than min width, don't.
if (newWidth < this.props.minWidth) {
return location;
}
if (newWidth > this.props.maxWidth) {
return location;
}
this.setState({
width: newWidth,
});
this.updateCSSWidth.bind(this)(newWidth);
return {
currentX: event.clientX,
currentY: location.currentY,
}
}
private updateCSSWidth(newWidth: number) {
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
}
private onMoueUp(event: MouseEvent) {
if (this.props.roomId) {
SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
}
}
render() {
return <Draggable className="mx_ProfileResizer" dragFunc={this.dragFunc.bind(this)} onMouseUp={this.onMoueUp.bind(this)}/>
}
};

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -156,16 +156,70 @@ export default class PersistedElement extends React.Component {
child.style.display = visible ? 'block' : 'none';
}
/*
* Clip element bounding rectangle to that of the parent elements.
* This is not a full visibility check, but prevents the persisted
* element from overflowing parent containers when inside a scrolled
* area.
*/
_getClippedBoundingClientRect(element) {
let parentElement = element.parentElement;
let rect = element.getBoundingClientRect();
rect = new DOMRect(rect.left, rect.top, rect.width, rect.height);
while (parentElement) {
const parentRect = parentElement.getBoundingClientRect();
if (parentRect.left > rect.left) {
rect.width = rect.width - (parentRect.left - rect.left);
rect.x = parentRect.x;
}
if (parentRect.top > rect.top) {
rect.height = rect.height - (parentRect.top - rect.top);
rect.y = parentRect.y;
}
if (parentRect.right < rect.right) {
rect.width = rect.width - (rect.right - parentRect.right);
}
if (parentRect.bottom < rect.bottom) {
rect.height = rect.height - (rect.bottom - parentRect.bottom);
}
parentElement = parentElement.parentElement;
}
if (rect.width < 0) rect.width = 0;
if (rect.height < 0) rect.height = 0;
return rect;
}
updateChildPosition(child, parent) {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
const clipRect = this._getClippedBoundingClientRect(parent);
Object.assign(child.parentElement.style, {
position: 'absolute',
top: clipRect.top + 'px',
left: clipRect.left + 'px',
width: clipRect.width + 'px',
height: clipRect.height + 'px',
overflow: "hidden",
});
Object.assign(child.style, {
position: 'absolute',
top: parentRect.top + 'px',
left: parentRect.left + 'px',
top: (parentRect.top - clipRect.top) + 'px',
left: (parentRect.left - clipRect.left) + 'px',
width: parentRect.width + 'px',
height: parentRect.height + 'px',
overflow: "hidden",
});
}

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
@ -191,7 +192,7 @@ const Pill = createReactClass({
onUserPillClicked: function() {
dis.dispatch({
action: 'view_user',
action: Action.ViewUser,
member: this.state.member,
});
},

View file

@ -19,7 +19,7 @@ import React from 'react';
import * as sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
@ -37,6 +37,8 @@ export default class ReplyThread extends React.Component {
// called when the ReplyThread contents has changed, including EventTiles thereof
onHeightChanged: PropTypes.func.isRequired,
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
// Specifies which layout to use.
useIRCLayout: PropTypes.bool,
};
static contextType = MatrixClientContext;
@ -176,12 +178,17 @@ export default class ReplyThread extends React.Component {
};
}
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) {
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
if (!ReplyThread.getParentEventId(parentEv)) {
return <div />;
return <div className="mx_ReplyThread_wrapper_empty" />;
}
return <ReplyThread parentEv={parentEv} onHeightChanged={onHeightChanged}
ref={ref} permalinkCreator={permalinkCreator} />;
return <ReplyThread
parentEv={parentEv}
onHeightChanged={onHeightChanged}
ref={ref}
permalinkCreator={permalinkCreator}
useIRCLayout={useIRCLayout}
/>;
}
componentDidMount() {
@ -331,11 +338,13 @@ export default class ReplyThread extends React.Component {
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
useIRCLayout={this.props.useIRCLayout}
/>
</blockquote>;
});
return <div>
return <div className="mx_ReplyThread_wrapper">
<div>{ header }</div>
<div>{ evTiles }</div>
</div>;

View file

@ -45,10 +45,10 @@ export default class RoomAliasField extends React.PureComponent {
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
<Field
label={_t("Room alias")}
label={_t("Room address")}
className="mx_RoomAliasField"
prefix={poundSign}
postfix={domain}
prefixComponent={poundSign}
postfixComponent={domain}
ref={ref => this._fieldRef = ref}
onValidate={this._onValidate}
placeholder={_t("e.g. my-room")}
@ -87,7 +87,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Please provide a room alias"),
invalid: () => _t("Please provide a room address"),
}, {
key: "taken",
final: true,
@ -107,8 +107,8 @@ export default class RoomAliasField extends React.PureComponent {
return !!err.errcode;
}
},
valid: () => _t("This alias is available to use"),
invalid: () => _t("This alias is already in use"),
valid: () => _t("This address is available to use"),
invalid: () => _t("This address is already in use"),
},
],
});

View file

@ -0,0 +1,146 @@
/*
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';
interface IProps {
// A callback for the selected value
onSelectionChange: (value: number) => void;
// The current value of the slider
value: number;
// The range and values of the slider
// Currently only supports an ascending, constant interval range
values: number[];
// A function for formatting the the values
displayFunc: (value: number) => string;
// Whether the slider is disabled
disabled: boolean;
}
export default class Slider extends React.Component<IProps> {
// offset is a terrible inverse approximation.
// if the values represents some function f(x) = y where x is the
// index of the array and y = values[x] then offset(f, y) = x
// s.t f(x) = y.
// it assumes a monotonic function and interpolates linearly between
// y values.
// Offset is used for finding the location of a value on a
// non linear slider.
private offset(values: number[], value: number): number {
// the index of the first number greater than value.
let closest = values.reduce((prev, curr) => {
return (value > curr ? prev + 1 : prev);
}, 0);
// Off the left
if (closest === 0) {
return 0;
}
// Off the right
if (closest === values.length) {
return 100;
}
// Now
const closestLessValue = values[closest - 1];
const closestGreaterValue = values[closest];
const intervalWidth = 1 / (values.length - 1);
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
return 100 * (closest - 1 + linearInterpolation) * intervalWidth
}
render(): React.ReactNode {
const dots = this.props.values.map(v =>
<Dot active={v <= this.props.value}
label={this.props.displayFunc(v)}
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
key={v}
disabled={this.props.disabled}
/>);
let selection = null;
if (!this.props.disabled) {
const offset = this.offset(this.props.values, this.props.value);
selection = <div className="mx_Slider_selection">
<div className="mx_Slider_selectionDot" style={{left: "calc(-0.55em + " + offset + "%)"}} />
<hr style={{width: offset + "%"}} />
</div>
}
return <div className="mx_Slider">
<div>
<div className="mx_Slider_bar">
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)}/>
{ selection }
</div>
<div className="mx_Slider_dotContainer">
{dots}
</div>
</div>
</div>;
}
onClick(event: React.MouseEvent) {
const width = (event.target as HTMLElement).clientWidth;
// nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX
// is supported by all modern browsers
const relativeClick = (event.nativeEvent.offsetX / width);
const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))];
this.props.onSelectionChange(nearestValue);
}
}
interface IDotProps {
// Callback for behavior onclick
onClick: () => void,
// Whether the dot should appear active
active: boolean,
// The label on the dot
label: string,
// Whether the slider is disabled
disabled: boolean;
}
class Dot extends React.PureComponent<IDotProps> {
render(): React.ReactNode {
let className = "mx_Slider_dot"
if (!this.props.disabled && this.props.active) {
className += " mx_Slider_dotActive";
}
return <span onClick={this.props.onClick} className="mx_Slider_dotValue">
<div className={className} />
<div className="mx_Slider_labelContainer">
<div className="mx_Slider_label">
{this.props.label}
</div>
</div>
</span>;
}
}

View file

@ -0,0 +1,56 @@
/*
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 { randomString } from "matrix-js-sdk/src/randomstring";
const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg");
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
interface IState {
}
export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
private id: string;
public static readonly defaultProps = {
className: "",
}
constructor(props: IProps) {
super(props);
// 56^10 so unlikely chance of collision.
this.id = "checkbox_" + randomString(10);
}
public render() {
const { children, className, ...otherProps } = this.props;
return <span className={"mx_Checkbox " + className}>
<input id={this.id} {...otherProps} type="checkbox" />
<label htmlFor={this.id}>
{/* Using the div to center the image */}
<div className="mx_Checkbox_background">
<img src={CHECK_BOX_SVG}/>
</div>
<div>
{ this.props.children }
</div>
</label>
</span>
}
}

View file

@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import * as FormattingUtils from '../../../utils/FormattingUtils';

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -18,67 +18,68 @@ limitations under the License.
*/
import React from 'react';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload';
import { Action } from '../../../dispatcher/actions';
const MIN_TOOLTIP_HEIGHT = 25;
export default createReactClass({
displayName: 'Tooltip',
propTypes: {
interface IProps {
// Class applied to the element used to position the tooltip
className: PropTypes.string,
className: string,
// Class applied to the tooltip itself
tooltipClassName: PropTypes.string,
tooltipClassName?: string,
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
visible: PropTypes.bool,
visible?: boolean,
// the react element to put into the tooltip
label: PropTypes.node,
},
label: React.ReactNode,
}
getDefaultProps() {
return {
visible: true,
};
},
export default class Tooltip extends React.Component<IProps> {
private tooltipContainer: HTMLElement;
private tooltip: void | Element | Component<Element, any, any>;
private parent: Element;
public static readonly defaultProps = {
visible: true,
};
// Create a wrapper for the tooltip outside the parent and attach it to the body element
componentDidMount: function() {
public componentDidMount() {
this.tooltipContainer = document.createElement("div");
this.tooltipContainer.className = "mx_Tooltip_wrapper";
document.body.appendChild(this.tooltipContainer);
window.addEventListener('scroll', this._renderTooltip, true);
window.addEventListener('scroll', this.renderTooltip, true);
this.parent = ReactDOM.findDOMNode(this).parentNode;
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
this._renderTooltip();
},
this.renderTooltip();
}
componentDidUpdate: function() {
this._renderTooltip();
},
public componentDidUpdate() {
this.renderTooltip();
}
// Remove the wrapper element, as the tooltip has finished using it
componentWillUnmount: function() {
dis.dispatch({
action: 'view_tooltip',
public componentWillUnmount() {
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: null,
parent: null,
});
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
document.body.removeChild(this.tooltipContainer);
window.removeEventListener('scroll', this._renderTooltip, true);
},
window.removeEventListener('scroll', this.renderTooltip, true);
}
_updatePosition(style) {
private updatePosition(style: {[key: string]: any}) {
const parentBox = this.parent.getBoundingClientRect();
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
@ -91,16 +92,15 @@ export default createReactClass({
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset;
return style;
},
}
_renderTooltip: function() {
private renderTooltip() {
// Add the parent's position to the tooltips, so it's correctly
// positioned, also taking into account any window zoom
// NOTE: The additional 6 pixels for the left position, is to take account of the
// tooltips chevron
const parent = ReactDOM.findDOMNode(this).parentNode;
let style = {};
style = this._updatePosition(style);
const parent = ReactDOM.findDOMNode(this).parentNode as Element;
const style = this.updatePosition({});
// Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount.
style.display = this.props.visible ? "block" : "none";
@ -118,21 +118,21 @@ export default createReactClass({
);
// Render the tooltip manually, as we wish it not to be rendered within the parent
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
// Tell the roomlist about us so it can manipulate us if it wishes
dis.dispatch({
action: 'view_tooltip',
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: this.tooltip,
parent: parent,
});
},
}
render: function() {
public render() {
// Render a placeholder
return (
<div className={this.props.className} >
</div>
);
},
});
}
}

View file

@ -67,7 +67,13 @@ class Category extends React.PureComponent {
const localScrollTop = Math.max(0, scrollTop - listTop);
return (
<section className="mx_EmojiPicker_category" data-category-id={this.props.id}>
<section
id={`mx_EmojiPicker_category_${this.props.id}`}
className="mx_EmojiPicker_category"
data-category-id={this.props.id}
role="tabpanel"
aria-label={name}
>
<h2 className="mx_EmojiPicker_category_label">
{name}
</h2>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MenuItem} from "../../structures/ContextMenu";
class Emoji extends React.PureComponent {
static propTypes = {
@ -30,14 +31,18 @@ class Emoji extends React.PureComponent {
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
return (
<li onClick={() => onClick(emoji)}
<MenuItem
element="li"
onClick={() => onClick(emoji)}
onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item_wrapper">
className="mx_EmojiPicker_item_wrapper"
label={emoji.unicode}
>
<div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
{emoji.unicode}
</div>
</li>
</MenuItem>
);
}
}

View file

@ -147,8 +147,12 @@ class EmojiPicker extends React.Component {
// We update this here instead of through React to avoid re-render on scroll.
if (cat.visible) {
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", true);
cat.ref.current.setAttribute("tabindex", 0);
} else {
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", false);
cat.ref.current.setAttribute("tabindex", -1);
}
}
}

View file

@ -16,23 +16,89 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames";
import {_t} from "../../../languageHandler";
import {Key} from "../../../Keyboard";
class Header extends React.PureComponent {
static propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
onAnchorClick: PropTypes.func.isRequired,
refs: PropTypes.object,
};
findNearestEnabled(index, delta) {
index += this.props.categories.length;
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
while (index < cats.length && index >= 0) {
if (cats[index].enabled) return index % this.props.categories.length;
index += delta > 0 ? 1 : -1;
}
}
changeCategoryRelative(delta) {
const current = this.props.categories.findIndex(c => c.visible);
this.changeCategoryAbsolute(current + delta, delta);
}
changeCategoryAbsolute(index, delta=1) {
const category = this.props.categories[this.findNearestEnabled(index, delta)];
if (category) {
this.props.onAnchorClick(category.id);
category.ref.current.focus();
}
}
// Implements ARIA Tabs with Automatic Activation pattern
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
onKeyDown = (ev) => {
let handled = true;
switch (ev.key) {
case Key.ARROW_LEFT:
this.changeCategoryRelative(-1);
break;
case Key.ARROW_RIGHT:
this.changeCategoryRelative(1);
break;
case Key.HOME:
this.changeCategoryAbsolute(0);
break;
case Key.END:
this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
break;
default:
handled = false;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
};
render() {
return (
<nav className="mx_EmojiPicker_header">
{this.props.categories.map(category => (
<button disabled={!category.enabled} key={category.id} ref={category.ref}
className={`mx_EmojiPicker_anchor ${category.visible ? 'mx_EmojiPicker_anchor_visible' : ''}
mx_EmojiPicker_anchor_${category.id}`}
onClick={() => this.props.onAnchorClick(category.id)} title={category.name} />
))}
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
{this.props.categories.map(category => {
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
mx_EmojiPicker_anchor_visible: category.visible,
});
// Properties of this button are also modified by EmojiPicker's updateVisibility in DOM.
return <button
disabled={!category.enabled}
key={category.id}
ref={category.ref}
className={classes}
onClick={() => this.props.onAnchorClick(category.id)}
title={category.name}
role="tab"
tabIndex={category.visible ? 0 : -1} // roving
aria-selected={category.visible}
aria-controls={`mx_EmojiPicker_category_${category.id}`}
/>;
})}
</nav>
);
}

View file

@ -72,7 +72,7 @@ class QuickReactions extends React.Component {
</React.Fragment>
}
</h2>
<ul className="mx_EmojiPicker_list">
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
{QUICK_REACTIONS.map(emoji => <Emoji
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}

View file

@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from "prop-types";
import EmojiPicker from "./EmojiPicker";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
class ReactionPicker extends React.Component {
static propTypes = {
@ -105,6 +106,7 @@ class ReactionPicker extends React.Component {
"key": reaction,
},
});
dis.dispatch({action: "message_sent"});
return true;
}
}

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';
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,91 +0,0 @@
/*
Copyright 2017, 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 { _t } from '../../../languageHandler';
import PlatformPeg from '../../../PlatformPeg';
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
export default createReactClass({
propTypes: {
status: PropTypes.string.isRequired,
// Currently for error detail but will be usable for download progress
// once that is a thing that squirrel passes through electron.
detail: PropTypes.string,
},
getDefaultProps: function() {
return {
detail: '',
};
},
getStatusText: function() {
// we can't import the enum from riot-web as we don't want matrix-react-sdk
// to depend on riot-web. so we grab it as a normal object via API instead.
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
switch (this.props.status) {
case updateCheckStatusEnum.ERROR:
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
case updateCheckStatusEnum.CHECKING:
return _t('Checking for an update...');
case updateCheckStatusEnum.NOTAVAILABLE:
return _t('No update available.');
case updateCheckStatusEnum.DOWNLOADING:
return _t('Downloading update...');
}
},
hideToolbar: function() {
PlatformPeg.get().stopUpdateCheck();
},
render: function() {
const message = this.getStatusText();
const warning = _t('Warning');
if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
return <div></div>;
}
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
const doneStatuses = [
updateCheckStatusEnum.ERROR,
updateCheckStatusEnum.NOTAVAILABLE,
];
let image;
if (doneStatuses.includes(this.props.status)) {
image = <img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />;
} else {
image = <img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/spinner.gif")} width="24" height="23" alt="" />;
}
return (
<div className="mx_MatrixToolbar">
{image}
<div className="mx_MatrixToolbar_content">
{message}
</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

@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import {_t} from '../../../languageHandler';
import classNames from 'classnames';
import {MatrixClientPeg} from "../../../MatrixClientPeg";

View file

@ -19,7 +19,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -28,6 +28,7 @@ import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {Action} from "../../../dispatcher/actions";
export default createReactClass({
displayName: 'GroupMemberInfo',
@ -103,7 +104,7 @@ export default createReactClass({
).then(() => {
// return to the user list
dis.dispatch({
action: "view_user",
action: Action.ViewUser,
member: null,
});
}).catch((e) => {
@ -124,7 +125,7 @@ export default createReactClass({
_onCancel: function(e) {
// Go back to the user list
dis.dispatch({
action: "view_user",
action: Action.ViewUser,
member: null,
});
},

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';

View file

@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -21,7 +21,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
export default class MKeyVerificationRequest extends React.Component {

View file

@ -1,7 +1,7 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@ -21,12 +21,10 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
import SettingsStore from '../../../settings/SettingsStore';
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -41,18 +39,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread();
const onCryptoClick = () => {
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event: mxEvent},
);
};
let e2eInfoCallback = null;
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
e2eInfoCallback = onCryptoClick;
}
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<MessageContextMenu
@ -60,7 +46,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
e2eInfoCallback={e2eInfoCallback}
onFinished={closeMenu}
/>
</ContextMenu>;

View file

@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher";
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
@ -60,6 +61,7 @@ export default class ReactionsRowButton extends React.PureComponent {
"key": content,
},
});
dis.dispatch({action: "message_sent"});
}
};

View file

@ -19,7 +19,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -131,7 +131,9 @@ export default createReactClass({
return (
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
{ content }
<div className="mx_SenderProfile_hover">
{ content }
</div>
</div>
);
},

View file

@ -25,7 +25,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
import {formatDate} from '../../../DateUtils';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore";

View file

@ -23,6 +23,8 @@ import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
const GROUP_PHASES = [
RIGHT_PANEL_PHASES.GroupMemberInfo,
@ -40,10 +42,10 @@ export default class GroupHeaderButtons extends HeaderButtons {
this._onRoomsClicked = this._onRoomsClicked.bind(this);
}
onAction(payload) {
onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === "view_user") {
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
} else {

View file

@ -19,7 +19,7 @@ limitations under the License.
*/
import React from 'react';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import RightPanelStore from "../../../stores/RightPanelStore";
export const HEADER_KIND_ROOM = "room";

View file

@ -23,6 +23,8 @@ import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList,
@ -39,9 +41,9 @@ export default class RoomHeaderButtons extends HeaderButtons {
this._onNotificationsClicked = this._onNotificationsClicked.bind(this);
}
onAction(payload) {
onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === "view_user") {
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
} else {

View file

@ -21,7 +21,7 @@ import React, {useCallback, useMemo, useState, useEffect, useContext} from 'reac
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Group, RoomMember, User} from 'matrix-js-sdk';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -44,6 +44,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
import {Action} from "../../../dispatcher/actions";
const _disambiguateDevices = (devices) => {
const names = Object.create(null);
@ -841,7 +842,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
// return to the user list
dis.dispatch({
action: "view_user",
action: Action.ViewUser,
member: null,
});
}).catch((e) => {

View file

@ -220,10 +220,10 @@ export default class AliasSettings extends React.Component {
}
}).catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
title: _t("Error creating alias"),
Modal.createTrackedDialog('Error creating address', '', ErrorDialog, {
title: _t("Error creating address"),
description: _t(
"There was an error creating that alias. It may not be allowed by the server " +
"There was an error creating that address. It may not be allowed by the server " +
"or a temporary failure occurred.",
),
});
@ -245,15 +245,15 @@ export default class AliasSettings extends React.Component {
console.error(err);
let description;
if (err.errcode === "M_FORBIDDEN") {
description = _t("You don't have permission to delete the alias.");
description = _t("You don't have permission to delete the address.");
} else {
description = _t(
"There was an error removing that alias. It may no longer exist or a temporary " +
"There was an error removing that address. It may no longer exist or a temporary " +
"error occurred.",
);
}
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
title: _t("Error removing alias"),
Modal.createTrackedDialog('Error removing address', '', ErrorDialog, {
title: _t("Error removing address"),
description,
});
});

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import Tinter from '../../../Tinter';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const ROOM_COLORS = [

View file

@ -23,8 +23,9 @@ import createReactClass from 'create-react-class';
import * as sdk from "../../../index";
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {Action} from "../../../dispatcher/actions";
export default createReactClass({
@ -37,7 +38,7 @@ export default createReactClass({
_onClickUserSettings: (e) => {
e.preventDefault();
e.stopPropagation();
dis.dispatch({action: 'view_user_settings'});
dis.fire(Action.ViewUserSettings);
},
render: function() {

View file

@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import * as ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t } from '../../../languageHandler';
@ -141,6 +141,15 @@ export default createReactClass({
return counters;
},
_onScroll: function(rect) {
if (this.props.onResize) {
this.props.onResize();
}
/* Force refresh of PersistedElements which may be partially hidden */
window.dispatchEvent(new Event('resize'));
},
render: function() {
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -265,7 +274,7 @@ export default createReactClass({
}
return (
<AutoHideScrollbar className={classes} style={style} >
<AutoHideScrollbar className={classes} style={style} onScroll={this._onScroll}>
{ stateViews }
{ appsDrawer }
{ fileDropTarget }

Some files were not shown because too many files have changed in this diff Show more