Merge branches 'develop' and 't3chguy/context_menus' of github.com:matrix-org/matrix-react-sdk into t3chguy/context_menus
This commit is contained in:
commit
a062fe0096
10 changed files with 108 additions and 60 deletions
|
@ -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: ">",
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -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")) {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue