Merge branch 'develop' into dbkr/deactivate_account

pull/21833/head
David Baker 2016-08-03 17:52:18 +01:00
commit 498ad7fa4c
16 changed files with 346 additions and 142 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -104,19 +104,25 @@ class ContentMessages {
var def = q.defer(); var def = q.defer();
if (file.type.indexOf('image/') == 0) { if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(file).then(function (imageInfo) { infoForImageFile(file).then(imageInfo=>{
extend(content.info, imageInfo); extend(content.info, imageInfo);
def.resolve(); def.resolve();
}, error=>{
content.msgtype = 'm.file';
def.resolve();
}); });
} else if (file.type.indexOf('audio/') == 0) { } else if (file.type.indexOf('audio/') == 0) {
content.msgtype = 'm.audio'; content.msgtype = 'm.audio';
def.resolve(); def.resolve();
} else if (file.type.indexOf('video/') == 0) { } else if (file.type.indexOf('video/') == 0) {
content.msgtype = 'm.video'; content.msgtype = 'm.video';
infoForVideoFile(file).then(function (videoInfo) { infoForVideoFile(file).then(videoInfo=>{
extend(content.info, videoInfo); extend(content.info, videoInfo);
def.resolve(); def.resolve();
}); }, error=>{
content.msgtype = 'm.file';
def.resolve();
});
} else { } else {
content.msgtype = 'm.file'; content.msgtype = 'm.file';
def.resolve(); def.resolve();

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { import {
Editor, Editor,
EditorState,
Modifier, Modifier,
ContentState, ContentState,
ContentBlock, ContentBlock,
@ -9,12 +10,13 @@ import {
DefaultDraftInlineStyle, DefaultDraftInlineStyle,
CompositeDecorator, CompositeDecorator,
SelectionState, SelectionState,
Entity,
} from 'draft-js'; } from 'draft-js';
import * as sdk from './index'; import * as sdk from './index';
import * as emojione from 'emojione'; import * as emojione from 'emojione';
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', { 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> 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 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}>`; let result = `<${elem}>${content.join('')}</${elem}>`;
// dirty hack because we don't want block level tags by default, but breaks // dirty hack because we don't want block level tags by default, but breaks
if(elem === 'span') if (elem === 'span')
result += '<br />'; result += '<br />';
return result; return result;
}).join(''); }).join('');
@ -75,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState {
return ContentState.createFromBlockArray(convertFromHTML(html)); 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. * 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) // unused until we make these decorators immutable (autocomplete needed)
let name = member ? member.name : null; let name = member ? member.name : null;
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : 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 return [usernameDecorator, roomDecorator, emojiDecorator];
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];
} }
export function getScopedMDDecorators(scope: any): CompositeDecorator { export function getScopedMDDecorators(scope: any): CompositeDecorator {
@ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
</a> </a>
) )
}); });
markdownDecorators.push(emojiDecorator);
return markdownDecorators; return markdownDecorators;
} }
@ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
export function selectionStateToTextOffsets(selectionState: SelectionState, export function selectionStateToTextOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} { contentBlocks: Array<ContentBlock>): {start: number, end: number} {
let offset = 0, start = 0, end = 0; let offset = 0, start = 0, end = 0;
for(let block of contentBlocks) { for (let block of contentBlocks) {
if (selectionState.getStartKey() === block.getKey()) { if (selectionState.getStartKey() === block.getKey()) {
start = offset + selectionState.getStartOffset(); start = offset + selectionState.getStartOffset();
} }
@ -240,3 +275,50 @@ export function textOffsetsToSelectionState({start, end}: {start: number, end: n
return selectionState; 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;
}

View File

@ -11,11 +11,11 @@ let instance = null;
export default class UserProvider extends AutocompleteProvider { export default class UserProvider extends AutocompleteProvider {
constructor() { constructor() {
super(USER_REGEX, { super(USER_REGEX, {
keys: ['displayName', 'userId'], keys: ['name', 'userId'],
}); });
this.users = []; this.users = [];
this.fuse = new Fuse([], { this.fuse = new Fuse([], {
keys: ['displayName', 'userId'], keys: ['name', 'userId'],
}); });
} }
@ -25,11 +25,12 @@ export default class UserProvider extends AutocompleteProvider {
if (command) { if (command) {
this.fuse.set(this.users); this.fuse.set(this.users);
completions = this.fuse.search(command[0]).map(user => { completions = this.fuse.search(command[0]).map(user => {
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
return { return {
completion: user.userId, completion: user.userId,
component: ( component: (
<TextualCompletion <TextualCompletion
title={user.displayName || user.userId} title={displayName}
description={user.userId} /> description={user.userId} />
), ),
range range

View File

@ -25,6 +25,7 @@ limitations under the License.
*/ */
module.exports.components = {}; 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.CreateRoom'] = require('./components/structures/CreateRoom');
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var classNames = require('classnames');
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
@ -27,6 +28,12 @@ var ReactDOM = require('react-dom');
module.exports = { module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container", ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: {
menuWidth: React.PropTypes.number,
menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number,
},
getOrCreateContainer: function() { getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId); var container = document.getElementById(this.ContextualMenuContainerId);
@ -45,29 +52,50 @@ module.exports = {
var closeMenu = function() { var closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments); if (props && props.onFinished) {
props.onFinished.apply(null, arguments);
}
}; };
var position = { var position = {
top: props.top - 20, top: props.top,
}; };
var chevronOffset = {
top: props.chevronOffset,
}
var chevron = null; var chevron = null;
if (props.left) { if (props.left) {
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" /> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
position.left = props.left + 8; position.left = props.left;
} else { } else {
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" /> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
position.right = props.right + 8; position.right = props.right;
} }
var className = 'mx_ContextualMenu_wrapper'; 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 // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
var menu = ( var menu = (
<div className={className}> <div className={className} style={position}>
<div className="mx_ContextualMenu" style={position}> <div className={menuClasses} style={menuSize}>
{chevron} {chevron}
<Element {...props} onFinished={closeMenu}/> <Element {...props} onFinished={closeMenu}/>
</div> </div>

View File

@ -20,7 +20,7 @@ var Favico = require('favico.js');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var Notifier = require("../../Notifier"); var Notifier = require("../../Notifier");
var ContextualMenu = require("../../ContextualMenu"); var ContextualMenu = require("./ContextualMenu");
var RoomListSorter = require("../../RoomListSorter"); var RoomListSorter = require("../../RoomListSorter");
var UserActivity = require("../../UserActivity"); var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence"); var Presence = require("../../Presence");

View File

@ -64,6 +64,9 @@ export default class Autocomplete extends React.Component {
onUpArrow(): boolean { onUpArrow(): boolean {
let completionCount = this.countCompletions(), let completionCount = this.countCompletions(),
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset); this.setSelection(selectionOffset);
return true; return true;
} }
@ -72,6 +75,9 @@ export default class Autocomplete extends React.Component {
onDownArrow(): boolean { onDownArrow(): boolean {
let completionCount = this.countCompletions(), let completionCount = this.countCompletions(),
selectionOffset = (this.state.selectionOffset + 1) % completionCount; selectionOffset = (this.state.selectionOffset + 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset); this.setSelection(selectionOffset);
return true; return true;
} }

View File

@ -23,7 +23,7 @@ var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg') var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent'); var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu'); var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher"); var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils'); var ObjectUtils = require('../../../ObjectUtils');
@ -249,12 +249,15 @@ module.exports = React.createClass({
}, },
onEditClicked: function(e) { onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect() 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; var self = this;
ContextualMenu.createMenu(MessageContextMenu, { ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
left: x, left: x,
top: y, top: y,

View File

@ -97,9 +97,11 @@ module.exports = React.createClass({
); );
} }
var deviceName = this.props.device.display_name || this.props.device.id;
return ( return (
<div className="mx_MemberDeviceInfo"> <div className="mx_MemberDeviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div> <div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator} {indicator}
{verifyButton} {verifyButton}
{blockButton} {blockButton}

View File

@ -36,7 +36,6 @@ export default class MessageComposer extends React.Component {
this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this); this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this);
this.onTab = this.onTab.bind(this);
this._tryComplete = this._tryComplete.bind(this); this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
@ -143,12 +142,6 @@ export default class MessageComposer extends React.Component {
return this.refs.autocomplete.onDownArrow(); 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 { _tryComplete(): boolean {
if (this.refs.autocomplete) { if (this.refs.autocomplete) {
return this.refs.autocomplete.onConfirm(); return this.refs.autocomplete.onConfirm();
@ -223,7 +216,6 @@ export default class MessageComposer extends React.Component {
tryComplete={this._tryComplete} tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow} onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow} onDownArrow={this.onDownArrow}
onTab={this.onTab}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} />, onContentChanged={this.onInputContentChanged} />,
uploadButton, uploadButton,

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
var marked = require("marked"); import marked from 'marked';
marked.setOptions({ marked.setOptions({
renderer: new marked.Renderer(), renderer: new marked.Renderer(),
gfm: true, gfm: true,
@ -24,7 +24,7 @@ marked.setOptions({
pedantic: false, pedantic: false,
sanitize: true, sanitize: true,
smartLists: true, smartLists: true,
smartypants: false smartypants: false,
}); });
import {Editor, EditorState, RichUtils, CompositeDecorator, import {Editor, EditorState, RichUtils, CompositeDecorator,
@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
import {stateToMarkdown} from 'draft-js-export-markdown'; import {stateToMarkdown} from 'draft-js-export-markdown';
var MatrixClientPeg = require("../../../MatrixClientPeg"); import MatrixClientPeg from '../../../MatrixClientPeg';
var SlashCommands = require("../../../SlashCommands"); import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
var Modal = require("../../../Modal"); import SlashCommands from '../../../SlashCommands';
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; import Modal from '../../../Modal';
var sdk = require('../../../index'); import sdk from '../../../index';
var dis = require("../../../dispatcher"); import dis from '../../../dispatcher';
var KeyCode = require("../../../KeyCode"); import KeyCode from '../../../KeyCode';
import * as RichText from '../../../RichText'; import * as RichText from '../../../RichText';
@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const KEY_M = 77; const KEY_M = 77;
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p> // FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
function mdownToHtml(mdown) { function mdownToHtml(mdown: string): string {
var html = marked(mdown) || ""; let html = marked(mdown) || "";
html = html.trim(); html = html.trim();
// strip start and end <p> tags else you get 'orrible spacing // strip start and end <p> tags else you get 'orrible spacing
if (html.indexOf("<p>") === 0) { if (html.indexOf("<p>") === 0) {
@ -66,6 +66,17 @@ function mdownToHtml(mdown) {
* The textInput part of the MessageComposer * The textInput part of the MessageComposer
*/ */
export default class MessageComposerInput extends React.Component { 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) { constructor(props, context) {
super(props, context); super(props, context);
this.onAction = this.onAction.bind(this); this.onAction = this.onAction.bind(this);
@ -79,7 +90,7 @@ export default class MessageComposerInput extends React.Component {
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
if(isRichtextEnabled == null) { if (isRichtextEnabled == null) {
isRichtextEnabled = 'true'; isRichtextEnabled = 'true';
} }
isRichtextEnabled = isRichtextEnabled === 'true'; isRichtextEnabled = isRichtextEnabled === 'true';
@ -95,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get(); 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: * "Does the right thing" to create an EditorState, based on:
* - whether we've got rich text mode enabled * - whether we've got rich text mode enabled
@ -347,15 +349,16 @@ export default class MessageComposerInput extends React.Component {
} }
setEditorState(editorState: EditorState) { setEditorState(editorState: EditorState) {
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
this.setState({editorState}); this.setState({editorState});
if(editorState.getCurrentContent().hasText()) { if (editorState.getCurrentContent().hasText()) {
this.onTypingActivity() this.onTypingActivity();
} else { } else {
this.onFinishedTyping(); this.onFinishedTyping();
} }
if(this.props.onContentChanged) { if (this.props.onContentChanged) {
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
RichText.selectionStateToTextOffsets(editorState.getSelection(), RichText.selectionStateToTextOffsets(editorState.getSelection(),
editorState.getCurrentContent().getBlocksAsArray())); editorState.getCurrentContent().getBlocksAsArray()));
@ -380,7 +383,7 @@ export default class MessageComposerInput extends React.Component {
} }
handleKeyCommand(command: string): boolean { handleKeyCommand(command: string): boolean {
if(command === 'toggle-mode') { if (command === 'toggle-mode') {
this.enableRichtext(!this.state.isRichtextEnabled); this.enableRichtext(!this.state.isRichtextEnabled);
return true; return true;
} }
@ -388,7 +391,7 @@ export default class MessageComposerInput extends React.Component {
let newState: ?EditorState = null; let newState: ?EditorState = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. // 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(), let contentState = this.state.editorState.getCurrentContent(),
selection = this.state.editorState.getSelection(); selection = this.state.editorState.getSelection();
@ -396,10 +399,10 @@ export default class MessageComposerInput extends React.Component {
bold: text => `**${text}**`, bold: text => `**${text}**`,
italic: text => `*${text}*`, italic: text => `*${text}*`,
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
code: text => `\`${text}\`` code: text => `\`${text}\``,
}[command]; }[command];
if(modifyFn) { if (modifyFn) {
newState = EditorState.push( newState = EditorState.push(
this.state.editorState, this.state.editorState,
RichText.modifyText(contentState, selection, modifyFn), 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); newState = RichUtils.handleKeyCommand(this.state.editorState, command);
if (newState != null) { if (newState != null) {
@ -423,12 +426,6 @@ export default class MessageComposerInput extends React.Component {
return false; return false;
} }
if(this.props.tryComplete) {
if(this.props.tryComplete()) {
return true;
}
}
const contentState = this.state.editorState.getCurrentContent(); const contentState = this.state.editorState.getCurrentContent();
if (!contentState.hasText()) { if (!contentState.hasText()) {
return true; return true;
@ -503,24 +500,20 @@ export default class MessageComposerInput extends React.Component {
} }
onUpArrow(e) { onUpArrow(e) {
if(this.props.onUpArrow) { if (this.props.onUpArrow && this.props.onUpArrow()) {
if(this.props.onUpArrow()) { e.preventDefault();
e.preventDefault();
}
} }
} }
onDownArrow(e) { onDownArrow(e) {
if(this.props.onDownArrow) { if (this.props.onDownArrow && this.props.onDownArrow()) {
if(this.props.onDownArrow()) { e.preventDefault();
e.preventDefault();
}
} }
} }
onTab(e) { onTab(e) {
if (this.props.onTab) { if (this.props.tryComplete) {
if (this.props.onTab()) { if (this.props.tryComplete()) {
e.preventDefault(); e.preventDefault();
} }
} }
@ -533,9 +526,11 @@ export default class MessageComposerInput extends React.Component {
content content
); );
this.setState({ let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
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 :( // for some reason, doing this right away does not update the editor :(
setTimeout(() => this.refs.editor.focus(), 50); setTimeout(() => this.refs.editor.focus(), 50);
@ -585,5 +580,6 @@ MessageComposerInput.propTypes = {
onDownArrow: React.PropTypes.func, onDownArrow: React.PropTypes.func,
onTab: React.PropTypes.func // attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,
}; };

View File

@ -268,9 +268,11 @@ module.exports = React.createClass({
}, },
_repositionTooltip: function(e) { _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); 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";
} }
}, },

View File

@ -21,6 +21,7 @@ var classNames = require('classnames');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils'; import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({ module.exports = React.createClass({
@ -43,16 +44,48 @@ module.exports = React.createClass({
}, },
getInitialState: function() { 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({ return({
hover : false, hover : false,
badgeHover : 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() { onClick: function() {
dis.dispatch({ dis.dispatch({
action: 'view_room', 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() { 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() { badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } ); 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() { render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId]; var me = this.props.room.currentState.members[myUserId];
@ -84,60 +151,63 @@ module.exports = React.createClass({
'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0, 'mx_RoomTile_unreadNotify': notificationCount > 0,
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': (me && me.membership == 'invite'), '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 // XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k) // room name js sdk gives is undefined (cannot repro this -- k)
var name = this.props.room.name || this.props.room.roomId; 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 name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge; var badge;
var badgeContent; var badgeContent;
var badgeClasses;
if (this.state.badgeHover) { if (this.state.badgeHover || this.state.menu) {
badgeContent = "\u00B7\u00B7\u00B7"; badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || notificationCount > 0) { } else if (this.props.highlight || notificationCount > 0) {
badgeContent = notificationCount ? notificationCount : '!'; var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!';
} else { } else {
badgeContent = '\u200B'; badgeContent = '\u200B';
} }
if (this.props.highlight || notificationCount > 0) { if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
badgeClasses = "mx_RoomTile_badge"; 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 { } 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 label;
var tooltip;
if (!this.props.collapsed) { 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); let nameHTML = emojifyText(name);
if (this.props.selected) { if (this.props.selected) {
name = <span dangerouslySetInnerHTML={nameHTML}></span>; let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div className={ className }>{ name }</div>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
} else { } 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) { else if (this.state.hover) {
@ -160,13 +230,16 @@ module.exports = React.createClass({
var connectDropTarget = this.props.connectDropTarget; var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget( return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar"> <div className={avatarClasses}>
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div> </div>
{ label }
{ badge }
{ incomingCallBox } { incomingCallBox }
{ tooltip }
</div> </div>
)); ));
} }

View File

@ -111,7 +111,9 @@ export default class DevicesPanelEntry extends React.Component {
<div className="mx_DevicesPanel_device"> <div className="mx_DevicesPanel_device">
<div className="mx_DevicesPanel_deviceName"> <div className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name} <EditableTextContainer initialValue={device.display_name}
onSubmit={this._onDisplayNameChanged} /> onSubmit={this._onDisplayNameChanged}
placeholder={device.device_id}
/>
</div> </div>
<div className="mx_DevicesPanel_lastSeen"> <div className="mx_DevicesPanel_lastSeen">
{lastSeen} {lastSeen}

View File

@ -224,7 +224,7 @@ describe('TimelinePanel', function() {
var scrollDefer; var scrollDefer;
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />, <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} />,
parentDiv parentDiv
); );
console.log("TimelinePanel rendered"); console.log("TimelinePanel rendered");
@ -238,17 +238,29 @@ describe('TimelinePanel', function() {
// the TimelinePanel fires a scroll event // the TimelinePanel fires a scroll event
var awaitScroll = function() { var awaitScroll = function() {
scrollDefer = q.defer(); 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() { function backPaginate() {
scrollingDiv.scrollTop = 0; console.log("back paginating...");
setScrollTop(0);
return awaitScroll().then(() => { return awaitScroll().then(() => {
if(scrollingDiv.scrollTop > 0) { if(scrollingDiv.scrollTop > 0) {
// need to go further // need to go further
return backPaginate(); return backPaginate();
} }
console.log("paginated to end."); console.log("paginated to start.");
// hopefully, we got to the start of the timeline // hopefully, we got to the start of the timeline
expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.backPaginating).toBe(false);
@ -262,7 +274,6 @@ describe('TimelinePanel', function() {
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
// back-paginate until we hit the start // back-paginate until we hit the start
console.log("back paginating...");
return backPaginate(); return backPaginate();
}).then(() => { }).then(() => {
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); 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 // we should now be able to scroll down, and paginate in the other
// direction. // direction.
console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop); setScrollTop(scrollingDiv.scrollHeight);
console.log("Going to set it to " + scrollingDiv.scrollHeight);
scrollingDiv.scrollTop = scrollingDiv.scrollHeight; scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
return awaitScroll(); return awaitScroll();
}).then(() => { }).then(() => {