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

This commit is contained in:
Michael Telatynski 2019-12-02 23:20:47 +00:00
commit a062fe0096
10 changed files with 108 additions and 60 deletions

View file

@ -78,6 +78,7 @@ export const Key = {
CONTROL: "Control", CONTROL: "Control",
META: "Meta", META: "Meta",
SHIFT: "Shift", SHIFT: "Shift",
CONTEXT_MENU: "ContextMenu",
LESS_THAN: "<", LESS_THAN: "<",
GREATER_THAN: ">", GREATER_THAN: ">",

View file

@ -1,7 +1,6 @@
const React = require('react'); const React = require('react');
const ReactDom = require('react-dom'); const ReactDom = require('react-dom');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
const Velocity = require('velocity-animate'); const Velocity = require('velocity-animate');
/** /**
@ -11,10 +10,8 @@ const Velocity = require('velocity-animate');
* from DOM order. This makes it a lot simpler and lighter: if you need fully * from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries. * automatic positional animation, look at react-shuffle or similar libraries.
*/ */
module.exports = createReactClass({ export default class Velociraptor extends React.Component {
displayName: 'Velociraptor', static propTypes = {
propTypes: {
// either a list of child nodes, or a single child. // either a list of child nodes, or a single child.
children: PropTypes.any, children: PropTypes.any,
@ -26,82 +23,71 @@ module.exports = createReactClass({
// a list of transition options from the corresponding startStyle // a list of transition options from the corresponding startStyle
enterTransitionOpts: PropTypes.array, enterTransitionOpts: PropTypes.array,
}, };
getDefaultProps: function() { static defaultProps = {
return { startStyles: [],
startStyles: [], enterTransitionOpts: [],
enterTransitionOpts: [], };
};
}, constructor(props) {
super(props);
componentWillMount: function() {
this.nodes = {}; this.nodes = {};
this._updateChildren(this.props.children); this._updateChildren(this.props.children);
}, }
componentWillReceiveProps: function(nextProps) { componentDidUpdate() {
this._updateChildren(nextProps.children); this._updateChildren(this.props.children);
}, }
/** _updateChildren(newChildren) {
* update `this.children` according to the new list of children given
*/
_updateChildren: function(newChildren) {
const self = this;
const oldChildren = this.children || {}; const oldChildren = this.children || {};
this.children = {}; this.children = {};
React.Children.toArray(newChildren).forEach(function(c) { React.Children.toArray(newChildren).forEach((c) => {
if (oldChildren[c.key]) { if (oldChildren[c.key]) {
const old = oldChildren[c.key]; const old = oldChildren[c.key];
const oldNode = ReactDom.findDOMNode(self.nodes[old.key]); const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left != c.props.style.left) { if (oldNode && oldNode.style.left !== c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => {
// special case visibility because it's nonsensical to animate an invisible element // special case visibility because it's nonsensical to animate an invisible element
// so we always hidden->visible pre-transition and visible->hidden after // so we always hidden->visible pre-transition and visible->hidden after
if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') { if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
oldNode.style.visibility = c.props.style.visibility; oldNode.style.visibility = c.props.style.visibility;
} }
}); });
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
} }
if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
oldNode.style.visibility = c.props.style.visibility; oldNode.style.visibility = c.props.style.visibility;
} }
// clone the old element with the props (and children) of the new element // clone the old element with the props (and children) of the new element
// so prop updates are still received by the children. // so prop updates are still received by the children.
self.children[c.key] = React.cloneElement(old, c.props, c.props.children); this.children[c.key] = React.cloneElement(old, c.props, c.props.children);
} else { } else {
// new element. If we have a startStyle, use that as the style and go through // new element. If we have a startStyle, use that as the style and go through
// the enter animations // the enter animations
const newProps = {}; const newProps = {};
const restingStyle = c.props.style; const restingStyle = c.props.style;
const startStyles = self.props.startStyles; const startStyles = this.props.startStyles;
if (startStyles.length > 0) { if (startStyles.length > 0) {
const startStyle = startStyles[0]; const startStyle = startStyles[0];
newProps.style = startStyle; newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); // console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
} }
newProps.ref = ((n) => self._collectNode( newProps.ref = ((n) => this._collectNode(
c.key, n, restingStyle, c.key, n, restingStyle,
)); ));
self.children[c.key] = React.cloneElement(c, newProps); this.children[c.key] = React.cloneElement(c, newProps);
} }
}); });
}, }
/** _collectNode(k, node, restingStyle) {
* called when a child element is mounted/unmounted
*
* @param {string} k key of the child
* @param {null|Object} node On mount: React node. On unmount: null
* @param {Object} restingStyle final style
*/
_collectNode: function(k, node, restingStyle) {
if ( if (
node && node &&
this.nodes[k] === undefined && this.nodes[k] === undefined &&
@ -125,12 +111,12 @@ module.exports = createReactClass({
// and then we animate to the resting state // and then we animate to the resting state
Velocity(domNode, restingStyle, Velocity(domNode, restingStyle,
transitionOpts[i-1]) transitionOpts[i-1])
.then(() => { .then(() => {
// once we've reached the resting state, hide the element if // once we've reached the resting state, hide the element if
// appropriate // appropriate
domNode.style.visibility = restingStyle.visibility; domNode.style.visibility = restingStyle.visibility;
}); });
/* /*
console.log("enter:", console.log("enter:",
@ -153,13 +139,13 @@ module.exports = createReactClass({
if (domNode) Velocity.Utilities.removeData(domNode); if (domNode) Velocity.Utilities.removeData(domNode);
} }
this.nodes[k] = node; this.nodes[k] = node;
}, }
render: function() { render() {
return ( return (
<span> <span>
{ Object.values(this.children) } { Object.values(this.children) }
</span> </span>
); );
}, }
}); }

View file

@ -401,6 +401,11 @@ const LoggedInView = createReactClass({
const isClickShortcut = ev.target !== document.body && const isClickShortcut = ev.target !== document.body &&
(ev.key === Key.SPACE || ev.key === Key.ENTER); (ev.key === Key.SPACE || ev.key === Key.ENTER);
// Do not capture the context menu key to improve keyboard accessibility
if (ev.key === Key.CONTEXT_MENU) {
return;
}
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036 // XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
// If using Slate, consume the Backspace without first focusing as it causes an implosion // If using Slate, consume the Backspace without first focusing as it causes an implosion
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) { if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {

View file

@ -693,6 +693,10 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId]; const readReceipts = this._readReceiptsByEvent[eventId];
// Dev note: `this._isUnmounting.bind(this)` is important - it ensures that
// the function is run in the context of this class and not EventTile, therefore
// ensuring the right `this._mounted` variable is used by read receipts (which
// don't update their position if we, the MessagePanel, is unmounting).
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
@ -707,7 +711,7 @@ export default class MessagePanel extends React.Component {
readReceipts={readReceipts} readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap} readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting} checkUnmounting={this._isUnmounting.bind(this)}
eventSendStatus={mxEv.getAssociatedStatus()} eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour} isTwelveHour={this.props.isTwelveHour}

View file

@ -62,7 +62,7 @@ EMOJIBASE.forEach(emoji => {
DATA_BY_CATEGORY[categoryId].push(emoji); DATA_BY_CATEGORY[categoryId].push(emoji);
} }
// This is used as the string to match the query against when filtering emojis. // This is used as the string to match the query against when filtering emojis.
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
}); });
export const CATEGORY_HEADER_HEIGHT = 22; export const CATEGORY_HEADER_HEIGHT = 22;
@ -201,6 +201,7 @@ class EmojiPicker extends React.Component {
} }
onChangeFilter(filter) { onChangeFilter(filter) {
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
for (const cat of this.categories) { for (const cat of this.categories) {
let emojis; let emojis;
// If the new filter string includes the old filter string, we don't have to re-filter the whole dataset. // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.

View file

@ -49,8 +49,6 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = { this.state = {
language: languageHandler.getCurrentLanguage(), language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null, serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false, idServerHasUnsignedTerms: false,
@ -62,6 +60,7 @@ export default class GeneralUserSettingsTab extends React.Component {
}, },
emails: [], emails: [],
msisdns: [], msisdns: [],
...this._calculateThemeState(),
}; };
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this._onAction);
@ -80,6 +79,39 @@ export default class GeneralUserSettingsTab extends React.Component {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_calculateThemeState() {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that.
if (systemThemeExplicit) {
return {
theme: themeChoice,
useSystemTheme: true,
};
}
// If the user has set a theme explicitly, use that (no system theme matching)
if (themeExplicit) {
return {
theme: themeChoice,
useSystemTheme: false,
};
}
// Otherwise assume the defaults for the settings
return {
theme: themeChoice,
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
};
}
_onAction = (payload) => { _onAction = (payload) => {
if (payload.action === 'id_server_changed') { if (payload.action === 'id_server_changed') {
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
@ -89,11 +121,11 @@ export default class GeneralUserSettingsTab extends React.Component {
_onEmailsChange = (emails) => { _onEmailsChange = (emails) => {
this.setState({ emails }); this.setState({ emails });
} };
_onMsisdnsChange = (msisdns) => { _onMsisdnsChange = (msisdns) => {
this.setState({ msisdns }); this.setState({ msisdns });
} };
async _getThreepidState() { async _getThreepidState() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -193,9 +225,9 @@ export default class GeneralUserSettingsTab extends React.Component {
_onUseSystemThemeChanged = (checked) => { _onUseSystemThemeChanged = (checked) => {
this.setState({useSystemTheme: checked}); this.setState({useSystemTheme: checked});
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch({action: 'recheck_theme'}); dis.dispatch({action: 'recheck_theme'});
} };
_onPasswordChangeError = (err) => { _onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog // TODO: Figure out a design that doesn't involve replacing the current dialog
@ -307,12 +339,15 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderThemeSection() { _renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
const themeWatcher = new ThemeWatcher(); const themeWatcher = new ThemeWatcher();
let systemThemeSection; let systemThemeSection;
if (themeWatcher.isSystemThemeSupported()) { if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div> systemThemeSection = <div>
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE} <LabelledToggleSwitch
value={this.state.useSystemTheme}
label={SettingsStore.getDisplayName("use_system_theme")}
onChange={this._onUseSystemThemeChanged} onChange={this._onUseSystemThemeChanged}
/> />
</div>; </div>;

View file

@ -49,6 +49,17 @@ export default class LabsUserSettingsTab extends React.Component {
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div> <div className="mx_SettingsTab_heading">{_t("Labs")}</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t('Customise your experience with experimental labs features. ' +
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
rel='noopener' target='_blank'>{sub}</a>;
},
})
}
</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{flags} {flags}
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} /> <SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />

View file

@ -645,6 +645,7 @@
"Access Token:": "Access Token:", "Access Token:": "Access Token:",
"click to reveal": "click to reveal", "click to reveal": "click to reveal",
"Labs": "Labs", "Labs": "Labs",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
"Ignored/Blocked": "Ignored/Blocked", "Ignored/Blocked": "Ignored/Blocked",
"Error adding ignored user/server": "Error adding ignored user/server", "Error adding ignored user/server": "Error adding ignored user/server",
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",

View file

@ -20,6 +20,8 @@ import {DEFAULT_THEME, enumerateThemes} from "../../theme";
export default class ThemeController extends SettingController { export default class ThemeController extends SettingController {
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
if (!calculatedValue) return null; // Don't override null themes
const themes = enumerateThemes(); const themes = enumerateThemes();
// Override in case some no longer supported theme is stored here // Override in case some no longer supported theme is stored here
if (!themes[calculatedValue]) { if (!themes[calculatedValue]) {

View file

@ -80,6 +80,8 @@ export class ThemeWatcher {
} }
getEffectiveTheme() { getEffectiveTheme() {
// Dev note: Much of this logic is replicated in the GeneralUserSettingsTab
// If the user has specifically enabled the system matching option (excluding default), // If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting // then use that over anything else. We pick the lowest possible level for the setting
// to ensure the ordering otherwise works. // to ensure the ordering otherwise works.