Merge branch 'develop' into dbkr/deactivate_account
commit
498ad7fa4c
|
@ -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();
|
||||||
|
|
112
src/RichText.js
112
src/RichText.js
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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>
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
Loading…
Reference in New Issue