merge in develop
This commit is contained in:
commit
90e5ab2ca3
90 changed files with 3640 additions and 1145 deletions
164
src/components/views/rooms/Autocomplete.js
Normal file
164
src/components/views/rooms/Autocomplete.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
|
||||
this.state = {
|
||||
// list of completionResults, each containing completions
|
||||
completions: [],
|
||||
|
||||
// array of completions, so we can look up current selection by offset quickly
|
||||
completionList: [],
|
||||
|
||||
// how far down the completion list we are
|
||||
selectionOffset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props, state) {
|
||||
if (props.query === this.props.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
getCompletions(props.query, props.selection).forEach(completionResult => {
|
||||
try {
|
||||
completionResult.completions.then(completions => {
|
||||
let i = this.state.completions.findIndex(
|
||||
completion => completion.provider === completionResult.provider
|
||||
);
|
||||
|
||||
i = i === -1 ? this.state.completions.length : i;
|
||||
let newCompletions = Object.assign([], this.state.completions);
|
||||
completionResult.completions = completions;
|
||||
newCompletions[i] = completionResult;
|
||||
|
||||
this.setState({
|
||||
completions: newCompletions,
|
||||
completionList: flatMap(newCompletions, provider => provider.completions),
|
||||
});
|
||||
}, err => {
|
||||
console.error(err);
|
||||
});
|
||||
} catch (e) {
|
||||
// An error in one provider shouldn't mess up the rest.
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
countCompletions(): number {
|
||||
return this.state.completions.map(completionResult => {
|
||||
return completionResult.completions.length;
|
||||
}).reduce((l, r) => l + r);
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onUpArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onDownArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** called from MessageComposerInput
|
||||
* @returns {boolean} whether confirmation was handled
|
||||
*/
|
||||
onConfirm(): boolean {
|
||||
if (this.countCompletions() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
|
||||
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset});
|
||||
}
|
||||
|
||||
render() {
|
||||
let position = 0;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
let className = classNames('mx_Autocomplete_Completion', {
|
||||
'selected': position === this.state.selectionOffset,
|
||||
});
|
||||
let componentPosition = position;
|
||||
position++;
|
||||
|
||||
let onMouseOver = () => this.setSelection(componentPosition);
|
||||
let onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={i}
|
||||
className={className}
|
||||
onMouseOver={onMouseOver}
|
||||
onClick={onClick}>
|
||||
{completion.component}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
|
||||
<ReactCSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="autocomplete"
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}>
|
||||
{completions}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_Autocomplete">
|
||||
<ReactCSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="autocomplete"
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}>
|
||||
{renderedCompletions}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Autocomplete.propTypes = {
|
||||
// the query string for which to show autocomplete suggestions
|
||||
query: React.PropTypes.string.isRequired,
|
||||
|
||||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: React.PropTypes.func.isRequired,
|
||||
};
|
|
@ -20,6 +20,7 @@ var React = require('react');
|
|||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
|
||||
var PRESENCE_CLASS = {
|
||||
|
@ -28,6 +29,23 @@ var PRESENCE_CLASS = {
|
|||
"unavailable": "mx_EntityTile_unavailable"
|
||||
};
|
||||
|
||||
|
||||
function presenceClassForMember(presenceState, lastActiveAgo) {
|
||||
// offline is split into two categories depending on whether we have
|
||||
// a last_active_ago for them.
|
||||
if (presenceState == 'offline') {
|
||||
if (lastActiveAgo) {
|
||||
return PRESENCE_CLASS['offline'] + '_beenactive';
|
||||
} else {
|
||||
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||
}
|
||||
} else if (presenceState) {
|
||||
return PRESENCE_CLASS[presenceState];
|
||||
} else {
|
||||
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EntityTile',
|
||||
|
||||
|
@ -78,10 +96,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline";
|
||||
const presenceClass = presenceClassForMember(
|
||||
this.props.presenceState, this.props.presenceLastActiveAgo
|
||||
);
|
||||
|
||||
var mainClassName = "mx_EntityTile ";
|
||||
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
|
||||
var nameEl;
|
||||
let nameHTML = emojifyText(this.props.name);
|
||||
|
||||
if (this.state.hover && !this.props.suppressOnHover) {
|
||||
var activeAgo = this.props.presenceLastActiveAgo ?
|
||||
|
@ -92,7 +114,7 @@ module.exports = React.createClass({
|
|||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
|
||||
<div className="mx_EntityTile_name_hover" dangerouslySetInnerHTML={nameHTML}></div>
|
||||
<PresenceLabel activeAgo={ activeAgo }
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
|
@ -101,8 +123,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
else {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_name">
|
||||
{ this.props.name }
|
||||
<div className="mx_EntityTile_name" dangerouslySetInnerHTML={nameHTML}>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
@ -101,6 +101,9 @@ module.exports = React.createClass({
|
|||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
/* is this the focused event */
|
||||
isSelectedEvent: React.PropTypes.bool,
|
||||
|
||||
|
@ -139,7 +142,8 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
|
@ -163,11 +167,12 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
client.removeListener("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.mxEvent.getSender()) {
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
|
@ -244,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,
|
||||
|
@ -357,6 +365,8 @@ module.exports = React.createClass({
|
|||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
||||
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
var content = this.props.mxEvent.getContent();
|
||||
var msgtype = content.msgtype;
|
||||
|
||||
|
@ -418,6 +428,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,17 +37,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
|
||||
this._emailEntity = null;
|
||||
// Load the complete user list for inviting new users
|
||||
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
|
||||
// it will do for now not being updated as random new users join different
|
||||
// rooms as this list will be reloaded every room swap.
|
||||
if (this._room) {
|
||||
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
||||
return !this._room.hasMembershipState(u.userId, "join");
|
||||
});
|
||||
}
|
||||
|
||||
// we have to update the list whenever membership changes
|
||||
// particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -55,6 +52,28 @@ module.exports = React.createClass({
|
|||
this.onSearchQueryChanged('');
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
},
|
||||
|
||||
_updateList: function() {
|
||||
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
// Load the complete user list for inviting new users
|
||||
if (this._room) {
|
||||
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
||||
return (!this._room.hasMembershipState(u.userId, "join") &&
|
||||
!this._room.hasMembershipState(u.userId, "invite"));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onInvite: function(ev) {
|
||||
this.props.onInvite(this._input);
|
||||
},
|
||||
|
|
|
@ -36,32 +36,75 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
onBlockClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.id, true
|
||||
);
|
||||
},
|
||||
|
||||
onUnblockClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.id, false
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var indicator = null, button = null;
|
||||
if (this.props.device.verified) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">✔</div>
|
||||
var indicator = null, blockButton = null, verifyButton = null;
|
||||
if (this.props.device.blocked) {
|
||||
blockButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
|
||||
onClick={this.onUnblockClick}>
|
||||
Unblock
|
||||
</div>
|
||||
);
|
||||
button = (
|
||||
} else {
|
||||
blockButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
|
||||
onClick={this.onBlockClick}>
|
||||
Block
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.device.verified) {
|
||||
verifyButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
Unverify
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
verifyButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
Verify
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.device.blocked) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_blocked">✖</div>
|
||||
);
|
||||
} else if (this.props.device.verified) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">✔</div>
|
||||
);
|
||||
|
||||
} else {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_unverified">?</div>
|
||||
);
|
||||
}
|
||||
|
||||
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_deviceKey">{this.props.device.key}</div>
|
||||
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
|
||||
{indicator}
|
||||
{button}
|
||||
{verifyButton}
|
||||
{blockButton}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ var Modal = require("../../../Modal");
|
|||
var sdk = require('../../../index');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
var createRoom = require('../../../createRoom');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -60,17 +61,21 @@ module.exports = React.createClass({
|
|||
updating: 0,
|
||||
devicesLoading: true,
|
||||
devices: null,
|
||||
existingOneToOneRoomId: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
|
||||
this.setState({
|
||||
existingOneToOneRoomId: this.getExistingOneToOneRoomId()
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._updateStateForNewMember(this.props.member);
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
|
@ -82,14 +87,67 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
if (this._cancelDeviceList) {
|
||||
this._cancelDeviceList();
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
getExistingOneToOneRoomId: function() {
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const userIds = [
|
||||
this.props.member.userId,
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
];
|
||||
let existingRoomId = null;
|
||||
let invitedRoomId = null;
|
||||
|
||||
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
|
||||
// abuse this to view users rather than room members.
|
||||
let currentMembers;
|
||||
if (this.props.member.roomId) {
|
||||
const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
||||
currentMembers = currentRoom.getJoinedMembers();
|
||||
}
|
||||
|
||||
// reuse the first private 1:1 we find
|
||||
existingRoomId = null;
|
||||
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
// don't try to reuse public 1:1 rooms
|
||||
const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
|
||||
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
|
||||
|
||||
const members = rooms[i].getJoinedMembers();
|
||||
if (members.length === 2 &&
|
||||
userIds.indexOf(members[0].userId) !== -1 &&
|
||||
userIds.indexOf(members[1].userId) !== -1)
|
||||
{
|
||||
existingRoomId = rooms[i].roomId;
|
||||
break;
|
||||
}
|
||||
|
||||
const invited = rooms[i].getMembersWithMembership('invite');
|
||||
if (members.length === 1 &&
|
||||
invited.length === 1 &&
|
||||
userIds.indexOf(members[0].userId) !== -1 &&
|
||||
userIds.indexOf(invited[0].userId) !== -1 &&
|
||||
invitedRoomId === null)
|
||||
{
|
||||
invitedRoomId = rooms[i].roomId;
|
||||
// keep looking: we'll use this one if there's nothing better
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRoomId === null) {
|
||||
existingRoomId = invitedRoomId;
|
||||
}
|
||||
|
||||
return existingRoomId;
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.member.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
|
@ -348,61 +406,29 @@ module.exports = React.createClass({
|
|||
|
||||
onChatClick: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
|
||||
|
||||
const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId);
|
||||
|
||||
// check if there are any existing rooms with just us and them (1:1)
|
||||
// If so, just view that room. If not, create a private room with them.
|
||||
var self = this;
|
||||
var rooms = MatrixClientPeg.get().getRooms();
|
||||
var userIds = [
|
||||
this.props.member.userId,
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
];
|
||||
var existingRoomId;
|
||||
|
||||
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
||||
var currentMembers = currentRoom.getJoinedMembers();
|
||||
// if we're currently in a 1:1 with this user, start a new chat
|
||||
if (currentMembers.length === 2 &&
|
||||
userIds.indexOf(currentMembers[0].userId) !== -1 &&
|
||||
userIds.indexOf(currentMembers[1].userId) !== -1)
|
||||
{
|
||||
existingRoomId = null;
|
||||
}
|
||||
// otherwise reuse the first private 1:1 we find
|
||||
else {
|
||||
existingRoomId = null;
|
||||
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
// don't try to reuse public 1:1 rooms
|
||||
var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
|
||||
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
|
||||
|
||||
var members = rooms[i].getJoinedMembers();
|
||||
if (members.length === 2 &&
|
||||
userIds.indexOf(members[0].userId) !== -1 &&
|
||||
userIds.indexOf(members[1].userId) !== -1)
|
||||
{
|
||||
existingRoomId = rooms[i].roomId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRoomId) {
|
||||
if (useExistingOneToOneRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: existingRoomId
|
||||
room_id: this.state.existingOneToOneRoomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
}
|
||||
else {
|
||||
self.setState({ updating: self.state.updating + 1 });
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
createRoom({
|
||||
createOpts: {
|
||||
invite: [this.props.member.userId],
|
||||
},
|
||||
}).finally(function() {
|
||||
self.props.onFinished();
|
||||
self.setState({ updating: self.state.updating - 1 });
|
||||
}).finally(() => {
|
||||
this.props.onFinished();
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
@ -535,7 +561,9 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
{devComponents}
|
||||
<div className="mx_MemberInfo_devices">
|
||||
{devComponents}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -545,7 +573,22 @@ module.exports = React.createClass({
|
|||
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||
// FIXME: we're referring to a vector component from react-sdk
|
||||
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
|
||||
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
|
||||
|
||||
var label;
|
||||
if (this.state.existingOneToOneRoomId) {
|
||||
if (this.state.existingOneToOneRoomId == this.props.member.roomId) {
|
||||
label = "Start new direct chat";
|
||||
}
|
||||
else {
|
||||
label = "Go to direct chat";
|
||||
}
|
||||
}
|
||||
else {
|
||||
label = "Start direct chat";
|
||||
}
|
||||
|
||||
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg"
|
||||
label={ label } onClick={ this.onChatClick }/>
|
||||
}
|
||||
|
||||
if (this.state.updating) {
|
||||
|
@ -594,6 +637,8 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
}
|
||||
|
||||
let memberNameHTML = emojifyText(this.props.member.name);
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
|
@ -603,7 +648,7 @@ module.exports = React.createClass({
|
|||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
||||
<h2>{ this.props.member.name }</h2>
|
||||
<h2 dangerouslySetInnerHTML={memberNameHTML}></h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
|
|
@ -54,7 +54,7 @@ module.exports = React.createClass({
|
|||
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
||||
state.members = this.roomMembers();
|
||||
return state;
|
||||
},
|
||||
|
||||
|
@ -64,7 +64,10 @@ module.exports = React.createClass({
|
|||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
cli.on("Room", this.onRoom); // invites
|
||||
cli.on("User.presence", this.onUserPresence);
|
||||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
||||
|
@ -75,24 +78,11 @@ module.exports = React.createClass({
|
|||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.presence", this.onUserPresence);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
|
||||
// Lazy-load in more than the first N members
|
||||
setTimeout(function() {
|
||||
if (!self.isMounted()) return;
|
||||
// lazy load to prevent it blocking the first render
|
||||
self.setState({
|
||||
members: self.roomMembers()
|
||||
});
|
||||
}, 50);
|
||||
},
|
||||
|
||||
/*
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
|
@ -121,7 +111,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
*/
|
||||
|
||||
onUserPresence(event, user) {
|
||||
onUserLastPresenceTs(event, user) {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
|
@ -325,7 +315,7 @@ module.exports = React.createClass({
|
|||
return all_members;
|
||||
},
|
||||
|
||||
roomMembers: function(limit) {
|
||||
roomMembers: function() {
|
||||
var all_members = this.memberDict || {};
|
||||
var all_user_ids = Object.keys(all_members);
|
||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
@ -334,7 +324,7 @@ module.exports = React.createClass({
|
|||
|
||||
var to_display = [];
|
||||
var count = 0;
|
||||
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
|
||||
for (var i = 0; i < all_user_ids.length; ++i) {
|
||||
var user_id = all_user_ids[i];
|
||||
var m = all_members[user_id];
|
||||
|
||||
|
@ -425,27 +415,7 @@ module.exports = React.createClass({
|
|||
|
||||
// For now, let's just order things by timestamp. It's really annoying
|
||||
// that a user disappears from sight just because they temporarily go offline
|
||||
/*
|
||||
var presenceMap = {
|
||||
online: 3,
|
||||
unavailable: 2,
|
||||
offline: 1
|
||||
};
|
||||
|
||||
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
|
||||
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
|
||||
|
||||
if (presenceOrdA != presenceOrdB) {
|
||||
return presenceOrdB - presenceOrdA;
|
||||
}
|
||||
*/
|
||||
|
||||
var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0;
|
||||
var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0;
|
||||
|
||||
// console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB);
|
||||
|
||||
return lastActiveTsB - lastActiveTsA;
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(input) {
|
||||
|
@ -462,9 +432,16 @@ module.exports = React.createClass({
|
|||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
if (query && m.name.toLowerCase().indexOf(query) === -1) {
|
||||
return false;
|
||||
|
||||
if (query) {
|
||||
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
|
||||
|
||||
if (!matchesName && !matchesId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return m.membership == membership;
|
||||
}).map(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
|
@ -515,7 +492,7 @@ module.exports = React.createClass({
|
|||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>Invited</h2>
|
||||
<div autoshow={true} className="mx_MemberList_wrapper">
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{invitedMemberTiles}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -544,7 +521,6 @@ module.exports = React.createClass({
|
|||
<div className="mx_MemberList">
|
||||
{inviteMemberListSection}
|
||||
<GeminiScrollbar autoshow={true}
|
||||
relayoutOnUpdate={false}
|
||||
className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
|
|
|
@ -20,54 +20,53 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
|
|||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this._tryComplete = this._tryComplete.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any,
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
selection: null,
|
||||
};
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
}
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: React.PropTypes.string,
|
||||
|
||||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
onUploadClick: function(ev) {
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't upload files. Please register to upload."
|
||||
description: "Guest users can't upload files. Please register to upload.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs.uploadInput.click();
|
||||
},
|
||||
}
|
||||
|
||||
onUploadFileSelected: function(ev) {
|
||||
var files = ev.target.files;
|
||||
onUploadFileSelected(ev) {
|
||||
let files = ev.target.files;
|
||||
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileList = [];
|
||||
for(var i=0; i<files.length; i++) {
|
||||
let fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
|
||||
</li>);
|
||||
|
@ -94,11 +93,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this.refs.uploadInput.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onHangupClick: function() {
|
||||
onHangupClick() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
|
@ -108,27 +107,55 @@ module.exports = React.createClass({
|
|||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId
|
||||
room_id: call.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onCallClick: function(ev) {
|
||||
onCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onVoiceCallClick: function(ev) {
|
||||
onVoiceCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
onUpArrow() {
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
}
|
||||
|
||||
onDownArrow() {
|
||||
return this.refs.autocomplete.onDownArrow();
|
||||
}
|
||||
|
||||
_tryComplete(): boolean {
|
||||
if (this.refs.autocomplete) {
|
||||
return this.refs.autocomplete.onConfirm();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_onAutocompleteConfirm(range, completion) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.onConfirmAutocompletion(range, completion);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
@ -154,12 +181,12 @@ module.exports = React.createClass({
|
|||
else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
|
||||
<TintableSvg src="img/voice.svg" width="16" height="26"/>
|
||||
</div>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
videoCallButton =
|
||||
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
|
||||
<TintableSvg src="img/call.svg" width="30" height="22"/>
|
||||
</div>
|
||||
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
|
@ -172,7 +199,7 @@ module.exports = React.createClass({
|
|||
var uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title="Upload file">
|
||||
<TintableSvg src="img/upload.svg" width="19" height="24"/>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
|
@ -181,8 +208,16 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
|
||||
onResize={this.props.onResize} room={this.props.room} />,
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
tryComplete={this._tryComplete}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||
onContentChanged={this.onInputContentChanged} />,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
|
@ -198,6 +233,13 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
onConfirm={this._onAutocompleteConfirm}
|
||||
query={this.state.autocompleteQuery}
|
||||
selection={this.state.selection} />
|
||||
</div>
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{controls}
|
||||
|
@ -206,5 +248,24 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
MessageComposer.propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: React.PropTypes.string,
|
||||
|
||||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number
|
||||
};
|
||||
|
|
|
@ -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,23 +66,38 @@ 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);
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.setEditorState = this.setEditorState.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this.onTab = this.onTab.bind(this);
|
||||
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';
|
||||
|
||||
this.state = {
|
||||
isRichtextEnabled: isRichtextEnabled,
|
||||
editorState: null
|
||||
editorState: null,
|
||||
};
|
||||
|
||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||
|
@ -91,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
|
||||
|
@ -207,11 +213,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
|
||||
if (contentJSON) {
|
||||
let content = convertFromRaw(JSON.parse(contentJSON));
|
||||
component.setState({
|
||||
editorState: component.createEditorState(component.state.isRichtextEnabled, content)
|
||||
});
|
||||
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -233,7 +237,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
onAction(payload) {
|
||||
var editor = this.refs.editor;
|
||||
let editor = this.refs.editor;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
|
@ -251,7 +255,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
payload.displayname
|
||||
);
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
||||
});
|
||||
editor.focus();
|
||||
}
|
||||
|
@ -344,28 +348,31 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.refs.editor.focus();
|
||||
}
|
||||
|
||||
onChange(editorState: EditorState) {
|
||||
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) {
|
||||
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
|
||||
RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||
editorState.getCurrentContent().getBlocksAsArray()));
|
||||
}
|
||||
}
|
||||
|
||||
enableRichtext(enabled: boolean) {
|
||||
if (enabled) {
|
||||
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html))
|
||||
});
|
||||
this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html)));
|
||||
} else {
|
||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
|
||||
contentState = ContentState.createFromText(markdown);
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, contentState)
|
||||
});
|
||||
this.setEditorState(this.createEditorState(enabled, contentState));
|
||||
}
|
||||
|
||||
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
|
||||
|
@ -376,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;
|
||||
}
|
||||
|
@ -384,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();
|
||||
|
||||
|
@ -392,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),
|
||||
|
@ -404,23 +411,26 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if(newState == null)
|
||||
if (newState == null)
|
||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||
|
||||
if (newState != null) {
|
||||
this.onChange(newState);
|
||||
this.setEditorState(newState);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if(ev.shiftKey)
|
||||
if (ev.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentState = this.state.editorState.getCurrentContent();
|
||||
if(!contentState.hasText())
|
||||
if (!contentState.hasText()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
let contentText = contentState.getPlainText(), contentHTML;
|
||||
|
||||
|
@ -489,10 +499,47 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
onUpArrow(e) {
|
||||
if (this.props.onUpArrow && this.props.onUpArrow()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onDownArrow(e) {
|
||||
if (this.props.onDownArrow && this.props.onDownArrow()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onTab(e) {
|
||||
if (this.props.tryComplete) {
|
||||
if (this.props.tryComplete()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmAutocompletion(range, content: string) {
|
||||
let contentState = Modifier.replaceText(
|
||||
this.state.editorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
|
||||
content
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = "mx_MessageComposer_input";
|
||||
|
||||
if(this.state.isRichtextEnabled) {
|
||||
if (this.state.isRichtextEnabled) {
|
||||
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
|
||||
}
|
||||
|
||||
|
@ -502,11 +549,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onChange}
|
||||
onChange={this.setEditorState}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
spellCheck={true} />
|
||||
</div>
|
||||
);
|
||||
|
@ -521,5 +571,15 @@ MessageComposerInput.propTypes = {
|
|||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// called with current plaintext content (as a string) whenever it changes
|
||||
onContentChanged: React.PropTypes.func,
|
||||
|
||||
onUpArrow: React.PropTypes.func,
|
||||
|
||||
onDownArrow: React.PropTypes.func,
|
||||
|
||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||
tryComplete: React.PropTypes.func,
|
||||
};
|
||||
|
|
|
@ -163,13 +163,13 @@ module.exports = React.createClass({
|
|||
};
|
||||
|
||||
return (
|
||||
<Velociraptor>
|
||||
<Velociraptor
|
||||
startStyles={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
startStyle={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
</Velociraptor>
|
||||
|
|
|
@ -24,6 +24,7 @@ var Modal = require("../../../Modal");
|
|||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -211,13 +212,12 @@ module.exports = React.createClass({
|
|||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
let roomNameHTML = emojifyText(roomName);
|
||||
|
||||
name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{ roomName }</div>
|
||||
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName } dangerouslySetInnerHTML={roomNameHTML}></div>
|
||||
{ searchStatus }
|
||||
<div className="mx_RoomHeader_settingsButton" title="Settings">
|
||||
<TintableSvg src="img/settings.svg" width="12" height="12"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -263,10 +263,18 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
settings_button =
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var leave_button;
|
||||
if (this.props.onLeaveClick) {
|
||||
leave_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onLeaveClick} title="Leave room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
}
|
||||
|
@ -274,7 +282,7 @@ module.exports = React.createClass({
|
|||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
}
|
||||
|
@ -288,10 +296,11 @@ module.exports = React.createClass({
|
|||
if (!this.props.editing) {
|
||||
right_row =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settings_button }
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||
</div>
|
||||
{ rightPanel_buttons }
|
||||
</div>;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -325,7 +327,6 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
relayoutOnUpdate={false}
|
||||
autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
|
|
|
@ -33,16 +33,24 @@ module.exports = React.createClass({
|
|||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail: React.PropTypes.string,
|
||||
canJoin: React.PropTypes.bool,
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error: React.PropTypes.object,
|
||||
|
||||
canPreview: React.PropTypes.bool,
|
||||
spinner: React.PropTypes.bool,
|
||||
room: React.PropTypes.object,
|
||||
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onJoinClick: function() {},
|
||||
canJoin: false,
|
||||
canPreview: true,
|
||||
};
|
||||
},
|
||||
|
@ -115,8 +123,24 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
}
|
||||
else if (this.props.canJoin) {
|
||||
var name = this.props.room ? this.props.room.name : "";
|
||||
else if (this.props.error) {
|
||||
var name = this.props.roomAlias || "This room";
|
||||
var error;
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
error = name + " does not exist";
|
||||
} else {
|
||||
error = name + " is not accessible at this time";
|
||||
}
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
var name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
name = name ? <b>{ name }</b> : "a room";
|
||||
joinBlock = (
|
||||
<div>
|
||||
|
|
|
@ -23,6 +23,14 @@ var Modal = require('../../../Modal');
|
|||
var ObjectUtils = require("../../../ObjectUtils");
|
||||
var dis = require("../../../dispatcher");
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||
// as an integer, return a default.
|
||||
function parseIntWithDefault(val, def) {
|
||||
var res = parseInt(val);
|
||||
return isNaN(res) ? def : res;
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomSettings',
|
||||
|
@ -59,9 +67,18 @@ module.exports = React.createClass({
|
|||
tags_changed: false,
|
||||
tags: tags,
|
||||
areNotifsMuted: areNotifsMuted,
|
||||
<<<<<<< HEAD
|
||||
isRoomPublished: this._originalIsRoomPublished, // loaded async in componentWillMount
|
||||
scalar_token: null,
|
||||
scalar_error: null,
|
||||
=======
|
||||
// isRoomPublished is loaded async in componentWillMount so when the component
|
||||
// inits, the saved value will always be undefined, however getInitialState()
|
||||
// is also called from the saving code so we must return the correct value here
|
||||
// if we have it (although this could race if the user saves before we load whether
|
||||
// the room is published or not).
|
||||
isRoomPublished: this._originalIsRoomPublished,
|
||||
>>>>>>> develop
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -209,11 +226,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
});
|
||||
}
|
||||
console.log("Performing %s operations", promises.length);
|
||||
|
||||
// color scheme
|
||||
promises.push(this.saveColor());
|
||||
|
||||
// url preview settings
|
||||
promises.push(this.saveUrlPreviewSettings());
|
||||
|
||||
// encryption
|
||||
promises.push(this.saveEncryption());
|
||||
|
||||
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
||||
return q.allSettled(promises);
|
||||
},
|
||||
|
||||
|
@ -227,6 +250,24 @@ module.exports = React.createClass({
|
|||
return this.refs.color_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveUrlPreviewSettings: function() {
|
||||
if (!this.refs.url_preview_settings) { return q(); }
|
||||
return this.refs.url_preview_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveEncryption: function () {
|
||||
if (!this.refs.encrypt) { return q(); }
|
||||
|
||||
var encrypt = this.refs.encrypt.checked;
|
||||
if (!encrypt) { return q(); }
|
||||
|
||||
var roomId = this.props.room.roomId;
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
roomId, "m.room.encryption",
|
||||
{ algorithm: "m.olm.v1.curve25519-aes-sha2" }
|
||||
);
|
||||
},
|
||||
|
||||
_hasDiff: function(strA, strB) {
|
||||
// treat undefined as an empty string because other components may blindly
|
||||
// call setName("") when there has been no diff made to the name!
|
||||
|
@ -261,7 +302,7 @@ module.exports = React.createClass({
|
|||
power_levels_changed: true
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
|
||||
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
|
||||
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
|
||||
|
@ -296,7 +337,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_onRoomAccessRadioToggle: function(ev) {
|
||||
|
||||
// join_rule
|
||||
|
@ -400,68 +441,72 @@ module.exports = React.createClass({
|
|||
}, "");
|
||||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
|
||||
var text = "Encryption is " + (isEncrypted ? "" : "not ") +
|
||||
"enabled in this room.";
|
||||
|
||||
var button;
|
||||
if (!isEncrypted &&
|
||||
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||
button = (
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" />
|
||||
Enable encryption (warning: cannot be disabled again!)
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>Encryption</h3>
|
||||
<label>{text}</label>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// TODO: go through greying out things you don't have permission to change
|
||||
// (or turning them into informative stuff)
|
||||
|
||||
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
var Loader = sdk.getComponent("elements.Spinner")
|
||||
|
||||
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
var events_levels = (power_levels ? power_levels.getContent().events : {}) || {};
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var user_id = cli.credentials.userId;
|
||||
|
||||
if (power_levels) {
|
||||
power_levels = power_levels.getContent();
|
||||
var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
|
||||
var power_levels = power_level_event ? power_level_event.getContent() : {};
|
||||
var events_levels = power_levels.events || {};
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var ban_level = parseInt(power_levels.ban);
|
||||
var kick_level = parseInt(power_levels.kick);
|
||||
var redact_level = parseInt(power_levels.redact);
|
||||
var invite_level = parseInt(power_levels.invite || 0);
|
||||
var send_level = parseInt(power_levels.events_default || 0);
|
||||
var state_level = parseInt(power_levels.state_default || 50);
|
||||
var default_user_level = parseInt(power_levels.users_default || 0);
|
||||
var ban_level = parseIntWithDefault(power_levels.ban, 50);
|
||||
var kick_level = parseIntWithDefault(power_levels.kick, 50);
|
||||
var redact_level = parseIntWithDefault(power_levels.redact, 50);
|
||||
var invite_level = parseIntWithDefault(power_levels.invite, 50);
|
||||
var send_level = parseIntWithDefault(power_levels.events_default, 0);
|
||||
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
|
||||
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
|
||||
|
||||
if (power_levels.ban == undefined) ban_level = 50;
|
||||
if (power_levels.kick == undefined) kick_level = 50;
|
||||
if (power_levels.redact == undefined) redact_level = 50;
|
||||
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var current_user_level = user_levels[user_id];
|
||||
if (current_user_level == undefined) current_user_level = default_user_level;
|
||||
|
||||
var power_level_level = events_levels["m.room.power_levels"];
|
||||
if (power_level_level == undefined) {
|
||||
power_level_level = state_level;
|
||||
}
|
||||
|
||||
var can_change_levels = current_user_level >= power_level_level;
|
||||
} else {
|
||||
var ban_level = 50;
|
||||
var kick_level = 50;
|
||||
var redact_level = 50;
|
||||
var invite_level = 0;
|
||||
var send_level = 0;
|
||||
var state_level = 0;
|
||||
var default_user_level = 0;
|
||||
|
||||
var user_levels = [];
|
||||
var events_levels = [];
|
||||
|
||||
var current_user_level = 0;
|
||||
|
||||
var power_level_level = 0;
|
||||
|
||||
var can_change_levels = false;
|
||||
var current_user_level = user_levels[user_id];
|
||||
if (current_user_level === undefined) {
|
||||
current_user_level = default_user_level;
|
||||
}
|
||||
|
||||
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
|
||||
var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
|
||||
|
||||
var canSetTag = !cli.isGuest();
|
||||
|
||||
|
@ -530,7 +575,7 @@ module.exports = React.createClass({
|
|||
|
||||
var tagsSection = null;
|
||||
if (canSetTag || self.state.tags) {
|
||||
var tagsSection =
|
||||
var tagsSection =
|
||||
<div className="mx_RoomSettings_tags">
|
||||
Tagged as: { canSetTag ?
|
||||
(tags.map(function(tag, i) {
|
||||
|
@ -666,10 +711,6 @@ module.exports = React.createClass({
|
|||
Members only (since they joined)
|
||||
</label>
|
||||
</div>
|
||||
<label className="mx_RoomSettings_encrypt">
|
||||
<input type="checkbox" />
|
||||
Encrypt room
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -690,6 +731,8 @@ module.exports = React.createClass({
|
|||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
||||
|
||||
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||
|
||||
<h3>Permissions</h3>
|
||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
|
@ -737,6 +780,8 @@ module.exports = React.createClass({
|
|||
|
||||
{ bannedUsersSection }
|
||||
|
||||
{ this._renderEncryptionSection() }
|
||||
|
||||
<h3>Advanced</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
This room's internal ID is <code>{ this.props.room.roomId }</code>
|
||||
|
|
|
@ -21,6 +21,8 @@ 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({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -42,13 +44,48 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return( { hover : false });
|
||||
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,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -60,6 +97,48 @@ module.exports = React.createClass({
|
|||
this.setState( { hover : false });
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// 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];
|
||||
|
@ -72,42 +151,64 @@ 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;
|
||||
if (this.props.highlight || notificationCount > 0) {
|
||||
badge = <div className="mx_RoomTile_badge">{ notificationCount ? notificationCount : '!' }</div>;
|
||||
var badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.menu) {
|
||||
badgeContent = "\u00B7\u00B7\u00B7";
|
||||
} else if (this.props.highlight || notificationCount > 0) {
|
||||
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
|
||||
badgeContent = notificationCount ? limitedCount : '!';
|
||||
} else {
|
||||
badgeContent = '\u200B';
|
||||
}
|
||||
/*
|
||||
if (this.props.highlight) {
|
||||
badge = <div className="mx_RoomTile_badge">!</div>;
|
||||
|
||||
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 {
|
||||
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</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>{ name }</span>;
|
||||
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
|
||||
|
||||
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
|
||||
}
|
||||
label = <div className={ className }>{ name }</div>;
|
||||
}
|
||||
else if (this.state.hover) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
|
@ -129,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>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -179,7 +179,6 @@ var SearchableEntityList = React.createClass({
|
|||
}
|
||||
list = (
|
||||
<GeminiScrollbar autoshow={true}
|
||||
relayoutOnUpdate={false}
|
||||
className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbar>
|
||||
|
|
|
@ -24,17 +24,17 @@ module.exports = React.createClass({
|
|||
displayName: 'TabCompleteBar',
|
||||
|
||||
propTypes: {
|
||||
entries: React.PropTypes.array.isRequired
|
||||
tabComplete: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
{this.props.tabComplete.peek(6).map((entry, i) => {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""}
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
{entry.getText()}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue