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

This commit is contained in:
Michael Telatynski 2018-05-02 13:08:38 +01:00
commit f2102e283c
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
71 changed files with 1699 additions and 1077 deletions

View file

@ -27,7 +27,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
@ -93,8 +92,8 @@ const CategoryRoomList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupStore
.addRoomToGroupSummary(addr.address)
return GroupStore
.addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
@ -174,7 +173,8 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupStore.removeRoomFromGroupSummary(
GroupStore.removeRoomFromGroupSummary(
this.props.groupId,
this.props.summaryInfo.room_id,
).catch((err) => {
console.error('Error whilst removing room from group summary', err);
@ -269,7 +269,7 @@ const RoleUserList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupStore
return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
@ -344,7 +344,8 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupStore.removeUserFromGroupSummary(
GroupStore.removeUserFromGroupSummary(
this.props.groupId,
this.props.summaryInfo.user_id,
).catch((err) => {
console.error('Error whilst removing user from group summary', err);
@ -390,15 +391,6 @@ const FeaturedUser = React.createClass({
},
});
const GroupContext = {
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
};
CategoryRoomList.contextTypes = GroupContext;
FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupContext;
const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite";
@ -415,12 +407,6 @@ export default React.createClass({
groupStore: PropTypes.instanceOf(GroupStore),
},
getChildContext: function() {
return {
groupStore: this._groupStore,
};
},
getInitialState: function() {
return {
summary: null,
@ -440,6 +426,7 @@ export default React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
@ -448,8 +435,8 @@ export default React.createClass({
},
componentWillUnmount: function() {
this._unmounted = true;
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupStore.removeAllListeners();
},
componentWillReceiveProps: function(newProps) {
@ -464,8 +451,7 @@ export default React.createClass({
},
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
if (this._unmounted || group.groupId !== this.props.groupId) return;
if (group.myMembership === 'leave') {
// Leave settings - the user might have clicked the "Leave" button
this._closeSettings();
@ -478,34 +464,11 @@ export default React.createClass({
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
}
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary();
if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which
// requires that the fields are strings, not null)
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
summary.profile[k] = summary.profile[k] || "";
});
}
this.setState({
summary,
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: this._groupStore.getGroupPublicity(),
isUserPrivileged: this._groupStore.isUserPrivileged(),
groupRooms: this._groupStore.getGroupRooms(),
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
isUserMember: this._groupStore.getGroupMembers().some(
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
});
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
let willDoOnboarding = false;
this._groupStore.on('error', (err) => {
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
GroupStore.on('error', (err, errorGroupId) => {
if (this._unmounted || groupId !== errorGroupId) return;
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
action: 'do_after_sync_prepared',
@ -520,15 +483,45 @@ export default React.createClass({
this.setState({
summary: null,
error: err,
editing: false,
});
});
},
onGroupStoreUpdated(firstInit) {
if (this._unmounted) return;
const summary = GroupStore.getSummary(this.props.groupId);
if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which
// requires that the fields are strings, not null)
["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
summary.profile[k] = summary.profile[k] || "";
});
}
this.setState({
summary,
summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
groupRooms: GroupStore.getGroupRooms(this.props.groupId),
groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
(m) => m.userId === this._matrixClient.credentials.userId,
),
error: null,
});
// XXX: This might not work but this.props.groupIsNew unused anyway
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
},
_fetchInviterProfile(userId) {
this.setState({
inviterProfileBusy: true,
});
this._matrixClient.getProfileInfo(userId).then((resp) => {
if (this._unmounted) return;
this.setState({
inviterProfile: {
avatarUrl: resp.avatar_url,
@ -538,6 +531,7 @@ export default React.createClass({
}).catch((e) => {
console.error('Error getting group inviter profile', e);
}).finally(() => {
if (this._unmounted) return;
this.setState({
inviterProfileBusy: false,
});
@ -677,7 +671,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.acceptGroupInvite().then(() => {
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -696,7 +690,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -715,7 +709,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.joinGroup().then(() => {
GroupStore.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -743,7 +737,7 @@ export default React.createClass({
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 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.
@ -351,16 +351,16 @@ export default React.createClass({
guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).catch((e) => {
console.error(`Error attempting to load session: ${e}`);
return false;
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
});
}).done();
// Note we don't catch errors from this: we catch everything within
// loadSession as there's logic there to ask the user if they want
// to try logging out.
});
},
componentWillUnmount: function() {

View file

@ -27,7 +27,7 @@ import Analytics from '../../Analytics';
import RateLimitedFunc from '../../ratelimitedfunc';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import { formatCount } from '../../utils/FormattingUtils';
@ -120,7 +120,7 @@ module.exports = React.createClass({
if (this.context.matrixClient) {
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
},
getInitialState: function() {
@ -132,26 +132,23 @@ module.exports = React.createClass({
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
_initGroupStore(groupId) {
if (!groupId) return;
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
onGroupStoreUpdated: function() {
this.setState({
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
},

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 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.
@ -63,6 +63,7 @@ const gHVersionLabel = function(repo, token='') {
const SIMPLE_SETTINGS = [
{ id: "urlPreviewsEnabled" },
{ id: "autoplayGifsAndVideos" },
{ id: "alwaysShowEncryptionIcons" },
{ id: "hideReadReceipts" },
{ id: "dontSendTypingNotifications" },
{ id: "alwaysShowTimestamps" },
@ -801,10 +802,10 @@ module.exports = React.createClass({
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contian messages.",
"other users. They do not contain messages.",
)
}</p>
<button className="mx_UserSettings_button danger"
<button className="mx_UserSettings_button"
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
</button>
</div>

View file

@ -22,7 +22,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -243,9 +243,8 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
const results = [];
groupStore.getGroupRooms().forEach((r) => {
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);

View file

@ -1,5 +1,6 @@
/*
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.
@ -36,8 +37,18 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
// Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
// cancelled (BaseDialog itself only calls this with false).
onFinished: PropTypes.func.isRequired,
// Whether the dialog should have a 'close' button that will
// cause the dialog to be cancelled. This should only be set
// to false if there is nothing the app can sensibly do if the
// dialog is cancelled, eg. "We can't restore your session and
// the app cannot work". Default: true.
hasCancel: PropTypes.bool,
// called when a key is pressed
onKeyDown: PropTypes.func,
@ -56,6 +67,12 @@ export default React.createClass({
contentId: React.PropTypes.string,
},
getDefaultProps: function() {
return {
hasCancel: true,
};
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
@ -74,15 +91,15 @@ export default React.createClass({
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (e.keyCode === KeyCode.ESCAPE) {
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
this.props.onFinished(false);
}
},
_onCancelClick: function(e) {
this.props.onFinished();
this.props.onFinished(false);
},
render: function() {
@ -101,11 +118,11 @@ export default React.createClass({
// AT users can skip its presentation.
aria-describedby={this.props.contentId}
>
<AccessibleButton onClick={this._onCancelClick}
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
</AccessibleButton> : null }
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title }
</div>

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 OpenMarket 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.
@ -105,6 +106,8 @@ export default class BugReportDialog extends React.Component {
render() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let error = null;
if (this.state.err) {
@ -113,13 +116,6 @@ export default class BugReportDialog extends React.Component {
</div>;
}
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel}>
{ _t("Cancel") }
</button>;
}
let progress = null;
if (this.state.busy) {
progress = (
@ -131,11 +127,11 @@ export default class BugReportDialog extends React.Component {
}
return (
<div className="mx_BugReportDialog">
<div className="mx_Dialog_title">
{ _t("Submit debug logs") }
</div>
<div className="mx_Dialog_content">
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>
{ _t(
"Debug logs contain application usage data including your " +
@ -146,7 +142,7 @@ export default class BugReportDialog extends React.Component {
</p>
<p>
{ _t(
"<a>Click here</a> to create a GitHub issue.",
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.",
{},
{
a: (sub) => <a
@ -191,19 +187,13 @@ export default class BugReportDialog extends React.Component {
{progress}
{error}
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onSubmit}
autoFocus={true}
disabled={this.state.busy}
>
{ _t("Send logs") }
</button>
{cancelButton}
</div>
</div>
<DialogButtons primaryButton={_t("Send logs")}
onPrimaryButtonClick={this._onSubmit}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -1,5 +1,6 @@
/*
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.
@ -30,59 +31,79 @@ export default React.createClass({
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
if (this.refs.bugreportLink) {
this.refs.bugreportLink.focus();
}
},
_sendBugReport: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
},
_continueClicked: function() {
this.props.onFinished(true);
_onClearStorageClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"),
description:
<div>{ _t("Log out and remove encryption keys?") }</div>,
button: _t("Sign out"),
danger: true,
onFinished: this.props.onFinished,
});
},
_onRefreshClick: function() {
// Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful.
window.location.reload(true);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bugreport;
const clearStorageButton = (
<button onClick={this._onClearStorageClick} className="danger">
{ _t("Clear Storage and Sign Out") }
</button>
);
let dialogButtons;
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>
{ _t(
"Otherwise, <a>click here</a> to send a bug report.",
{},
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
key="bugreport" href='#'>{ sub }</a> },
) }
</p>
);
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
onPrimaryButtonClick={this._sendBugReport}
focus={true}
hasCancel={false}
>
{ clearStorageButton }
</DialogButtons>;
} else {
dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
onPrimaryButtonClick={this._onRefreshClick}
focus={true}
hasCancel={false}
>
{ clearStorageButton }
</DialogButtons>;
}
const shouldFocusContinueButton =!(bugreport==true);
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={_t('Unable to restore session')}
title={_t('Unable to restore session')}
contentId='mx_Dialog_content'
hasCancel={false}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{ _t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.") }</p>
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.") }</p>
<p>{ _t(
"If you have previously used a more recent version of Riot, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.",
) }</p>
{ bugreport }
<p>{ _t(
"Clearing your browser's storage may fix the problem, but will sign you " +
"out and cause any encrypted chat history to become unreadable.",
) }</p>
</div>
<DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
onCancel={this.props.onFinished} />
{ dialogButtons }
</BaseDialog>
);
},

View file

@ -54,6 +54,7 @@ export default class AppTile extends React.Component {
this._onInitialLoad = this._onInitialLoad.bind(this);
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
}
/**
@ -499,6 +500,13 @@ export default class AppTile extends React.Component {
}
}
_onPopoutWidgetClick(e) {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes,noreferrer=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
}
render() {
let appTileBody;
@ -581,6 +589,7 @@ export default class AppTile extends React.Component {
// Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
const showPictureSnapshotIcon = 'img/camera_green.svg';
const popoutWidgetIcon = 'img/button-new-window.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
return (
@ -599,15 +608,25 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <TintableSvgButton
src={showPictureSnapshotIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Picture')}
onClick={this._onSnapshotClick}
width="10"
height="10"
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <TintableSvgButton
src={popoutWidgetIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
width="10"
height="10"
/> }
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <TintableSvgButton
src={showPictureSnapshotIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Picture')}
onClick={this._onSnapshotClick}
width="10"
height="10"
/> }
{ /* Edit widget */ }
{ showEditButton && <TintableSvgButton
@ -670,6 +689,8 @@ AppTile.propTypes = {
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the delete icon
showDelete: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Widget apabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
@ -686,6 +707,7 @@ AppTile.defaultProps = {
showTitle: true,
showMinimise: true,
showDelete: true,
showPopout: true,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
};

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Aidan Gauland
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.
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
@ -33,10 +32,26 @@ module.exports = React.createClass({
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// onClick handler for the cancel button.
onCancel: PropTypes.func.isRequired,
onCancel: PropTypes.func,
focus: PropTypes.bool,
disabled: PropTypes.bool,
},
getDefaultProps: function() {
return {
hasCancel: true,
disabled: false,
};
},
_onCancelClick: function() {
this.props.onCancel();
},
render: function() {
@ -44,18 +59,23 @@ module.exports = React.createClass({
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
let cancelButton;
if (this.props.hasCancel) {
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
{ _t("Cancel") }
</button>;
}
return (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
{ cancelButton }
</div>
);
},

View file

@ -24,6 +24,7 @@ import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -57,6 +58,8 @@ export default React.createClass({
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
this._onFlairStoreUpdated();
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
@ -80,6 +83,11 @@ export default React.createClass({
});
},
_refreshGroup(groupId) {
GroupStore.refreshGroupRooms(groupId);
GroupStore.refreshGroupMembers(groupId);
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
@ -89,6 +97,10 @@ export default React.createClass({
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
if (this.props.tag[0] === '+') {
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
},
onContextButtonClick: function(e) {

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import TintableSvg from './TintableSvg';
import AccessibleButton from './AccessibleButton';
export default class TintableSvgButton extends React.Component {
@ -39,9 +40,11 @@ export default class TintableSvgButton extends React.Component {
width={this.props.width}
height={this.props.height}
></TintableSvg>
<span
<AccessibleButton
onClick={this.props.onClick}
element='span'
title={this.props.title}
onClick={this.props.onClick} />
/>
</span>
);
}

View file

@ -23,7 +23,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
@ -47,33 +47,37 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore(this.props.groupId);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
onGroupStoreUpdated: function() {
if (this._unmounted) return;
this.setState({
isUserInvited: this._groupStore.getGroupInvitedMembers().some(
isUserInvited: GroupStore.getGroupInvitedMembers(this.props.groupId).some(
(m) => m.userId === this.props.groupMember.userId,
),
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
},

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -42,9 +42,12 @@ export default React.createClass({
this._initGroupStore(this.props.groupId);
},
componentWillUnmount: function() {
this._unmounted = true;
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
GroupStore.registerListener(groupId, () => {
this._fetchMembers();
});
},
@ -52,8 +55,8 @@ export default React.createClass({
_fetchMembers: function() {
if (this._unmounted) return;
this.setState({
members: this._groupStore.getGroupMembers(),
invitedMembers: this._groupStore.getGroupInvitedMembers(),
members: GroupStore.getGroupMembers(this.props.groupId),
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
});
},

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import { _t } from '../../../languageHandler.js';
@ -41,15 +40,18 @@ export default React.createClass({
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
this.setState({
isGroupPublicised: this._groupStore.getGroupPublicity(),
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: GroupStore.getGroupPublicity(groupId),
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
});
});
},
componentWillUnmount() {
if (this._groupStoreToken) this._groupStoreToken.unregister();
},
_onPublicityToggle: function(e) {
e.stopPropagation();
this.setState({
@ -57,7 +59,7 @@ export default React.createClass({
// Optimistic early update
isGroupPublicised: !this.state.isGroupPublicised,
});
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
this.setState({
busy: false,
});

View file

@ -21,7 +21,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
module.exports = React.createClass({
displayName: 'GroupRoomInfo',
@ -50,29 +50,26 @@ module.exports = React.createClass({
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
componentWillUnmount() {
this._unregisterGroupStore();
this._unregisterGroupStore(this.props.groupId);
},
_initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this._groupStore.registerListener(this.onGroupStoreUpdated);
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
_unregisterGroupStore() {
if (this._groupStore) {
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
}
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
_updateGroupRoom() {
this.setState({
groupRoom: this._groupStore.getGroupRooms().find(
groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
(r) => r.roomId === this.props.groupRoomId,
),
});
@ -80,7 +77,7 @@ module.exports = React.createClass({
onGroupStoreUpdated: function() {
this.setState({
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
this._updateGroupRoom();
},
@ -100,7 +97,7 @@ module.exports = React.createClass({
this.setState({groupRoomRemoveLoading: true});
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
this._groupStore.removeRoomFromGroup(roomId).then(() => {
GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
dis.dispatch({
action: "view_group_room_list",
});
@ -134,7 +131,7 @@ module.exports = React.createClass({
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
const roomName = this.state.groupRoom.displayname;
this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
const INITIAL_LOAD_NUM_ROOMS = 30;
@ -39,22 +39,31 @@ export default React.createClass({
this._initGroupStore(this.props.groupId);
},
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore();
},
_unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this._fetchRooms();
});
this._groupStore.on('error', (err) => {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
// XXX: This is also leaked - we should remove it when unmounting
GroupStore.on('error', (err, errorGroupId) => {
if (errorGroupId !== groupId) return;
this.setState({
rooms: null,
});
});
},
_fetchRooms: function() {
onGroupStoreUpdated: function() {
if (this._unmounted) return;
this.setState({
rooms: this._groupStore.getGroupRooms(),
rooms: GroupStore.getGroupRooms(this.props.groupId),
});
},

View file

@ -20,7 +20,7 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
export default class MAudioBody extends React.Component {
@ -54,7 +54,7 @@ export default class MAudioBody extends React.Component {
let decryptedBlob;
decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(decryptedBlob);
return URL.createObjectURL(decryptedBlob);
}).done((url) => {
this.setState({
decryptedUrl: url,
@ -69,6 +69,12 @@ export default class MAudioBody extends React.Component {
}
}
componentWillUnmount() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
}
render() {
const content = this.props.mxEvent.getContent();

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket 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.
@ -99,16 +100,27 @@ Tinter.registerTintable(updateTintedDownloadImage);
// overridable so that people running their own version of the client can
// choose a different renderer.
//
// To that end the first version of the blob generation will be the following
// To that end the current version of the blob generation is the following
// html:
//
// <html><head><script>
// window.onmessage=function(e){eval("("+e.data.code+")")(e)}
// var params = window.location.search.substring(1).split('&');
// var lockOrigin;
// for (var i = 0; i < params.length; ++i) {
// var parts = params[i].split('=');
// if (parts[0] == 'origin') lockOrigin = decodeURIComponent(parts[1]);
// }
// window.onmessage=function(e){
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
// }
// </script></head><body></body></html>
//
// This waits to receive a message event sent using the window.postMessage API.
// When it receives the event it evals a javascript function in data.code and
// runs the function passing the event as an argument.
// runs the function passing the event as an argument. This version adds
// support for a query parameter controlling the origin from which messages
// will be processed as an extra layer of security (note that the default URL
// is still 'v1' since it is backwards compatible).
//
// In particular it means that the rendering function can be written as a
// ordinary javascript function which then is turned into a string using
@ -325,6 +337,7 @@ module.exports = React.createClass({
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
renderer_url = this.context.appConfig.cross_origin_renderer_url;
}
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
@ -348,7 +361,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MFileBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
<a className="mx_MFileBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName }
</a>
<div className="mx_MImageBody_size">

View file

@ -25,7 +25,7 @@ import ImageUtils from '../../../ImageUtils';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -49,6 +49,8 @@ export default class extends React.Component {
super(props);
this.onAction = this.onAction.bind(this);
this.onImageError = this.onImageError.bind(this);
this.onImageLoad = this.onImageLoad.bind(this);
this.onImageEnter = this.onImageEnter.bind(this);
this.onImageLeave = this.onImageLeave.bind(this);
this.onClientSync = this.onClientSync.bind(this);
@ -70,6 +72,7 @@ export default class extends React.Component {
this.context.matrixClient.on('sync', this.onClientSync);
}
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
onClientSync(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
@ -136,6 +139,11 @@ export default class extends React.Component {
});
}
onImageLoad() {
this.fixupHeight();
this.props.onWidgetLoad();
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
@ -153,6 +161,13 @@ export default class extends React.Component {
return this.state.decryptedThumbnailUrl;
}
return this.state.decryptedUrl;
} else if (content.info.mimetype == "image/svg+xml" && content.info.thumbnail_url) {
// special case to return client-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent
// billion lol attacks and similar
return this.context.matrixClient.mxcUrlToHttp(
content.info.thumbnail_url, 800, 600,
);
} else {
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
}
@ -160,7 +175,6 @@ export default class extends React.Component {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.fixupHeight();
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
@ -168,21 +182,20 @@ export default class extends React.Component {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
});
}
let decryptedBlob;
thumbnailPromise.then((thumbnailUrl) => {
return decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
}).then((contentUrl) => {
this.setState({
decryptedUrl: contentUrl,
decryptedThumbnailUrl: thumbnailUrl,
decryptedBlob: decryptedBlob,
});
this.props.onWidgetLoad();
});
}).catch((err) => {
console.warn("Unable to decrypt attachment: ", err);
@ -205,6 +218,13 @@ export default class extends React.Component {
dis.unregister(this.dispatcherRef);
this.context.matrixClient.removeListener('sync', this.onClientSync);
this._afterComponentWillUnmount();
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
if (this.state.decryptedThumbnailUrl) {
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
}
}
// To be overridden by subclasses (e.g. MStickerBody) for further
@ -229,7 +249,16 @@ export default class extends React.Component {
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
// which may well be much smaller than the 800x600 bounding box.
// FIXME: It will also break really badly for images with broken or missing thumbnails
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
// us, we can't even really layout the page nicely for it. Instead we have to assume
// it'll target 800x600 and we'll downsize if needed to make things fit.
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
let thumbHeight = null;
if (content.info) {
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
@ -239,18 +268,22 @@ export default class extends React.Component {
}
_messageContent(contentUrl, thumbUrl, content) {
const thumbnail = (
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
);
return (
<span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
</span>
);
}
@ -285,14 +318,6 @@ export default class extends React.Component {
);
}
if (this.state.imgError) {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
const contentUrl = this._getContentUrl();
let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
@ -301,20 +326,6 @@ export default class extends React.Component {
thumbUrl = this._getThumbUrl();
}
if (thumbUrl) {
return this._messageContent(contentUrl, thumbUrl, content);
} else if (content.body) {
return (
<span className="mx_MImageBody">
{ _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) }
</span>
);
} else {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
return this._messageContent(contentUrl, thumbUrl, content);
}
}

View file

@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -94,14 +94,14 @@ module.exports = React.createClass({
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
});
}
let decryptedBlob;
thumbnailPromise.then((thumbnailUrl) => {
return decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return readBlobAsDataUri(blob);
return URL.createObjectURL(blob);
}).then((contentUrl) => {
this.setState({
decryptedUrl: contentUrl,
@ -120,6 +120,15 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
if (this.state.decryptedThumbnailUrl) {
URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
}
},
render: function() {
const content = this.props.mxEvent.getContent();

View file

@ -33,6 +33,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
import {makeEventPermalink} from "../../../matrix-to";
import SettingsStore from "../../../settings/SettingsStore";
const ObjectUtils = require('../../../ObjectUtils');
@ -759,7 +760,11 @@ function E2ePadlockUnencrypted(props) {
}
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
} else {
return <img className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" {...props} />;
}
}
module.exports.getHandlerTile = getHandlerTile;

View file

@ -30,7 +30,7 @@ import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt');
import TagOrderStore from '../../../stores/TagOrderStore';
import RoomListStore from '../../../stores/RoomListStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -83,8 +83,6 @@ module.exports = React.createClass({
cli.on("Group.myMembership", this._onGroupMyMembership);
const dmRoomMap = DMRoomMap.shared();
this._groupStores = {};
this._groupStoreTokens = [];
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
@ -93,22 +91,22 @@ module.exports = React.createClass({
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getRooms();
// When the selected tags are changed, initialise a group store if necessary
this._tagStoreToken = TagOrderStore.addListener(() => {
// Listen to updates to group data. RoomList cares about members and rooms in order
// to filter the room list when group tags are selected.
this._groupStoreToken = GroupStore.registerListener(null, () => {
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
if (tag[0] !== '+') {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
this._groupStoreTokens.push(
this._groupStores[tag].registerListener(() => {
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
}),
);
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
});
// Filters themselves have changed, refresh the selected tags
});
this._tagStoreToken = TagOrderStore.addListener(() => {
// Filters themselves have changed
this.updateVisibleRooms();
});
@ -183,9 +181,9 @@ module.exports = React.createClass({
this._roomListStoreToken.remove();
}
if (this._groupStoreTokens.length > 0) {
// NB: GroupStore is not a Flux.Store
this._groupStoreTokens.forEach((token) => token.unregister());
// NB: GroupStore is not a Flux.Store
if (this._groupStoreToken) {
this._groupStoreToken.unregister();
}
// cancel any pending calls to the rate_limited_funcs
@ -259,12 +257,11 @@ module.exports = React.createClass({
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
const store = this._groupStores[tag];
if (!store) return;
if (tag[0] !== '+') return;
this._visibleRoomsForGroup[tag] = [];
store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
store.getGroupMembers().forEach((member) => {
GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
GroupStore.getGroupMembers(tag).forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),

View file

@ -174,6 +174,7 @@ export default class Stickerpicker extends React.Component {
showTitle={false}
showMinimise={true}
showDelete={false}
showPopout={false}
onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true}
whitelistCapabilities={['m.sticker']}