mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge branches 'develop' and 't3chguy/context_menus' of github.com:matrix-org/matrix-react-sdk into t3chguy/context_menus
						commit
						a062fe0096
					
				|  | @ -78,6 +78,7 @@ export const Key = { | |||
|     CONTROL: "Control", | ||||
|     META: "Meta", | ||||
|     SHIFT: "Shift", | ||||
|     CONTEXT_MENU: "ContextMenu", | ||||
| 
 | ||||
|     LESS_THAN: "<", | ||||
|     GREATER_THAN: ">", | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| const React = require('react'); | ||||
| const ReactDom = require('react-dom'); | ||||
| import PropTypes from 'prop-types'; | ||||
| import createReactClass from 'create-react-class'; | ||||
| 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 | ||||
|  * automatic positional animation, look at react-shuffle or similar libraries. | ||||
|  */ | ||||
| module.exports = createReactClass({ | ||||
|     displayName: 'Velociraptor', | ||||
| 
 | ||||
|     propTypes: { | ||||
| export default class Velociraptor extends React.Component { | ||||
|     static propTypes = { | ||||
|         // either a list of child nodes, or a single child.
 | ||||
|         children: PropTypes.any, | ||||
| 
 | ||||
|  | @ -26,82 +23,71 @@ module.exports = createReactClass({ | |||
| 
 | ||||
|         // a list of transition options from the corresponding startStyle
 | ||||
|         enterTransitionOpts: PropTypes.array, | ||||
|     }, | ||||
|     }; | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|         return { | ||||
|             startStyles: [], | ||||
|             enterTransitionOpts: [], | ||||
|         }; | ||||
|     }, | ||||
|     static defaultProps = { | ||||
|         startStyles: [], | ||||
|         enterTransitionOpts: [], | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this.nodes = {}; | ||||
|         this._updateChildren(this.props.children); | ||||
|     }, | ||||
|     } | ||||
| 
 | ||||
|     componentWillReceiveProps: function(nextProps) { | ||||
|         this._updateChildren(nextProps.children); | ||||
|     }, | ||||
|     componentDidUpdate() { | ||||
|         this._updateChildren(this.props.children); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * update `this.children` according to the new list of children given | ||||
|      */ | ||||
|     _updateChildren: function(newChildren) { | ||||
|         const self = this; | ||||
|     _updateChildren(newChildren) { | ||||
|         const oldChildren = this.children || {}; | ||||
|         this.children = {}; | ||||
|         React.Children.toArray(newChildren).forEach(function(c) { | ||||
|         React.Children.toArray(newChildren).forEach((c) => { | ||||
|             if (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) { | ||||
|                     Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { | ||||
|                 if (oldNode && oldNode.style.left !== c.props.style.left) { | ||||
|                     Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => { | ||||
|                         // special case visibility because it's nonsensical to animate an invisible element
 | ||||
|                         // 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; | ||||
|                         } | ||||
|                     }); | ||||
|                     //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; | ||||
|                 } | ||||
|                 // clone the old element with the props (and children) of the new element
 | ||||
|                 // 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 { | ||||
|                 // new element. If we have a startStyle, use that as the style and go through
 | ||||
|                 // the enter animations
 | ||||
|                 const newProps = {}; | ||||
|                 const restingStyle = c.props.style; | ||||
| 
 | ||||
|                 const startStyles = self.props.startStyles; | ||||
|                 const startStyles = this.props.startStyles; | ||||
|                 if (startStyles.length > 0) { | ||||
|                     const startStyle = startStyles[0]; | ||||
|                     newProps.style = startStyle; | ||||
|                     // console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
 | ||||
|                 } | ||||
| 
 | ||||
|                 newProps.ref = ((n) => self._collectNode( | ||||
|                 newProps.ref = ((n) => this._collectNode( | ||||
|                     c.key, n, restingStyle, | ||||
|                 )); | ||||
| 
 | ||||
|                 self.children[c.key] = React.cloneElement(c, newProps); | ||||
|                 this.children[c.key] = React.cloneElement(c, newProps); | ||||
|             } | ||||
|         }); | ||||
|     }, | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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) { | ||||
|     _collectNode(k, node, restingStyle) { | ||||
|         if ( | ||||
|             node && | ||||
|             this.nodes[k] === undefined && | ||||
|  | @ -125,12 +111,12 @@ module.exports = createReactClass({ | |||
| 
 | ||||
|             // and then we animate to the resting state
 | ||||
|             Velocity(domNode, restingStyle, | ||||
|                      transitionOpts[i-1]) | ||||
|             .then(() => { | ||||
|                 // once we've reached the resting state, hide the element if
 | ||||
|                 // appropriate
 | ||||
|                 domNode.style.visibility = restingStyle.visibility; | ||||
|             }); | ||||
|                 transitionOpts[i-1]) | ||||
|                 .then(() => { | ||||
|                     // once we've reached the resting state, hide the element if
 | ||||
|                     // appropriate
 | ||||
|                     domNode.style.visibility = restingStyle.visibility; | ||||
|                 }); | ||||
| 
 | ||||
|             /* | ||||
|             console.log("enter:", | ||||
|  | @ -153,13 +139,13 @@ module.exports = createReactClass({ | |||
|             if (domNode) Velocity.Utilities.removeData(domNode); | ||||
|         } | ||||
|         this.nodes[k] = node; | ||||
|     }, | ||||
|     } | ||||
| 
 | ||||
|     render: function() { | ||||
|     render() { | ||||
|         return ( | ||||
|             <span> | ||||
|                 { Object.values(this.children) } | ||||
|             </span> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -401,6 +401,11 @@ const LoggedInView = createReactClass({ | |||
|             const isClickShortcut = ev.target !== document.body && | ||||
|                 (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
 | ||||
|             // If using Slate, consume the Backspace without first focusing as it causes an implosion
 | ||||
|             if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) { | ||||
|  |  | |||
|  | @ -693,6 +693,10 @@ export default class MessagePanel extends React.Component { | |||
| 
 | ||||
|         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( | ||||
|             <li key={eventId} | ||||
|                 ref={this._collectEventNode.bind(this, eventId)} | ||||
|  | @ -707,7 +711,7 @@ export default class MessagePanel extends React.Component { | |||
|                     readReceipts={readReceipts} | ||||
|                     readReceiptMap={this._readReceiptMap} | ||||
|                     showUrlPreview={this.props.showUrlPreview} | ||||
|                     checkUnmounting={this._isUnmounting} | ||||
|                     checkUnmounting={this._isUnmounting.bind(this)} | ||||
|                     eventSendStatus={mxEv.getAssociatedStatus()} | ||||
|                     tileShape={this.props.tileShape} | ||||
|                     isTwelveHour={this.props.isTwelveHour} | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ EMOJIBASE.forEach(emoji => { | |||
|         DATA_BY_CATEGORY[categoryId].push(emoji); | ||||
|     } | ||||
|     // 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; | ||||
|  | @ -201,6 +201,7 @@ class EmojiPicker extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     onChangeFilter(filter) { | ||||
|         filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
 | ||||
|         for (const cat of this.categories) { | ||||
|             let emojis; | ||||
|             // 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 = { | ||||
|             language: languageHandler.getCurrentLanguage(), | ||||
|             theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), | ||||
|             useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), | ||||
|             haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), | ||||
|             serverSupportsSeparateAddAndBind: null, | ||||
|             idServerHasUnsignedTerms: false, | ||||
|  | @ -62,6 +60,7 @@ export default class GeneralUserSettingsTab extends React.Component { | |||
|             }, | ||||
|             emails: [], | ||||
|             msisdns: [], | ||||
|             ...this._calculateThemeState(), | ||||
|         }; | ||||
| 
 | ||||
|         this.dispatcherRef = dis.register(this._onAction); | ||||
|  | @ -80,6 +79,39 @@ export default class GeneralUserSettingsTab extends React.Component { | |||
|         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) => { | ||||
|         if (payload.action === 'id_server_changed') { | ||||
|             this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); | ||||
|  | @ -89,11 +121,11 @@ export default class GeneralUserSettingsTab extends React.Component { | |||
| 
 | ||||
|     _onEmailsChange = (emails) => { | ||||
|         this.setState({ emails }); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     _onMsisdnsChange = (msisdns) => { | ||||
|         this.setState({ msisdns }); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     async _getThreepidState() { | ||||
|         const cli = MatrixClientPeg.get(); | ||||
|  | @ -193,9 +225,9 @@ export default class GeneralUserSettingsTab extends React.Component { | |||
| 
 | ||||
|     _onUseSystemThemeChanged = (checked) => { | ||||
|         this.setState({useSystemTheme: checked}); | ||||
|         SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); | ||||
|         dis.dispatch({action: 'recheck_theme'}); | ||||
|     } | ||||
| 
 | ||||
|     }; | ||||
| 
 | ||||
|     _onPasswordChangeError = (err) => { | ||||
|         // 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() { | ||||
|         const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); | ||||
|         const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); | ||||
| 
 | ||||
|         const themeWatcher = new ThemeWatcher(); | ||||
|         let systemThemeSection; | ||||
|         if (themeWatcher.isSystemThemeSupported()) { | ||||
|             systemThemeSection = <div> | ||||
|                 <SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE} | ||||
|                 <LabelledToggleSwitch | ||||
|                     value={this.state.useSystemTheme} | ||||
|                     label={SettingsStore.getDisplayName("use_system_theme")} | ||||
|                     onChange={this._onUseSystemThemeChanged} | ||||
|                 /> | ||||
|             </div>; | ||||
|  |  | |||
|  | @ -49,6 +49,17 @@ export default class LabsUserSettingsTab extends React.Component { | |||
|         return ( | ||||
|             <div className="mx_SettingsTab"> | ||||
|                 <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"> | ||||
|                     {flags} | ||||
|                     <SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} /> | ||||
|  |  | |||
|  | @ -645,6 +645,7 @@ | |||
|     "Access Token:": "Access Token:", | ||||
|     "click to reveal": "click to reveal", | ||||
|     "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", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ import {DEFAULT_THEME, enumerateThemes} from "../../theme"; | |||
| 
 | ||||
| export default class ThemeController extends SettingController { | ||||
|     getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { | ||||
|         if (!calculatedValue) return null; // Don't override null themes
 | ||||
| 
 | ||||
|         const themes = enumerateThemes(); | ||||
|         // Override in case some no longer supported theme is stored here
 | ||||
|         if (!themes[calculatedValue]) { | ||||
|  |  | |||
|  | @ -80,6 +80,8 @@ export class ThemeWatcher { | |||
|     } | ||||
| 
 | ||||
|     getEffectiveTheme() { | ||||
|         // Dev note: Much of this logic is replicated in the GeneralUserSettingsTab
 | ||||
| 
 | ||||
|         // 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
 | ||||
|         // to ensure the ordering otherwise works.
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski