1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-10 09:22:25 +03:00

Merge branch 'develop' into dbkr/deactivate_account

This commit is contained in:
David Baker
2016-08-03 17:52:18 +01:00
16 changed files with 346 additions and 142 deletions

View File

@@ -0,0 +1,110 @@
/*
Copyright 2015, 2016 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.
*/
'use strict';
var classNames = require('classnames');
var React = require('react');
var ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
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);
if (!container) {
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function (Element, props) {
var self = this;
var closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) {
props.onFinished.apply(null, arguments);
}
};
var position = {
top: props.top,
};
var chevronOffset = {
top: props.chevronOffset,
}
var chevron = null;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
position.right = props.right;
}
var className = 'mx_ContextualMenu_wrapper';
var menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left,
'mx_ContextualMenu_right': !props.left,
});
var menuSize = {};
if (props.menuWidth) {
menuSize.width = props.menuWidth;
}
if (props.menuHeight) {
menuSize.height = props.menuHeight;
}
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
var menu = (
<div className={className} style={position}>
<div className={menuClasses} style={menuSize}>
{chevron}
<Element {...props} onFinished={closeMenu}/>
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
</div>
);
ReactDOM.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},
};

View File

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

View File

@@ -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;
}

View File

@@ -23,7 +23,7 @@ var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu');
var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
@@ -249,12 +249,15 @@ module.exports = React.createClass({
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu');
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2);
// The window X and Y offsets are to adjust position when zoomed in to page
var x = buttonRect.right + window.pageXOffset;
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
var self = this;
ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
left: x,
top: y,

View File

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

View File

@@ -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,

View File

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

View File

@@ -268,9 +268,11 @@ module.exports = React.createClass({
},
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
// We access the parent of the parent, as the tooltip is inside a container
// Needs refactoring into a better multipurpose tooltip
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
}
},

View File

@@ -21,6 +21,7 @@ var classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
@@ -43,16 +44,48 @@ module.exports = React.createClass({
},
getInitialState: function() {
var areNotifsMuted = false;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return({
hover : false,
badgeHover : false,
menu: false,
areNotifsMuted: areNotifsMuted,
});
},
onAction: function(payload) {
switch (payload.action) {
case 'notification_change':
// Is the notification about this room?
if (payload.roomId === this.props.room.roomId) {
this.setState( { areNotifsMuted : payload.isMuted });
}
break;
}
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
@@ -65,13 +98,47 @@ module.exports = React.createClass({
},
badgeOnMouseEnter: function() {
this.setState( { badgeHover : true } );
// Only allow none guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } );
},
onBadgeClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest()) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu');
var elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
var x = elementRect.right + window.pageXOffset + 3;
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
var self = this;
ContextualMenu.createMenu(Menu, {
menuWidth: 188,
menuHeight: 126,
chevronOffset: 45,
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ menu: false });
}
});
this.setState({ menu: true });
}
},
render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
@@ -84,60 +151,63 @@ module.exports = React.createClass({
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0,
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menu': this.state.menu,
});
var avatarClasses = classNames({
'mx_RoomTile_avatar': true,
'mx_RoomTile_mute': this.state.areNotifsMuted,
});
var badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu,
'mx_RoomTile_badgeMute': this.state.areNotifsMuted,
});
// XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k)
var name = this.props.room.name || this.props.room.roomId;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge;
var badgeContent;
var badgeClasses;
if (this.state.badgeHover) {
if (this.state.badgeHover || this.state.menu) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || notificationCount > 0) {
badgeContent = notificationCount ? notificationCount : '!';
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!';
} else {
badgeContent = '\u200B';
}
if (this.props.highlight || notificationCount > 0) {
badgeClasses = "mx_RoomTile_badge";
if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>;
} else {
badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread";
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
}
badge = <div className={ badgeClasses } onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
/*
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge">!</div>;
}
else if (this.props.unread) {
badge = <div className="mx_RoomTile_badge">1</div>;
}
var nameCell;
if (badge) {
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
}
else {
nameCell = <div className="mx_RoomTile_name">{name}</div>;
}
*/
var label;
var tooltip;
if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
var nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_mute': this.state.areNotifsMuted,
'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted,
});
let nameHTML = emojifyText(name);
if (this.props.selected) {
name = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div className={ className }>{ name }</div>;
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
} else {
label = <div className={ className } dangerouslySetInnerHTML={nameHTML}></div>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
}
}
else if (this.state.hover) {
@@ -160,13 +230,16 @@ module.exports = React.createClass({
var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} width={24} height={24} />
<div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ label }
{ badge }
{ incomingCallBox }
{ tooltip }
</div>
));
}

View File

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