From b33452216875ba3ffd09747158bd5e1a12512e96 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 8 Jul 2016 12:54:28 +0530 Subject: [PATCH 01/47] feat: code cleanup & emoji replacement in composer --- src/RichText.js | 112 +++++++++++++++--- .../views/rooms/MessageComposerInput.js | 71 +++++------ 2 files changed, 135 insertions(+), 48 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index c24a510e05..a5bc554b95 100644 --- a/src/RichText.js +++ b/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
by default which we don't really like, so we're using this is probably not a good idea since is not a block level element but @@ -65,7 +67,7 @@ export function contentStateToHTML(contentState: ContentState): string { let result = `<${elem}>${content.join('')}`; // dirty hack because we don't want block level tags by default, but breaks - if(elem === 'span') + if (elem === 'span') result += '
'; 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; +} + +// 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) => { + 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 ({props.children}); + }, +}; + /** * 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 ? : null; - return {avatar} {props.children}; + return {avatar}{props.children}; } }; @@ -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 - } - }; - - return [usernameDecorator, roomDecorator]; + return [usernameDecorator, roomDecorator, emojiDecorator]; } export function getScopedMDDecorators(scope: any): CompositeDecorator { @@ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { ) }); + markdownDecorators.push(emojiDecorator); return markdownDecorators; } @@ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection export function selectionStateToTextOffsets(selectionState: SelectionState, contentBlocks: Array): {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; +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..fea4e8fea0 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -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

-function mdownToHtml(mdown) { - var html = marked(mdown) || ""; +function mdownToHtml(mdown: string): string { + let html = marked(mdown) || ""; html = html.trim(); // strip start and end

tags else you get 'orrible spacing if (html.indexOf("

") === 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) { @@ -533,9 +536,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); From 775fc971020772c2ee80e0b291b2d1e00b88840a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 14:33:54 +0100 Subject: [PATCH 02/47] Slight refactor to better match current code --- src/components/views/rooms/RoomTile.js | 28 +++----------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index aa83110632..3dccb6ee6a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,6 +84,7 @@ 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'), }); @@ -91,11 +92,10 @@ module.exports = React.createClass({ // 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) { badgeContent = "\u00B7\u00B7\u00B7"; @@ -105,29 +105,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - if (this.props.highlight || notificationCount > 0) { - badgeClasses = "mx_RoomTile_badge"; - } else { - badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; - } - - badge =

{ badgeContent }
; - - /* - if (this.props.highlight) { - badge =
!
; - } - else if (this.props.unread) { - badge =
1
; - } - var nameCell; - if (badge) { - nameCell =
{name}
{badge}
; - } - else { - nameCell =
{name}
; - } - */ + badge =
{ badgeContent }
; var label; if (!this.props.collapsed) { From 4013ea75d0f9eba00a24816213ad1a5f68b4d926 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 17:44:31 +0100 Subject: [PATCH 03/47] Testing out the context menu --- src/components/views/rooms/RoomTile.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index aa83110632..2b8d98d42d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -21,6 +21,7 @@ var classNames = require('classnames'); var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +var ContextualMenu = require('../../../ContextualMenu'); import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ @@ -46,6 +47,7 @@ module.exports = React.createClass({ return({ hover : false, badgeHover : false, + menu: false, }); }, @@ -72,6 +74,22 @@ module.exports = React.createClass({ this.setState( { badgeHover : false } ); }, + onBadgeClicked: function(e) { + var Label = sdk.getComponent('elements.Label'); + var elementRect = e.target.getBoundingClientRect(); + var x = elementRect.right; + var y = elementRect.height + (elementRect.height / 2); + var self = this; + ContextualMenu.createMenu(Label, { + left: x, + top: y, + 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]; @@ -111,7 +129,7 @@ module.exports = React.createClass({ badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; } - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; /* if (this.props.highlight) { @@ -160,9 +178,9 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; return connectDragSource(connectDropTarget( -
+
- +
{ label } { badge } From 762873350a5489f77e8b256f35a29aeff03a6c64 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 18:20:12 +0100 Subject: [PATCH 04/47] Badge dohickey shown on name hover and badge hover --- src/components/views/rooms/RoomTile.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3dccb6ee6a..2857a99aa2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -97,6 +97,11 @@ module.exports = React.createClass({ var badge; var badgeContent; + var badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': this.state.badgeHover, + }); + if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { @@ -105,7 +110,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; var label; if (!this.props.collapsed) { @@ -113,9 +118,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } } else if (this.state.hover) { @@ -138,9 +143,9 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; return connectDragSource(connectDropTarget( -
+
- +
{ label } { badge } From 922bb0f40204763794c353958632d4ccc794da60 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 18:50:07 +0100 Subject: [PATCH 05/47] +99 badge when notifications are greater the 99 --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2857a99aa2..0012f66306 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -105,7 +105,8 @@ module.exports = React.createClass({ if (this.state.badgeHover) { 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'; } From 3dd83922f17dbe3d8282d8db89d327e246d52afe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:12:11 +0100 Subject: [PATCH 06/47] Tooltip positioning tweaked --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index aff03182a1..7d41b69567 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -270,7 +270,7 @@ module.exports = React.createClass({ _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.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.offsetTop - this._getScrollNode().scrollTop) + "px"; } }, From 5d4b03c1f4e9790ce6bb589c899b13ba02e953e5 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:28:11 +0100 Subject: [PATCH 07/47] Put back clicking on the name to acces the room --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2b8d98d42d..f0ac208ef9 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -153,9 +153,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } } else if (this.state.hover) { From ca75d93434ad840cc2f76f3cd3dfc470b0839fbe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:31:26 +0100 Subject: [PATCH 08/47] Tweaked the offset for the tooltip so that it is next to the element it is tipping --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index aff03182a1..7d41b69567 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -270,7 +270,7 @@ module.exports = React.createClass({ _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.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.offsetTop - this._getScrollNode().scrollTop) + "px"; } }, From 6984a55b11a9e87100d19b3e6c43f89c973f99d0 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 14:58:09 +0100 Subject: [PATCH 09/47] The tooltip now appears even when not collapsed when hover over the avater, to allow the full name to be shown --- src/components/views/rooms/RoomTile.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 0012f66306..20d0c9824c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -114,6 +114,7 @@ module.exports = React.createClass({ badge =
{ badgeContent }
; var label; + var tooltip; if (!this.props.collapsed) { var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); let nameHTML = emojifyText(name); @@ -123,6 +124,11 @@ module.exports = React.createClass({ } else { label =
; } + + if (this.state.hover) { + var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); + tooltip = ; + } } else if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); @@ -151,6 +157,7 @@ module.exports = React.createClass({ { label } { badge } { incomingCallBox } + { tooltip }
)); } From e4dd6c8dbeced4604dea6d28970f6d0162fac3fd Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 15:15:24 +0100 Subject: [PATCH 10/47] Browser tooltip being used instead of the HTML styled one. --- src/components/views/rooms/RoomTile.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 20d0c9824c..47d61cf95a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -120,15 +120,17 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } + /* if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); tooltip = ; } + */ } else if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); From 9b318e8f610af905c6b7b1aab82e98405a011a2e Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 16:12:20 +0100 Subject: [PATCH 11/47] Getting the corrct height for the placing the context menu --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f0ac208ef9..48cb345d91 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -78,7 +78,7 @@ module.exports = React.createClass({ var Label = sdk.getComponent('elements.Label'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; - var y = elementRect.height + (elementRect.height / 2); + var y = elementRect.top + (elementRect.height / 2); var self = this; ContextualMenu.createMenu(Label, { left: x, From c89904bc25136a9520f798b4e6cc61335f18b5f1 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 17:30:31 +0100 Subject: [PATCH 12/47] Initial unstyled mentions state notifier context menu --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 48cb345d91..9c39b50427 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Label = sdk.getComponent('elements.Label'); + var Label = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; var y = elementRect.top + (elementRect.height / 2); @@ -83,6 +83,7 @@ module.exports = React.createClass({ ContextualMenu.createMenu(Label, { left: x, top: y, + room: this.props.room, onFinished: function() { self.setState({menu: false}); } From a69107f4f1d5351a6bda37bd5c5608fd9b22a96f Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 10:58:43 +0100 Subject: [PATCH 13/47] Trying to get the context menu dohicky to stick when menu open --- src/components/views/rooms/RoomTile.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 9c39b50427..387cca6775 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,20 +75,27 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Label = sdk.getComponent('rooms.NotificationStateContextMenu'); + console.log("DEBUG: MENU FALSE"); + var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; var y = elementRect.top + (elementRect.height / 2); var self = this; - ContextualMenu.createMenu(Label, { + ContextualMenu.createMenu(Menu, { left: x, top: y, room: this.props.room, onFinished: function() { - self.setState({menu: false}); + self.setState({ + menu: false, + badgeHover: false, + }); } }); - this.setState({menu: true}); + this.setState({ + menu: true, + badgeHover: true, + }); }, render: function() { From 8b8486a8d096b285e2b03032aedf5917afcdfa7d Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 14:39:15 +0100 Subject: [PATCH 14/47] Move from +99 to 99+ for more than 99 messages --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 47d61cf95a..b39e80cb54 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -105,7 +105,7 @@ module.exports = React.createClass({ if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { - var limitedCount = (notificationCount > 99) ? '+99' : notificationCount; + var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; badgeContent = notificationCount ? limitedCount : '!'; } else { badgeContent = '\u200B'; From 9c9c5ec4588e56ddd3b3ad56619b452454f84e4b Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 17:18:45 +0100 Subject: [PATCH 15/47] Menu context dohickey now sticks when menu shown --- src/components/views/rooms/RoomTile.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 85a19eeb6e..d373cbc69c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -86,16 +86,10 @@ module.exports = React.createClass({ top: y, room: this.props.room, onFinished: function() { - self.setState({ - menu: false, - badgeHover: false, - }); + self.setState({ menu: false }); } }); - this.setState({ - menu: true, - badgeHover: true, - }); + this.setState({ menu: true }); }, render: function() { @@ -113,6 +107,7 @@ module.exports = React.createClass({ '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, }); // XXX: We should never display raw room IDs, but sometimes the @@ -128,7 +123,7 @@ module.exports = React.createClass({ 'mx_RoomTile_badgeButton': this.state.badgeHover, }); - if (this.state.badgeHover) { + if (this.state.badgeHover || this.state.menu) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; From 8f3e93214c73531808f1153b735c680c370cefed Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 18:02:30 +0100 Subject: [PATCH 16/47] Drawing cheveron with CSS so it can be styled --- src/ContextualMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index e720b69eda..65d3e98910 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -54,10 +54,10 @@ module.exports = { var chevron = null; if (props.left) { - chevron = + chevron =
position.left = props.left + 8; } else { - chevron = + chevron =
position.right = props.right + 8; } From 477a17b49f0aeb94f5ed803463502138d116cca6 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 26 Jul 2016 10:39:34 +0100 Subject: [PATCH 17/47] Removed debug statement --- src/components/views/rooms/RoomTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d373cbc69c..28574fcec5 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,6 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - console.log("DEBUG: MENU FALSE"); var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; From 0eb15085e97c55238089f9f8ae3b76eac66e7052 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 26 Jul 2016 17:25:16 +0100 Subject: [PATCH 18/47] some code tidyup --- src/ContextualMenu.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index 65d3e98910..695b8445ab 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -45,7 +45,9 @@ 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 = { @@ -54,10 +56,10 @@ module.exports = { var chevron = null; if (props.left) { - chevron =
+ chevron =
position.left = props.left + 8; } else { - chevron =
+ chevron =
position.right = props.right + 8; } From dca4702b7b3df510e9b9fd28524e5e5ac0b57448 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 09:51:50 +0100 Subject: [PATCH 19/47] Fixed a bug where the contextual menu was being incorrectly positioned when zoom in on the webpage --- src/components/views/rooms/EventTile.js | 6 ++++-- src/components/views/rooms/RoomTile.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 77be8226a2..129eb120fa 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -251,8 +251,10 @@ module.exports = React.createClass({ onEditClicked: function(e) { var MessageContextMenu = sdk.getComponent('rooms.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; var self = this; ContextualMenu.createMenu(MessageContextMenu, { mxEvent: this.props.mxEvent, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 28574fcec5..ce4888e896 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -77,8 +77,9 @@ module.exports = React.createClass({ onBadgeClicked: function(e) { var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); - var x = elementRect.right; - var y = elementRect.top + (elementRect.height / 2); + // The window X and Y offsets are to adjust position when zoomed in to page + var x = elementRect.right + window.pageXOffset; + var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; var self = this; ContextualMenu.createMenu(Menu, { left: x, From 4b26ac58afeff66af403ef4110f32e95c75a4d08 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 10:41:24 +0100 Subject: [PATCH 20/47] Re-modularised the context menus --- src/{ => components/structures}/ContextualMenu.js | 0 src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/RoomTile.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => components/structures}/ContextualMenu.js (100%) diff --git a/src/ContextualMenu.js b/src/components/structures/ContextualMenu.js similarity index 100% rename from src/ContextualMenu.js rename to src/components/structures/ContextualMenu.js diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 129eb120fa..6006ff3ebd 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -249,7 +249,7 @@ 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() // The window X and Y offsets are to adjust position when zoomed in to page diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ce4888e896..654bf268e1 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); + 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; From 6d141d1a7ba1929799edc109fb1a8b632349c120 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 11:58:40 +0100 Subject: [PATCH 21/47] Only allow none guests to access the context menu --- src/components/views/rooms/RoomTile.js | 38 +++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 654bf268e1..43d6a31738 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -67,7 +67,10 @@ module.exports = React.createClass({ }, badgeOnMouseEnter: function() { - this.setState( { badgeHover : true } ); + // Only allow none guests to access the context menu + if (!MatrixClientPeg.get().isGuest()) { + this.setState( { badgeHover : true } ); + } }, badgeOnMouseLeave: function() { @@ -75,21 +78,24 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - 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; - var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; - var self = this; - ContextualMenu.createMenu(Menu, { - left: x, - top: y, - room: this.props.room, - onFinished: function() { - self.setState({ menu: false }); - } - }); - this.setState({ menu: true }); + // Only allow none guests to access the context menu + if (!MatrixClientPeg.get().isGuest()) { + 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; + var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + var self = this; + ContextualMenu.createMenu(Menu, { + left: x, + top: y, + room: this.props.room, + onFinished: function() { + self.setState({ menu: false }); + } + }); + this.setState({ menu: true }); + } }, render: function() { From 3df746ef143a02103272081f384280961bc18b81 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:16:17 +0100 Subject: [PATCH 22/47] Revert merge up from develop --- package.json | 10 +- src/GuestAccess.js | 51 ------- src/MatrixClientPeg.js | 171 +++++++++++++---------- src/TabComplete.js | 7 +- src/components/structures/RoomView.js | 18 ++- src/components/views/rooms/MemberInfo.js | 8 +- src/ratelimitedfunc.js | 8 ++ 7 files changed, 130 insertions(+), 143 deletions(-) delete mode 100644 src/GuestAccess.js diff --git a/package.json b/package.json index cd81ad7c56..60fc45db77 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "reskindex": "./reskindex.js" }, "scripts": { - "reskindex": "reskindex -h header", + "reskindex": "./reskindex.js -h header", "build": "babel src -d lib --source-maps --stage 1", "start": "babel src -w -d lib --source-maps --stage 1", "lint": "eslint src/", @@ -42,10 +42,10 @@ "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", - "react": "^15.0.1", - "react-addons-css-transition-group": "^15.1.0", - "react-dom": "^15.0.1", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", + "react": "^15.2.1", + "react-addons-css-transition-group": "^15.2.1", + "react-dom": "^15.2.1", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" diff --git a/src/GuestAccess.js b/src/GuestAccess.js deleted file mode 100644 index ef48d23ded..0000000000 --- a/src/GuestAccess.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -const IS_GUEST_KEY = "matrix-is-guest"; - -class GuestAccess { - - constructor(localStorage) { - this.localStorage = localStorage; - try { - this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true"; - } - catch (e) {} // don't care - } - - setPeekedRoom(roomId) { - // we purposefully do not persist this to local storage as peeking is - // entirely transient. - this._peekedRoomId = roomId; - } - - getPeekedRoom() { - return this._peekedRoomId; - } - - isGuest() { - return this._isGuest; - } - - markAsGuest(isGuest) { - try { - this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest)); - } catch (e) {} // ignore. If they don't do LS, they'll just get a new account. - this._isGuest = isGuest; - this._peekedRoomId = null; - } -} - -module.exports = GuestAccess; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 7c1c5b34d7..ce4b5ba743 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,13 +16,9 @@ limitations under the License. 'use strict'; -// A thing that holds your Matrix Client -var Matrix = require("matrix-js-sdk"); -var GuestAccess = require("./GuestAccess"); +import Matrix from 'matrix-js-sdk'; -let matrixClient: MatrixClient = null; - -var localStorage = window.localStorage; +const localStorage = window.localStorage; function deviceId() { // XXX: is Math.random()'s deterministicity a problem here? @@ -35,82 +31,42 @@ function deviceId() { return id; } -function createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess) { - var opts = { - baseUrl: hs_url, - idBaseUrl: is_url, - accessToken: access_token, - userId: user_id, - timelineSupport: true, - }; - - if (localStorage) { - opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - opts.deviceId = deviceId(); - } - - matrixClient = Matrix.createClient(opts); - - // we're going to add eventlisteners for each matrix event tile, so the - // potential number of event listeners is quite high. - matrixClient.setMaxListeners(500); - - if (guestAccess) { - console.log("Guest: %s", guestAccess.isGuest()); - matrixClient.setGuest(guestAccess.isGuest()); - var peekedRoomId = guestAccess.getPeekedRoom(); - if (peekedRoomId) { - console.log("Peeking in room %s", peekedRoomId); - matrixClient.peekInRoom(peekedRoomId); - } - } -} - -if (localStorage) { - var hs_url = localStorage.getItem("mx_hs_url"); - var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; - var access_token = localStorage.getItem("mx_access_token"); - var user_id = localStorage.getItem("mx_user_id"); - var guestAccess = new GuestAccess(localStorage); - if (access_token && user_id && hs_url) { - console.log("Restoring session for %s", user_id); - createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess); - } - else { - console.log("Session not found."); - } -} - -class MatrixClient { - - constructor(guestAccess) { - this.guestAccess = guestAccess; +/** + * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk + * Handles the creation/initialisation of client objects. + * This module provides a singleton instance of this class so the 'current' + * Matrix Client object is available easily. + */ +class MatrixClientPeg { + constructor() { + this.matrixClient = null; } get(): MatrixClient { - return matrixClient; + return this.matrixClient; } unset() { - matrixClient = null; + this.matrixClient = null; } - // FIXME, XXX: this all seems very convoluted :( - // - // Why do we have this peg wrapper rather than just MatrixClient.get()? - // Why do we name MatrixClient as MatrixClientPeg when we export it? - // - // -matthew - + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs but no credentials + */ replaceUsingUrls(hs_url, is_url) { - this.replaceClient(hs_url, is_url); + this._replaceClient(hs_url, is_url); } + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs and active credentials + */ replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { - this.replaceClient(hs_url, is_url, user_id, access_token, isGuest); + this._replaceClient(hs_url, is_url, user_id, access_token, isGuest); } - replaceClient(hs_url, is_url, user_id, access_token, isGuest) { + _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); @@ -118,15 +74,19 @@ class MatrixClient { console.warn("Error clearing local storage", e); } } - this.guestAccess.markAsGuest(Boolean(isGuest)); - createClientForPeg(hs_url, is_url, user_id, access_token, this.guestAccess); + this._createClient(hs_url, is_url, user_id, access_token, isGuest); + if (localStorage) { try { localStorage.setItem("mx_hs_url", hs_url); localStorage.setItem("mx_is_url", is_url); - localStorage.setItem("mx_user_id", user_id); - localStorage.setItem("mx_access_token", access_token); - console.log("Session persisted for %s", user_id); + + if (user_id !== undefined && access_token !== undefined) { + localStorage.setItem("mx_user_id", user_id); + localStorage.setItem("mx_access_token", access_token); + localStorage.setItem("mx_is_guest", JSON.stringify(isGuest)); + console.log("Session persisted for %s", user_id); + } } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -134,9 +94,68 @@ class MatrixClient { console.warn("No local storage available: can't persist session!"); } } + + getCredentials() { + return [ + this.matrixClient.baseUrl, + this.matrixClient.idBaseUrl, + this.matrixClient.credentials.userId, + this.matrixClient.getAccessToken(), + this.matrixClient.isGuest(), + ]; + } + + tryRestore() { + if (localStorage) { + const hs_url = localStorage.getItem("mx_hs_url"); + const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; + const access_token = localStorage.getItem("mx_access_token"); + const user_id = localStorage.getItem("mx_user_id"); + + let is_guest; + if (localStorage.getItem("mx_is_guest") !== null) { + is_guest = localStorage.getItem("mx_is_guest") === "true"; + } else { + // legacy key name + is_guest = localStorage.getItem("matrix-is-guest") === "true"; + } + + if (access_token && user_id && hs_url) { + console.log("Restoring session for %s", user_id); + this._createClient(hs_url, is_url, user_id, access_token); + this.matrixClient.setGuest(is_guest); + } else { + console.log("Session not found."); + } + } + } + + _createClient(hs_url, is_url, user_id, access_token, isGuest) { + var opts = { + baseUrl: hs_url, + idBaseUrl: is_url, + accessToken: access_token, + userId: user_id, + timelineSupport: true, + }; + + if (localStorage) { + opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + opts.deviceId = deviceId(); + } + + this.matrixClient = Matrix.createClient(opts); + + // we're going to add eventlisteners for each matrix event tile, so the + // potential number of event listeners is quite high. + this.matrixClient.setMaxListeners(500); + + this.matrixClient.setGuest(Boolean(isGuest)); + } } -if (!global.mxMatrixClient) { - global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage)); +if (!global.mxMatrixClientPeg) { + global.mxMatrixClientPeg = new MatrixClientPeg(); + global.mxMatrixClientPeg.tryRestore(); } -module.exports = global.mxMatrixClient; +module.exports = global.mxMatrixClientPeg; diff --git a/src/TabComplete.js b/src/TabComplete.js index 0ec0b77802..65441c9381 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -341,7 +341,12 @@ class TabComplete { } if (a.kind == 'member') { - return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + let orderA = this.memberTabOrder[a.member.userId]; + let orderB = this.memberTabOrder[b.member.userId]; + if (orderA === undefined) orderA = -1; + if (orderB === undefined) orderB = -1; + + return orderB - orderA; } // anything else we have no ordering for diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index accf96f349..9fbdb51f11 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -493,6 +493,17 @@ module.exports = React.createClass({ return; } + if (this.props.ConferenceHandler && + member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + this._updateConfCallNotification(); + } + + this._updateRoomMembers(); + }, + + // rate limited because a power level change will emit an event for every + // member in the room. + _updateRoomMembers: new rate_limited_func(function() { // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); this._updateAutoComplete(); @@ -506,12 +517,7 @@ module.exports = React.createClass({ joining: false }); } - - if (this.props.ConferenceHandler && - member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { - this._updateConfCallNotification(); - } - }, + }, 500), _hasUnsentMessages: function(room) { return this._getUnsentMessages(room).length > 0; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c439f8b40c..07a7b9398d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -406,14 +406,14 @@ module.exports = React.createClass({ this.props.onFinished(); } else { - self.setState({ updating: self.state.updating + 1 }); + this.setState({ updating: this.state.updating + 1 }); createRoom({ createOpts: { invite: [this.props.member.userId], }, - }).finally(function() { - self.props.onFinished(); - self.setState({ updating: self.state.updating - 1 }); + }).finally(() => { + this.props.onFinished(); + this.setState({ updating: this.state.updating - 1 }); }).done(); } }, diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 453669b477..ed892f4eac 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * 'debounces' a function to only execute every n milliseconds. + * Useful when react-sdk gets many, many events but only wants + * to update the interface once for all of them. + * + * Note that the function must not take arguments, since the args + * could be different for each invocarion of the function. + */ module.exports = function(f, minIntervalMs) { this.lastCall = 0; this.scheduledCall = undefined; From 3cb3dd96a64d6929bc161e458e3d61e67f89ee9d Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:23:37 +0100 Subject: [PATCH 23/47] Added the moved ContextualMenu to the components list --- src/component-index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/component-index.js b/src/component-index.js index 5fadb18b6a..7dfdcd6f8b 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -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'); From 0660b9feff800761731d06a60246c7be8e9165ee Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:49:10 +0100 Subject: [PATCH 24/47] Fixes to properly point to the moved components --- .DS_Store | Bin 0 -> 6148 bytes src/components/structures/MatrixChat.js | 2 +- src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/RoomTile.js | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 27 Jul 2016 16:09:07 +0100 Subject: [PATCH 25/47] Refactor so that chevron and menu can be positioned independantly --- src/components/structures/ContextualMenu.js | 19 +++++++++++++------ src/components/views/rooms/EventTile.js | 3 ++- src/components/views/rooms/RoomTile.js | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 695b8445ab..c6f8e32b11 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; +var classNames = require('classnames'); var React = require('react'); var ReactDOM = require('react-dom'); @@ -51,25 +52,31 @@ module.exports = { }; var position = { - top: props.top - 20, + top: props.top, }; var chevron = null; if (props.left) { - chevron =
- position.left = props.left + 8; + chevron =
+ position.left = props.left; } else { chevron =
- position.right = props.right + 8; + 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, + }); + // 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 = ( -
-
+
+
{chevron}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a24c597ad4..7945debd1a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -254,9 +254,10 @@ module.exports = React.createClass({ // 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; + 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, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a3ebf3ed45..13b20118e3 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,9 +84,10 @@ module.exports = React.createClass({ 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; - var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; var self = this; ContextualMenu.createMenu(Menu, { + chevronOffset: 35, left: x, top: y, room: this.props.room, From 8246d9148cc4d1345d28be54c979021e686773e1 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 17:43:48 +0100 Subject: [PATCH 26/47] Initial context menu with all it's elements --- src/components/structures/ContextualMenu.js | 11 ++++++++++- src/components/views/rooms/RoomTile.js | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index c6f8e32b11..bf63227283 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -72,11 +72,20 @@ module.exports = { '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 = (
-
+
{chevron}
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 13b20118e3..a500f18cb1 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -87,6 +87,8 @@ module.exports = React.createClass({ var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; var self = this; ContextualMenu.createMenu(Menu, { + menuWidth: 188, + menuHeight: 126, chevronOffset: 35, left: x, top: y, From 46a643ac38b5b9b93e368c64c5f9752477650448 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 18:10:45 +0100 Subject: [PATCH 27/47] Now the text spaceing is correct need to align cheveron --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a500f18cb1..1f77c6a110 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -89,7 +89,7 @@ module.exports = React.createClass({ ContextualMenu.createMenu(Menu, { menuWidth: 188, menuHeight: 126, - chevronOffset: 35, + chevronOffset: 45, left: x, top: y, room: this.props.room, From 867b14d2cacd6db7eb5a1e1404b40081d44eb836 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 18:14:46 +0100 Subject: [PATCH 28/47] Reposition context menu now that the styling is final --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1f77c6a110..a0af243ea4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,7 +84,7 @@ module.exports = React.createClass({ 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; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; + var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; var self = this; ContextualMenu.createMenu(Menu, { menuWidth: 188, From bc902a9741c4d72e3bf9c8c7079f5564fefab5a3 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 14:32:59 +0100 Subject: [PATCH 29/47] Tidy up of the contextual menu refactor --- src/components/structures/ContextualMenu.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index bf63227283..114bdaad1b 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -28,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); @@ -55,12 +61,16 @@ module.exports = { top: props.top, }; + var chevronOffset = { + top: props.cheveronOffset, + } + var chevron = null; if (props.left) { - chevron =
+ chevron =
position.left = props.left; } else { - chevron =
+ chevron =
position.right = props.right; } From 94350bc780f17bba6b9e050e876c1beb761e3520 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 15:51:46 +0100 Subject: [PATCH 30/47] Fixed spelling mistake --- src/components/structures/ContextualMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 114bdaad1b..fcfc5d5e50 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -62,7 +62,7 @@ module.exports = { }; var chevronOffset = { - top: props.cheveronOffset, + top: props.chevronOffset, } var chevron = null; From 5889beacf3452883e9c7c1d197f9fd01f4fd651a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 17:24:58 +0100 Subject: [PATCH 31/47] Mute state now handled correctly --- src/components/views/rooms/RoomTile.js | 68 +++++++++++++++++++++----- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a0af243ea4..48b2ea84f4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -44,17 +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 notificaion 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, }); }, @@ -119,6 +150,17 @@ module.exports = React.createClass({ '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; @@ -127,11 +169,6 @@ module.exports = React.createClass({ var badge; var badgeContent; - var badgeClasses = classNames({ - 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover, - }); - if (this.state.badgeHover || this.state.menu) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { @@ -141,19 +178,28 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
{ badgeContent }
; + if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { + badge =
; + } else { + badge =
{ badgeContent }
; + } 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, + }); + let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } } else if (this.state.hover) { @@ -177,7 +223,7 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
-
+
{ label } From 38f504bdcbaf6c57bdc7ab6ab8512154cff76efb Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 11:10:16 +0100 Subject: [PATCH 32/47] Hide tooltip when badge clicked and collapsed --- src/components/views/rooms/RoomTile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 48b2ea84f4..3f6704c2a2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -111,6 +111,12 @@ module.exports = React.createClass({ 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 From 229664a624eac2768c6522511431188e00fc81d0 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 17:49:42 +0100 Subject: [PATCH 33/47] first pass on css re-write --- src/components/views/rooms/RoomTile.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3f6704c2a2..e74647f717 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -229,11 +229,15 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
-
- +
+
+ +
{ label } - { badge } +
+ { badge } +
{ incomingCallBox } { tooltip }
From 1487c600ee5510ba8dd9bd3a2d6166d9d98224bb Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 17:53:18 +0100 Subject: [PATCH 34/47] Revert so that the --- src/components/views/rooms/RoomTile.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e74647f717..3f6704c2a2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -229,15 +229,11 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
-
-
- -
+
+
{ label } -
- { badge } -
+ { badge } { incomingCallBox } { tooltip }
From 398e56c9f9eec489be6b80dd9f208e5185742485 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sat, 30 Jul 2016 12:28:10 +0100 Subject: [PATCH 35/47] Fixed bug where the long hover tooltip for a room name show an object rather than the name --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3f6704c2a2..e31514e015 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -201,9 +201,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { - name = ; + let nameSelected = ; - label =
{ name }
; + label =
{ nameSelected }
; } else { label =
; } From d16aa276e2a29ea0ece9f5e39e0ef7afe7ed4879 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sat, 30 Jul 2016 12:52:39 +0100 Subject: [PATCH 36/47] Positional tweaks for the name, badge and context menu, to better match design in both normal and collapsed states --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e31514e015..d6b33b9409 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -120,7 +120,7 @@ module.exports = React.createClass({ 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; + var x = elementRect.right + window.pageXOffset + 3; var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; var self = this; ContextualMenu.createMenu(Menu, { From 2c2f689361d1fa12352acd8555d2db831d30ad61 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 2 Aug 2016 14:46:47 +0100 Subject: [PATCH 37/47] New design for long names --- src/components/views/rooms/RoomList.js | 6 ++++-- src/components/views/rooms/RoomTile.js | 18 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e2bc25653c..8e57ceab9b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -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 = (3 + 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"; } }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d6b33b9409..5e367ffd80 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -99,7 +99,8 @@ module.exports = React.createClass({ badgeOnMouseEnter: function() { // Only allow none guests to access the context menu - if (!MatrixClientPeg.get().isGuest()) { + // and only change it if it needs to change + if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) { this.setState( { badgeHover : true } ); } }, @@ -185,9 +186,9 @@ module.exports = React.createClass({ } if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { - badge =
; + badge =
; } else { - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; } var label; @@ -197,15 +198,16 @@ module.exports = React.createClass({ '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) { let nameSelected = ; - label =
{ nameSelected }
; + label =
{ nameSelected }
; } else { - label =
; + label =
; } } else if (this.state.hover) { @@ -232,8 +234,10 @@ module.exports = React.createClass({
- { label } - { badge } +
+ { label } + { badge } +
{ incomingCallBox } { tooltip }
From 2cddf18461851c851d75eea55b21e830325754a5 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 2 Aug 2016 10:00:12 +0530 Subject: [PATCH 38/47] strip (IRC) displayname suffix from autocomplete Fixes vector-im/vector-web#574 --- src/autocomplete/UserProvider.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index a583592bae..8828f8cb70 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -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: ( ), range From 1f9a396fa53b5da3fd8f6e9e6ea6669d13a92d00 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 17:51:40 +0530 Subject: [PATCH 39/47] fix: autocomplete to use tab instead of return --- src/components/views/rooms/MessageComposer.js | 8 -------- src/components/views/rooms/MessageComposerInput.js | 13 ++++--------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 282f7f013f..edd8ed7b9a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -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, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..690da28c01 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -422,12 +422,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()) { @@ -519,8 +513,8 @@ export default class MessageComposerInput extends React.Component { } onTab(e) { - if (this.props.onTab) { - if (this.props.onTab()) { + if (this.props.tryComplete) { + if (this.props.tryComplete()) { e.preventDefault(); } } @@ -585,5 +579,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, }; From 569b6057c3c35b778728bf237a26e2f0f34a92b8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 3 Aug 2016 13:27:06 +0100 Subject: [PATCH 40/47] fix upload for video or image files where sniffing the content.info fails --- src/ContentMessages.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 796c1ed58d..fd18b22d30 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -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(); From a2d64f51197109c3a85d18fbf81704147362b07e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 18:04:52 +0530 Subject: [PATCH 41/47] fix: allow up/down normally for no completions Autocomplete current eats up up/down key events by unconditionally returning true for onUpArrow and onDownArrow. Instead, only do that if there are completions actually visible. --- src/components/views/rooms/Autocomplete.js | 6 ++++++ src/components/views/rooms/MessageComposerInput.js | 12 ++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 95133778ba..32e568e2ba 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -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; } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..1c81a69f16 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -503,18 +503,14 @@ 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(); } } From 8e66e6dfdd7c82c626bb90e1c7432d3693255ec1 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 18:27:49 +0530 Subject: [PATCH 42/47] fix: Switch to opacity: 0 for composer emoji. This seems to be the best option for displaying emoji in the composer. While it means selected emoji don't actually have the selection colour applied, it's the most functional of all the options. Facebook uses the same approach. --- src/RichText.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index a5bc554b95..7cd78a14c9 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -98,7 +98,7 @@ function unicodeToEmojiUri(str) { return str; } -// Unused for now, due to https://github.com/facebook/draft-js/issues/414 +// Workaround for https://github.com/facebook/draft-js/issues/414 let emojiDecorator = { strategy: (contentBlock, callback) => { findWithRegex(EMOJI_REGEX, contentBlock, callback); @@ -115,7 +115,7 @@ let emojiDecorator = { backgroundPosition: 'center center', overflow: 'hidden', }; - return ({props.children}); + return ({props.children}); }, }; From 8e19532e5a231c3331bc91e5dadcafc8209ebb46 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 3 Aug 2016 14:09:10 +0100 Subject: [PATCH 43/47] Hover state for badges, now only on the badges themselves --- src/components/views/rooms/RoomTile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 5e367ffd80..30245f61ee 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -186,9 +186,9 @@ module.exports = React.createClass({ } if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { - badge =
; + badge =
; } else { - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; } var label; @@ -234,7 +234,7 @@ module.exports = React.createClass({
-
+
{ label } { badge }
From 55f4d23625af7d71a25eb2c7c79dacbde63a59e0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 14:18:09 +0100 Subject: [PATCH 44/47] MemberDeviceInfo: Use the device name, where available we now have device name for e2e devices: use it! --- src/components/views/rooms/MemberDeviceInfo.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index b7ddf9b2ce..7e684c89a2 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -97,9 +97,11 @@ module.exports = React.createClass({ ); } + var deviceName = this.props.device.display_name || this.props.device.id; + return (
-
{this.props.device.id}
+
{deviceName}
{indicator} {verifyButton} {blockButton} From f4d41b78473200be4c9e19f433e3e97dbdb2413d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 14:19:54 +0100 Subject: [PATCH 45/47] DevicesPanel: use device_id as a placeholder A device may have no display_name set, in which case we probably want to use the device_id as a placeholder. --- src/components/views/settings/DevicesPanelEntry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index 6858e62102..b660f196c8 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -111,7 +111,9 @@ export default class DevicesPanelEntry extends React.Component {
+ onSubmit={this._onDisplayNameChanged} + placeholder={device.device_id} + />
{lastSeen} From c1cfbd6b59ed9b4a7b1eaea7706b11b50aea00fe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 3 Aug 2016 14:47:53 +0100 Subject: [PATCH 46/47] Corrected comment typo --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 30245f61ee..602ed4ee04 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -66,7 +66,7 @@ module.exports = React.createClass({ onAction: function(payload) { switch (payload.action) { case 'notification_change': - // Is the notificaion about this room + // Is the notification about this room? if (payload.roomId === this.props.room.roomId) { this.setState( { areNotifsMuted : payload.isMuted }); } From 8a57881618d30bcb2469791268b8786d55a5651c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 15:23:12 +0100 Subject: [PATCH 47/47] Add more logging to TimelinePanel-test In an attempt to figure out why this is timing out sometimes, add even more debugging. --- .../structures/TimelinePanel-test.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index cd9d86cd64..7a603d138f 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -224,7 +224,7 @@ describe('TimelinePanel', function() { var scrollDefer; var panel = ReactDOM.render( - {scrollDefer.resolve()}} />, + {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(() => {