Merge branch 'develop' into dbkr/deactivate_account
						commit
						498ad7fa4c
					
				|  | @ -104,19 +104,25 @@ class ContentMessages { | |||
|         var def = q.defer(); | ||||
|         if (file.type.indexOf('image/') == 0) { | ||||
|             content.msgtype = 'm.image'; | ||||
|             infoForImageFile(file).then(function (imageInfo) { | ||||
|             infoForImageFile(file).then(imageInfo=>{ | ||||
|                 extend(content.info, imageInfo); | ||||
|                 def.resolve(); | ||||
|             }, error=>{ | ||||
|                 content.msgtype = 'm.file'; | ||||
|                 def.resolve(); | ||||
|             }); | ||||
|         } else if (file.type.indexOf('audio/') == 0) { | ||||
|             content.msgtype = 'm.audio'; | ||||
|             def.resolve(); | ||||
|         } else if (file.type.indexOf('video/') == 0) { | ||||
|           content.msgtype = 'm.video'; | ||||
|           infoForVideoFile(file).then(function (videoInfo) { | ||||
|               extend(content.info, videoInfo); | ||||
|               def.resolve(); | ||||
|           }); | ||||
|             content.msgtype = 'm.video'; | ||||
|             infoForVideoFile(file).then(videoInfo=>{ | ||||
|                 extend(content.info, videoInfo); | ||||
|                 def.resolve(); | ||||
|             }, error=>{ | ||||
|                 content.msgtype = 'm.file'; | ||||
|                 def.resolve(); | ||||
|             }); | ||||
|         } else { | ||||
|             content.msgtype = 'm.file'; | ||||
|             def.resolve(); | ||||
|  |  | |||
							
								
								
									
										112
									
								
								src/RichText.js
								
								
								
								
							
							
						
						
									
										112
									
								
								src/RichText.js
								
								
								
								
							|  | @ -1,6 +1,7 @@ | |||
| import React from 'react'; | ||||
| import { | ||||
|     Editor, | ||||
|     EditorState, | ||||
|     Modifier, | ||||
|     ContentState, | ||||
|     ContentBlock, | ||||
|  | @ -9,12 +10,13 @@ import { | |||
|     DefaultDraftInlineStyle, | ||||
|     CompositeDecorator, | ||||
|     SelectionState, | ||||
|     Entity, | ||||
| } from 'draft-js'; | ||||
| import * as sdk from  './index'; | ||||
| import * as emojione from 'emojione'; | ||||
| 
 | ||||
| const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', { | ||||
|     element: 'span' | ||||
|     element: 'span', | ||||
|     /* | ||||
|      draft uses <div> by default which we don't really like, so we're using <span> | ||||
|      this is probably not a good idea since <span> is not a block level element but | ||||
|  | @ -65,7 +67,7 @@ export function contentStateToHTML(contentState: ContentState): string { | |||
|         let result = `<${elem}>${content.join('')}</${elem}>`; | ||||
| 
 | ||||
|         // dirty hack because we don't want block level tags by default, but breaks
 | ||||
|         if(elem === 'span') | ||||
|         if (elem === 'span') | ||||
|             result += '<br />'; | ||||
|         return result; | ||||
|     }).join(''); | ||||
|  | @ -75,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState { | |||
|     return ContentState.createFromBlockArray(convertFromHTML(html)); | ||||
| } | ||||
| 
 | ||||
| function unicodeToEmojiUri(str) { | ||||
|     let replaceWith, unicode, alt; | ||||
|     if ((!emojione.unicodeAlt) || (emojione.sprites)) { | ||||
|         // if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
 | ||||
|         let mappedUnicode = emojione.mapUnicodeToShort(); | ||||
|     } | ||||
| 
 | ||||
|     str = str.replace(emojione.regUnicode, function(unicodeChar) { | ||||
|         if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { | ||||
|             // if the unicodeChar doesnt exist just return the entire match
 | ||||
|             return unicodeChar; | ||||
|         } else { | ||||
|             // get the unicode codepoint from the actual char
 | ||||
|             unicode = emojione.jsEscapeMap[unicodeChar]; | ||||
|             return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     return str; | ||||
| } | ||||
| 
 | ||||
| // Workaround for https://github.com/facebook/draft-js/issues/414
 | ||||
| let emojiDecorator = { | ||||
|     strategy: (contentBlock, callback) => { | ||||
|         findWithRegex(EMOJI_REGEX, contentBlock, callback); | ||||
|     }, | ||||
|     component: (props) => { | ||||
|         let uri = unicodeToEmojiUri(props.children[0].props.text); | ||||
|         let shortname = emojione.toShort(props.children[0].props.text); | ||||
|         let style = { | ||||
|             display: 'inline-block', | ||||
|             width: '1em', | ||||
|             maxHeight: '1em', | ||||
|             background: `url(${uri})`, | ||||
|             backgroundSize: 'contain', | ||||
|             backgroundPosition: 'center center', | ||||
|             overflow: 'hidden', | ||||
|         }; | ||||
|         return (<span title={shortname} style={style}><span style={{opacity: 0}}>{props.children}</span></span>); | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Returns a composite decorator which has access to provided scope. | ||||
|  */ | ||||
|  | @ -90,7 +134,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { | |||
|             // unused until we make these decorators immutable (autocomplete needed)
 | ||||
|             let name = member ? member.name : null; | ||||
|             let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null; | ||||
|             return <span className="mx_UserPill">{avatar} {props.children}</span>; | ||||
|             return <span className="mx_UserPill">{avatar}{props.children}</span>; | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|  | @ -103,17 +147,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     // Unused for now, due to https://github.com/facebook/draft-js/issues/414
 | ||||
|     let emojiDecorator = { | ||||
|         strategy: (contentBlock, callback) => { | ||||
|             findWithRegex(EMOJI_REGEX, contentBlock, callback); | ||||
|         }, | ||||
|         component: (props) => { | ||||
|             return <span dangerouslySetInnerHTML={{__html: ' ' + emojione.unicodeToImage(props.children[0].props.text)}}/> | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return [usernameDecorator, roomDecorator]; | ||||
|     return [usernameDecorator, roomDecorator, emojiDecorator]; | ||||
| } | ||||
| 
 | ||||
| export function getScopedMDDecorators(scope: any): CompositeDecorator { | ||||
|  | @ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { | |||
|             </a> | ||||
|         ) | ||||
|     }); | ||||
|     markdownDecorators.push(emojiDecorator); | ||||
| 
 | ||||
|     return markdownDecorators; | ||||
| } | ||||
|  | @ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection | |||
| export function selectionStateToTextOffsets(selectionState: SelectionState, | ||||
|                                             contentBlocks: Array<ContentBlock>): {start: number, end: number} { | ||||
|     let offset = 0, start = 0, end = 0; | ||||
|     for(let block of contentBlocks) { | ||||
|     for (let block of contentBlocks) { | ||||
|         if (selectionState.getStartKey() === block.getKey()) { | ||||
|             start = offset + selectionState.getStartOffset(); | ||||
|         } | ||||
|  | @ -240,3 +275,50 @@ export function textOffsetsToSelectionState({start, end}: {start: number, end: n | |||
| 
 | ||||
|     return selectionState; | ||||
| } | ||||
| 
 | ||||
| // modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
 | ||||
| export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState { | ||||
|     const contentState = editorState.getCurrentContent(); | ||||
|     const blocks = contentState.getBlockMap(); | ||||
|     let newContentState = contentState; | ||||
| 
 | ||||
|     blocks.forEach((block) => { | ||||
|         const plainText = block.getText(); | ||||
| 
 | ||||
|         const addEntityToEmoji = (start, end) => { | ||||
|             const existingEntityKey = block.getEntityAt(start); | ||||
|             if (existingEntityKey) { | ||||
|                 // avoid manipulation in case the emoji already has an entity
 | ||||
|                 const entity = Entity.get(existingEntityKey); | ||||
|                 if (entity && entity.get('type') === 'emoji') { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const selection = SelectionState.createEmpty(block.getKey()) | ||||
|                 .set('anchorOffset', start) | ||||
|                 .set('focusOffset', end); | ||||
|             const emojiText = plainText.substring(start, end); | ||||
|             const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText }); | ||||
|             newContentState = Modifier.replaceText( | ||||
|                 newContentState, | ||||
|                 selection, | ||||
|                 emojiText, | ||||
|                 null, | ||||
|                 entityKey, | ||||
|             ); | ||||
|         }; | ||||
| 
 | ||||
|         findWithRegex(EMOJI_REGEX, block, addEntityToEmoji); | ||||
|     }); | ||||
| 
 | ||||
|     if (!newContentState.equals(contentState)) { | ||||
|         return EditorState.push( | ||||
|             editorState, | ||||
|             newContentState, | ||||
|             'convert-to-immutable-emojis', | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return editorState; | ||||
| } | ||||
|  |  | |||
|  | @ -11,11 +11,11 @@ let instance = null; | |||
| export default class UserProvider extends AutocompleteProvider { | ||||
|     constructor() { | ||||
|         super(USER_REGEX, { | ||||
|             keys: ['displayName', 'userId'], | ||||
|             keys: ['name', 'userId'], | ||||
|         }); | ||||
|         this.users = []; | ||||
|         this.fuse = new Fuse([], { | ||||
|             keys: ['displayName', 'userId'], | ||||
|             keys: ['name', 'userId'], | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -25,11 +25,12 @@ export default class UserProvider extends AutocompleteProvider { | |||
|         if (command) { | ||||
|             this.fuse.set(this.users); | ||||
|             completions = this.fuse.search(command[0]).map(user => { | ||||
|                 const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
 | ||||
|                 return { | ||||
|                     completion: user.userId, | ||||
|                     component: ( | ||||
|                         <TextualCompletion | ||||
|                             title={user.displayName || user.userId} | ||||
|                             title={displayName} | ||||
|                             description={user.userId} /> | ||||
|                     ), | ||||
|                     range | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ limitations under the License. | |||
|  */ | ||||
| 
 | ||||
| module.exports.components = {}; | ||||
| module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu'); | ||||
| module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); | ||||
| module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); | ||||
| module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var classNames = require('classnames'); | ||||
| var React = require('react'); | ||||
| var ReactDOM = require('react-dom'); | ||||
| 
 | ||||
|  | @ -27,6 +28,12 @@ var ReactDOM = require('react-dom'); | |||
| module.exports = { | ||||
|     ContextualMenuContainerId: "mx_ContextualMenu_Container", | ||||
| 
 | ||||
|      propTypes: { | ||||
|         menuWidth: React.PropTypes.number, | ||||
|         menuHeight: React.PropTypes.number, | ||||
|         chevronOffset: React.PropTypes.number, | ||||
|     }, | ||||
| 
 | ||||
|     getOrCreateContainer: function() { | ||||
|         var container = document.getElementById(this.ContextualMenuContainerId); | ||||
| 
 | ||||
|  | @ -45,29 +52,50 @@ module.exports = { | |||
|         var closeMenu = function() { | ||||
|             ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); | ||||
| 
 | ||||
|             if (props && props.onFinished) props.onFinished.apply(null, arguments); | ||||
|             if (props && props.onFinished) { | ||||
|                 props.onFinished.apply(null, arguments); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         var position = { | ||||
|             top: props.top - 20, | ||||
|             top: props.top, | ||||
|         }; | ||||
| 
 | ||||
|         var chevronOffset = { | ||||
|             top: props.chevronOffset, | ||||
|         } | ||||
| 
 | ||||
|         var chevron = null; | ||||
|         if (props.left) { | ||||
|             chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" /> | ||||
|             position.left = props.left + 8; | ||||
|             chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div> | ||||
|             position.left = props.left; | ||||
|         } else { | ||||
|             chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" /> | ||||
|             position.right = props.right + 8; | ||||
|             chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div> | ||||
|             position.right = props.right; | ||||
|         } | ||||
| 
 | ||||
|         var className = 'mx_ContextualMenu_wrapper'; | ||||
| 
 | ||||
|         var menuClasses = classNames({ | ||||
|             'mx_ContextualMenu': true, | ||||
|             'mx_ContextualMenu_left': props.left, | ||||
|             'mx_ContextualMenu_right': !props.left, | ||||
|         }); | ||||
| 
 | ||||
|         var menuSize = {}; | ||||
|         if (props.menuWidth) { | ||||
|             menuSize.width = props.menuWidth; | ||||
|         } | ||||
| 
 | ||||
|         if (props.menuHeight) { | ||||
|             menuSize.height = props.menuHeight; | ||||
|         } | ||||
| 
 | ||||
|         // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
 | ||||
|         // property set here so you can't close the menu from a button click!
 | ||||
|         var menu = ( | ||||
|             <div className={className}> | ||||
|                 <div className="mx_ContextualMenu" style={position}> | ||||
|             <div className={className} style={position}> | ||||
|                 <div className={menuClasses} style={menuSize}> | ||||
|                     {chevron} | ||||
|                     <Element {...props} onFinished={closeMenu}/> | ||||
|                 </div> | ||||
|  | @ -20,7 +20,7 @@ var Favico = require('favico.js'); | |||
| 
 | ||||
| var MatrixClientPeg = require("../../MatrixClientPeg"); | ||||
| var Notifier = require("../../Notifier"); | ||||
| var ContextualMenu = require("../../ContextualMenu"); | ||||
| var ContextualMenu = require("./ContextualMenu"); | ||||
| var RoomListSorter = require("../../RoomListSorter"); | ||||
| var UserActivity = require("../../UserActivity"); | ||||
| var Presence = require("../../Presence"); | ||||
|  |  | |||
|  | @ -64,6 +64,9 @@ export default class Autocomplete extends React.Component { | |||
|     onUpArrow(): boolean { | ||||
|         let completionCount = this.countCompletions(), | ||||
|             selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; | ||||
|         if (!completionCount) { | ||||
|             return false; | ||||
|         } | ||||
|         this.setSelection(selectionOffset); | ||||
|         return true; | ||||
|     } | ||||
|  | @ -72,6 +75,9 @@ export default class Autocomplete extends React.Component { | |||
|     onDownArrow(): boolean { | ||||
|         let completionCount = this.countCompletions(), | ||||
|             selectionOffset = (this.state.selectionOffset + 1) % completionCount; | ||||
|         if (!completionCount) { | ||||
|             return false; | ||||
|         } | ||||
|         this.setSelection(selectionOffset); | ||||
|         return true; | ||||
|     } | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ var sdk = require('../../../index'); | |||
| var MatrixClientPeg = require('../../../MatrixClientPeg') | ||||
| var TextForEvent = require('../../../TextForEvent'); | ||||
| 
 | ||||
| var ContextualMenu = require('../../../ContextualMenu'); | ||||
| var ContextualMenu = require('../../structures/ContextualMenu'); | ||||
| var dispatcher = require("../../../dispatcher"); | ||||
| 
 | ||||
| var ObjectUtils = require('../../../ObjectUtils'); | ||||
|  | @ -249,12 +249,15 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     onEditClicked: function(e) { | ||||
|         var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); | ||||
|         var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); | ||||
|         var buttonRect = e.target.getBoundingClientRect() | ||||
|         var x = buttonRect.right; | ||||
|         var y = buttonRect.top + (e.target.height / 2); | ||||
| 
 | ||||
|         // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|         var x = buttonRect.right + window.pageXOffset; | ||||
|         var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19; | ||||
|         var self = this; | ||||
|         ContextualMenu.createMenu(MessageContextMenu, { | ||||
|             chevronOffset: 10, | ||||
|             mxEvent: this.props.mxEvent, | ||||
|             left: x, | ||||
|             top: y, | ||||
|  |  | |||
|  | @ -97,9 +97,11 @@ module.exports = React.createClass({ | |||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         var deviceName = this.props.device.display_name || this.props.device.id; | ||||
| 
 | ||||
|         return ( | ||||
|             <div className="mx_MemberDeviceInfo"> | ||||
|                 <div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div> | ||||
|                 <div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div> | ||||
|                 {indicator} | ||||
|                 {verifyButton} | ||||
|                 {blockButton} | ||||
|  |  | |||
|  | @ -36,7 +36,6 @@ export default class MessageComposer extends React.Component { | |||
|         this.onInputContentChanged = this.onInputContentChanged.bind(this); | ||||
|         this.onUpArrow = this.onUpArrow.bind(this); | ||||
|         this.onDownArrow = this.onDownArrow.bind(this); | ||||
|         this.onTab = this.onTab.bind(this); | ||||
|         this._tryComplete = this._tryComplete.bind(this); | ||||
|         this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); | ||||
| 
 | ||||
|  | @ -143,12 +142,6 @@ export default class MessageComposer extends React.Component { | |||
|         return this.refs.autocomplete.onDownArrow(); | ||||
|     } | ||||
| 
 | ||||
|     onTab() { | ||||
|         // FIXME Autocomplete doesn't have an onTab - what is this supposed to do?
 | ||||
|         // return this.refs.autocomplete.onTab();
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     _tryComplete(): boolean { | ||||
|         if (this.refs.autocomplete) { | ||||
|             return this.refs.autocomplete.onConfirm(); | ||||
|  | @ -223,7 +216,6 @@ export default class MessageComposer extends React.Component { | |||
|                     tryComplete={this._tryComplete} | ||||
|                     onUpArrow={this.onUpArrow} | ||||
|                     onDownArrow={this.onDownArrow} | ||||
|                     onTab={this.onTab} | ||||
|                     tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
 | ||||
|                     onContentChanged={this.onInputContentChanged} />, | ||||
|                 uploadButton, | ||||
|  |  | |||
|  | @ -14,8 +14,8 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| import React from 'react'; | ||||
| 
 | ||||
| var marked = require("marked"); | ||||
| import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; | ||||
| import marked from 'marked'; | ||||
| marked.setOptions({ | ||||
|     renderer: new marked.Renderer(), | ||||
|     gfm: true, | ||||
|  | @ -24,7 +24,7 @@ marked.setOptions({ | |||
|     pedantic: false, | ||||
|     sanitize: true, | ||||
|     smartLists: true, | ||||
|     smartypants: false | ||||
|     smartypants: false, | ||||
| }); | ||||
| 
 | ||||
| import {Editor, EditorState, RichUtils, CompositeDecorator, | ||||
|  | @ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator, | |||
| 
 | ||||
| import {stateToMarkdown} from 'draft-js-export-markdown'; | ||||
| 
 | ||||
| var MatrixClientPeg = require("../../../MatrixClientPeg"); | ||||
| var SlashCommands = require("../../../SlashCommands"); | ||||
| var Modal = require("../../../Modal"); | ||||
| var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; | ||||
| var sdk = require('../../../index'); | ||||
| import MatrixClientPeg from '../../../MatrixClientPeg'; | ||||
| import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; | ||||
| import SlashCommands from '../../../SlashCommands'; | ||||
| import Modal from '../../../Modal'; | ||||
| import sdk from '../../../index'; | ||||
| 
 | ||||
| var dis = require("../../../dispatcher"); | ||||
| var KeyCode = require("../../../KeyCode"); | ||||
| import dis from '../../../dispatcher'; | ||||
| import KeyCode from '../../../KeyCode'; | ||||
| 
 | ||||
| import * as RichText from '../../../RichText'; | ||||
| 
 | ||||
|  | @ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; | |||
| const KEY_M = 77; | ||||
| 
 | ||||
| // FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
 | ||||
| function mdownToHtml(mdown) { | ||||
|     var html = marked(mdown) || ""; | ||||
| function mdownToHtml(mdown: string): string { | ||||
|     let html = marked(mdown) || ""; | ||||
|     html = html.trim(); | ||||
|     // strip start and end <p> tags else you get 'orrible spacing
 | ||||
|     if (html.indexOf("<p>") === 0) { | ||||
|  | @ -66,6 +66,17 @@ function mdownToHtml(mdown) { | |||
|  * The textInput part of the MessageComposer | ||||
|  */ | ||||
| export default class MessageComposerInput extends React.Component { | ||||
|     static getKeyBinding(e: SyntheticKeyboardEvent): string { | ||||
|         // C-m => Toggles between rich text and markdown modes
 | ||||
|         if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { | ||||
|             return 'toggle-mode'; | ||||
|         } | ||||
| 
 | ||||
|         return getDefaultKeyBinding(e); | ||||
|     } | ||||
| 
 | ||||
|     client: MatrixClient; | ||||
| 
 | ||||
|     constructor(props, context) { | ||||
|         super(props, context); | ||||
|         this.onAction = this.onAction.bind(this); | ||||
|  | @ -79,7 +90,7 @@ export default class MessageComposerInput extends React.Component { | |||
|         this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); | ||||
| 
 | ||||
|         let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); | ||||
|         if(isRichtextEnabled == null) { | ||||
|         if (isRichtextEnabled == null) { | ||||
|             isRichtextEnabled = 'true'; | ||||
|         } | ||||
|         isRichtextEnabled = isRichtextEnabled === 'true'; | ||||
|  | @ -95,15 +106,6 @@ export default class MessageComposerInput extends React.Component { | |||
|         this.client = MatrixClientPeg.get(); | ||||
|     } | ||||
| 
 | ||||
|     static getKeyBinding(e: SyntheticKeyboardEvent): string { | ||||
|         // C-m => Toggles between rich text and markdown modes
 | ||||
|         if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { | ||||
|             return 'toggle-mode'; | ||||
|         } | ||||
| 
 | ||||
|         return getDefaultKeyBinding(e); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * "Does the right thing" to create an EditorState, based on: | ||||
|      * - whether we've got rich text mode enabled | ||||
|  | @ -347,15 +349,16 @@ export default class MessageComposerInput extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     setEditorState(editorState: EditorState) { | ||||
|         editorState = RichText.attachImmutableEntitiesToEmoji(editorState); | ||||
|         this.setState({editorState}); | ||||
| 
 | ||||
|         if(editorState.getCurrentContent().hasText()) { | ||||
|             this.onTypingActivity() | ||||
|         if (editorState.getCurrentContent().hasText()) { | ||||
|             this.onTypingActivity(); | ||||
|         } else { | ||||
|             this.onFinishedTyping(); | ||||
|         } | ||||
| 
 | ||||
|         if(this.props.onContentChanged) { | ||||
|         if (this.props.onContentChanged) { | ||||
|             this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), | ||||
|                 RichText.selectionStateToTextOffsets(editorState.getSelection(), | ||||
|                     editorState.getCurrentContent().getBlocksAsArray())); | ||||
|  | @ -380,7 +383,7 @@ export default class MessageComposerInput extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     handleKeyCommand(command: string): boolean { | ||||
|         if(command === 'toggle-mode') { | ||||
|         if (command === 'toggle-mode') { | ||||
|             this.enableRichtext(!this.state.isRichtextEnabled); | ||||
|             return true; | ||||
|         } | ||||
|  | @ -388,7 +391,7 @@ export default class MessageComposerInput extends React.Component { | |||
|         let newState: ?EditorState = null; | ||||
| 
 | ||||
|         // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
 | ||||
|         if(!this.state.isRichtextEnabled) { | ||||
|         if (!this.state.isRichtextEnabled) { | ||||
|             let contentState = this.state.editorState.getCurrentContent(), | ||||
|                 selection = this.state.editorState.getSelection(); | ||||
| 
 | ||||
|  | @ -396,10 +399,10 @@ export default class MessageComposerInput extends React.Component { | |||
|                 bold: text => `**${text}**`, | ||||
|                 italic: text => `*${text}*`, | ||||
|                 underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
 | ||||
|                 code: text => `\`${text}\`` | ||||
|                 code: text => `\`${text}\``, | ||||
|             }[command]; | ||||
| 
 | ||||
|             if(modifyFn) { | ||||
|             if (modifyFn) { | ||||
|                 newState = EditorState.push( | ||||
|                     this.state.editorState, | ||||
|                     RichText.modifyText(contentState, selection, modifyFn), | ||||
|  | @ -408,7 +411,7 @@ export default class MessageComposerInput extends React.Component { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if(newState == null) | ||||
|         if (newState == null) | ||||
|             newState = RichUtils.handleKeyCommand(this.state.editorState, command); | ||||
| 
 | ||||
|         if (newState != null) { | ||||
|  | @ -422,12 +425,6 @@ export default class MessageComposerInput extends React.Component { | |||
|         if (ev.shiftKey) { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         if(this.props.tryComplete) { | ||||
|             if(this.props.tryComplete()) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const contentState = this.state.editorState.getCurrentContent(); | ||||
|         if (!contentState.hasText()) { | ||||
|  | @ -503,24 +500,20 @@ export default class MessageComposerInput extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     onUpArrow(e) { | ||||
|         if(this.props.onUpArrow) { | ||||
|             if(this.props.onUpArrow()) { | ||||
|                 e.preventDefault(); | ||||
|             } | ||||
|         if (this.props.onUpArrow && this.props.onUpArrow()) { | ||||
|             e.preventDefault(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onDownArrow(e) { | ||||
|         if(this.props.onDownArrow) { | ||||
|             if(this.props.onDownArrow()) { | ||||
|                 e.preventDefault(); | ||||
|             } | ||||
|         if (this.props.onDownArrow && this.props.onDownArrow()) { | ||||
|             e.preventDefault(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onTab(e) { | ||||
|         if (this.props.onTab) { | ||||
|             if (this.props.onTab()) { | ||||
|         if (this.props.tryComplete) { | ||||
|             if (this.props.tryComplete()) { | ||||
|                 e.preventDefault(); | ||||
|             } | ||||
|         } | ||||
|  | @ -533,9 +526,11 @@ export default class MessageComposerInput extends React.Component { | |||
|             content | ||||
|         ); | ||||
| 
 | ||||
|         this.setState({ | ||||
|             editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), | ||||
|         }); | ||||
|         let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); | ||||
| 
 | ||||
|         editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter()); | ||||
| 
 | ||||
|         this.setEditorState(editorState); | ||||
| 
 | ||||
|         // for some reason, doing this right away does not update the editor :(
 | ||||
|         setTimeout(() => this.refs.editor.focus(), 50); | ||||
|  | @ -585,5 +580,6 @@ MessageComposerInput.propTypes = { | |||
| 
 | ||||
|     onDownArrow: React.PropTypes.func, | ||||
| 
 | ||||
|     onTab: React.PropTypes.func | ||||
|     // attempts to confirm currently selected completion, returns whether actually confirmed
 | ||||
|     tryComplete: React.PropTypes.func, | ||||
| }; | ||||
|  |  | |||
|  | @ -268,9 +268,11 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     _repositionTooltip: function(e) { | ||||
|         if (this.tooltip && this.tooltip.parentElement) { | ||||
|         // We access the parent of the parent, as the tooltip is inside a container
 | ||||
|         // Needs refactoring into a better multipurpose tooltip
 | ||||
|         if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) { | ||||
|             var scroll = ReactDOM.findDOMNode(this); | ||||
|             this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; | ||||
|             this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ var classNames = require('classnames'); | |||
| var dis = require("../../../dispatcher"); | ||||
| var MatrixClientPeg = require('../../../MatrixClientPeg'); | ||||
| var sdk = require('../../../index'); | ||||
| var ContextualMenu = require('../../structures/ContextualMenu'); | ||||
| import {emojifyText} from '../../../HtmlUtils'; | ||||
| 
 | ||||
| module.exports = React.createClass({ | ||||
|  | @ -43,16 +44,48 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         var areNotifsMuted = false; | ||||
|         var cli = MatrixClientPeg.get(); | ||||
|         if (!cli.isGuest()) { | ||||
|             var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId); | ||||
|             if (roomPushRule) { | ||||
|                 if (0 <= roomPushRule.actions.indexOf("dont_notify")) { | ||||
|                     areNotifsMuted = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return({ | ||||
|             hover : false, | ||||
|             badgeHover : false, | ||||
|             menu: false, | ||||
|             areNotifsMuted: areNotifsMuted, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onAction: function(payload) { | ||||
|         switch (payload.action) { | ||||
|             case 'notification_change': | ||||
|                 // Is the notification about this room?
 | ||||
|                 if (payload.roomId === this.props.room.roomId) { | ||||
|                     this.setState( { areNotifsMuted : payload.isMuted }); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount: function() { | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|         dis.unregister(this.dispatcherRef); | ||||
|     }, | ||||
| 
 | ||||
|     onClick: function() { | ||||
|         dis.dispatch({ | ||||
|             action: 'view_room', | ||||
|             room_id: this.props.room.roomId | ||||
|             room_id: this.props.room.roomId, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -65,13 +98,47 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     badgeOnMouseEnter: function() { | ||||
|         this.setState( { badgeHover : true } ); | ||||
|         // Only allow none guests to access the context menu
 | ||||
|         // and only change it if it needs to change
 | ||||
|         if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) { | ||||
|             this.setState( { badgeHover : true } ); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     badgeOnMouseLeave: function() { | ||||
|         this.setState( { badgeHover : false } ); | ||||
|     }, | ||||
| 
 | ||||
|     onBadgeClicked: function(e) { | ||||
|         // Only allow none guests to access the context menu
 | ||||
|         if (!MatrixClientPeg.get().isGuest()) { | ||||
| 
 | ||||
|             // If the badge is clicked, then no longer show tooltip
 | ||||
|             if (this.props.collapsed) { | ||||
|                 this.setState({ hover: false }); | ||||
|             } | ||||
| 
 | ||||
|             var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); | ||||
|             var elementRect = e.target.getBoundingClientRect(); | ||||
|             // The window X and Y offsets are to adjust position when zoomed in to page
 | ||||
|             var x = elementRect.right + window.pageXOffset + 3; | ||||
|             var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; | ||||
|             var self = this; | ||||
|             ContextualMenu.createMenu(Menu, { | ||||
|                 menuWidth: 188, | ||||
|                 menuHeight: 126, | ||||
|                 chevronOffset: 45, | ||||
|                 left: x, | ||||
|                 top: y, | ||||
|                 room: this.props.room, | ||||
|                 onFinished: function() { | ||||
|                     self.setState({ menu: false }); | ||||
|                 } | ||||
|             }); | ||||
|             this.setState({ menu: true }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var myUserId = MatrixClientPeg.get().credentials.userId; | ||||
|         var me = this.props.room.currentState.members[myUserId]; | ||||
|  | @ -84,60 +151,63 @@ module.exports = React.createClass({ | |||
|             'mx_RoomTile_selected': this.props.selected, | ||||
|             'mx_RoomTile_unread': this.props.unread, | ||||
|             'mx_RoomTile_unreadNotify': notificationCount > 0, | ||||
|             'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0), | ||||
|             'mx_RoomTile_highlight': this.props.highlight, | ||||
|             'mx_RoomTile_invited': (me && me.membership == 'invite'), | ||||
|             'mx_RoomTile_menu': this.state.menu, | ||||
|         }); | ||||
| 
 | ||||
|         var avatarClasses = classNames({ | ||||
|             'mx_RoomTile_avatar': true, | ||||
|             'mx_RoomTile_mute': this.state.areNotifsMuted, | ||||
|         }); | ||||
| 
 | ||||
|         var badgeClasses = classNames({ | ||||
|             'mx_RoomTile_badge': true, | ||||
|             'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu, | ||||
|             'mx_RoomTile_badgeMute': this.state.areNotifsMuted, | ||||
|         }); | ||||
| 
 | ||||
|         // XXX: We should never display raw room IDs, but sometimes the
 | ||||
|         // room name js sdk gives is undefined (cannot repro this -- k)
 | ||||
|         var name = this.props.room.name || this.props.room.roomId; | ||||
| 
 | ||||
|         name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
 | ||||
| 
 | ||||
|         var badge; | ||||
|         var badgeContent; | ||||
|         var badgeClasses; | ||||
| 
 | ||||
|         if (this.state.badgeHover) { | ||||
|         if (this.state.badgeHover || this.state.menu) { | ||||
|             badgeContent = "\u00B7\u00B7\u00B7"; | ||||
|         } else if (this.props.highlight || notificationCount > 0) { | ||||
|             badgeContent = notificationCount ? notificationCount : '!'; | ||||
|             var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; | ||||
|             badgeContent = notificationCount ? limitedCount : '!'; | ||||
|         } else { | ||||
|             badgeContent = '\u200B'; | ||||
|         } | ||||
| 
 | ||||
|         if (this.props.highlight || notificationCount > 0) { | ||||
|             badgeClasses = "mx_RoomTile_badge"; | ||||
|         if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { | ||||
|             badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>; | ||||
|         } else { | ||||
|             badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; | ||||
|             badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>; | ||||
|         } | ||||
| 
 | ||||
|         badge = <div className={ badgeClasses } onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>; | ||||
| 
 | ||||
|         /* | ||||
|         if (this.props.highlight) { | ||||
|             badge = <div className="mx_RoomTile_badge">!</div>; | ||||
|         } | ||||
|         else if (this.props.unread) { | ||||
|             badge = <div className="mx_RoomTile_badge">1</div>; | ||||
|         } | ||||
|         var nameCell; | ||||
|         if (badge) { | ||||
|             nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>; | ||||
|         } | ||||
|         else { | ||||
|             nameCell = <div className="mx_RoomTile_name">{name}</div>; | ||||
|         } | ||||
|         */ | ||||
| 
 | ||||
|         var label; | ||||
|         var tooltip; | ||||
|         if (!this.props.collapsed) { | ||||
|             var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); | ||||
|             var nameClasses = classNames({ | ||||
|                 'mx_RoomTile_name': true, | ||||
|                 'mx_RoomTile_invite': this.props.isInvite, | ||||
|                 'mx_RoomTile_mute': this.state.areNotifsMuted, | ||||
|                 'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted, | ||||
|             }); | ||||
| 
 | ||||
|             let nameHTML = emojifyText(name); | ||||
|             if (this.props.selected) { | ||||
|                 name = <span dangerouslySetInnerHTML={nameHTML}></span>; | ||||
|                 label = <div className={ className }>{ name }</div>; | ||||
|                 let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>; | ||||
| 
 | ||||
|                 label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>; | ||||
|             } else { | ||||
|                 label = <div className={ className } dangerouslySetInnerHTML={nameHTML}></div>; | ||||
|                 label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>; | ||||
|             } | ||||
|         } | ||||
|         else if (this.state.hover) { | ||||
|  | @ -160,13 +230,16 @@ module.exports = React.createClass({ | |||
|         var connectDropTarget = this.props.connectDropTarget; | ||||
| 
 | ||||
|         return connectDragSource(connectDropTarget( | ||||
|             <div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> | ||||
|                 <div className="mx_RoomTile_avatar"> | ||||
|                     <RoomAvatar room={this.props.room} width={24} height={24} /> | ||||
|             <div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> | ||||
|                 <div className={avatarClasses}> | ||||
|                     <RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} /> | ||||
|                 </div> | ||||
|                 <div className="mx_RoomTile_nameContainer"> | ||||
|                     { label } | ||||
|                     { badge } | ||||
|                 </div> | ||||
|                 { label } | ||||
|                 { badge } | ||||
|                 { incomingCallBox } | ||||
|                 { tooltip } | ||||
|             </div> | ||||
|         )); | ||||
|     } | ||||
|  |  | |||
|  | @ -111,7 +111,9 @@ export default class DevicesPanelEntry extends React.Component { | |||
|             <div className="mx_DevicesPanel_device"> | ||||
|                 <div className="mx_DevicesPanel_deviceName"> | ||||
|                     <EditableTextContainer initialValue={device.display_name} | ||||
|                         onSubmit={this._onDisplayNameChanged} /> | ||||
|                         onSubmit={this._onDisplayNameChanged} | ||||
|                         placeholder={device.device_id} | ||||
|                     /> | ||||
|                 </div> | ||||
|                 <div className="mx_DevicesPanel_lastSeen"> | ||||
|                     {lastSeen} | ||||
|  |  | |||
|  | @ -224,7 +224,7 @@ describe('TimelinePanel', function() { | |||
| 
 | ||||
|         var scrollDefer; | ||||
|         var panel = ReactDOM.render( | ||||
|             <TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />, | ||||
|             <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} />, | ||||
|             parentDiv | ||||
|         ); | ||||
|         console.log("TimelinePanel rendered"); | ||||
|  | @ -238,17 +238,29 @@ describe('TimelinePanel', function() { | |||
|         // the TimelinePanel fires a scroll event
 | ||||
|         var awaitScroll = function() { | ||||
|             scrollDefer = q.defer(); | ||||
|             return scrollDefer.promise; | ||||
|             return scrollDefer.promise.then(() => { | ||||
|                 console.log("got scroll event; scrollTop now " + | ||||
|                             scrollingDiv.scrollTop); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         function setScrollTop(scrollTop) { | ||||
|             const before = scrollingDiv.scrollTop; | ||||
|             scrollingDiv.scrollTop = scrollTop; | ||||
|             console.log("setScrollTop: before update: " + before + | ||||
|                         "; assigned: " + scrollTop + | ||||
|                         "; after update: " + scrollingDiv.scrollTop); | ||||
|         } | ||||
| 
 | ||||
|         function backPaginate() { | ||||
|             scrollingDiv.scrollTop = 0; | ||||
|             console.log("back paginating..."); | ||||
|             setScrollTop(0); | ||||
|             return awaitScroll().then(() => { | ||||
|                 if(scrollingDiv.scrollTop > 0) { | ||||
|                     // need to go further
 | ||||
|                     return backPaginate(); | ||||
|                 } | ||||
|                 console.log("paginated to end."); | ||||
|                 console.log("paginated to start."); | ||||
| 
 | ||||
|                 // hopefully, we got to the start of the timeline
 | ||||
|                 expect(messagePanel.props.backPaginating).toBe(false); | ||||
|  | @ -262,7 +274,6 @@ describe('TimelinePanel', function() { | |||
|             expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); | ||||
| 
 | ||||
|             // back-paginate until we hit the start
 | ||||
|             console.log("back paginating..."); | ||||
|             return backPaginate(); | ||||
|         }).then(() => { | ||||
|             expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); | ||||
|  | @ -271,8 +282,7 @@ describe('TimelinePanel', function() { | |||
| 
 | ||||
|             // we should now be able to scroll down, and paginate in the other
 | ||||
|             // direction.
 | ||||
|             console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop); | ||||
|             console.log("Going to set it to " + scrollingDiv.scrollHeight); | ||||
|             setScrollTop(scrollingDiv.scrollHeight); | ||||
|             scrollingDiv.scrollTop = scrollingDiv.scrollHeight; | ||||
|             return awaitScroll(); | ||||
|         }).then(() => { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker