Merge branch 'develop' into wmwragg/direct-chat-sublist

This commit is contained in:
wmwragg
2016-08-30 11:19:54 +01:00
24 changed files with 707 additions and 130 deletions

View File

@@ -69,6 +69,7 @@ module.exports = React.createClass({
UserSettings: "user_settings",
CreateRoom: "create_room",
RoomDirectory: "room_directory",
UserView: "user_view",
},
AuxPanel: {
@@ -87,6 +88,10 @@ module.exports = React.createClass({
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
currentRoomId: null,
// If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null,
logged_in: false,
collapse_lhs: false,
collapse_rhs: false,
@@ -94,6 +99,9 @@ module.exports = React.createClass({
width: 10000,
sideOpacity: 1.0,
middleOpacity: 1.0,
version: null,
newVersion: null,
};
return s;
},
@@ -736,6 +744,18 @@ module.exports = React.createClass({
} else {
dis.dispatch(payload);
}
} else if (screen.indexOf('user/') == 0) {
var userId = screen.substring(5);
this.setState({ viewUserId: userId });
this._setPage(this.PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
var member = new Matrix.RoomMember(null, userId);
if (member) {
dis.dispatch({
action: 'view_user',
member: member,
});
}
}
else {
console.info("Ignoring showScreen for '%s'", screen);
@@ -756,15 +776,13 @@ module.exports = React.createClass({
onUserClick: function(event, userId) {
event.preventDefault();
/*
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
var member = new Matrix.RoomMember(null, userId);
ContextualMenu.createMenu(MemberInfo, {
member: member,
right: window.innerWidth - event.pageX,
top: event.pageY
});
*/
// var MemberInfo = sdk.getComponent('rooms.MemberInfo');
// var member = new Matrix.RoomMember(null, userId);
// ContextualMenu.createMenu(MemberInfo, {
// member: member,
// right: window.innerWidth - event.pageX,
// top: event.pageY
// });
var member = new Matrix.RoomMember(null, userId);
if (!member) { return; }
@@ -856,6 +874,7 @@ module.exports = React.createClass({
onVersion: function(current, latest) {
this.setState({
version: current,
newVersion: latest,
hasNewVersion: current !== latest
});
},
@@ -988,11 +1007,15 @@ module.exports = React.createClass({
page_element = <RoomDirectory />
right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break;
case this.PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.state.viewUserId} collapsed={false} opacity={this.state.sideOpacity} />
break;
}
var topBar;
if (this.state.hasNewVersion) {
topBar = <NewVersionBar />;
topBar = <NewVersionBar version={this.state.version} newVersion={this.state.newVersion} />;
}
else if (MatrixClientPeg.get().isGuest()) {
topBar = <GuestWarningBar />;

View File

@@ -31,7 +31,6 @@ var KeyCode = require('../../KeyCode');
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
var TIMELINE_CAP = 250; // the most events to show in a timeline
var DEBUG = false;
@@ -82,6 +81,9 @@ var TimelinePanel = React.createClass({
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// maximum number of events to show in a timeline
timelineCap: React.PropTypes.number,
},
statics: {
@@ -92,6 +94,12 @@ var TimelinePanel = React.createClass({
roomReadMarkerTsMap: {},
},
getDefaultProps: function() {
return {
timelineCap: 250,
};
},
getInitialState: function() {
var initialReadMarker =
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
@@ -684,7 +692,7 @@ var TimelinePanel = React.createClass({
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow(
MatrixClientPeg.get(), this.props.room,
{windowLimit: TIMELINE_CAP});
{windowLimit: this.props.timelineCap});
var onLoaded = () => {
this._reloadEvents();

View File

@@ -47,6 +47,9 @@ module.exports = React.createClass({
},
_getState: function(props) {
if (!props.member) {
console.error("MemberAvatar called somehow with null member");
}
return {
name: props.member.name,
title: props.member.userId,

View File

@@ -22,12 +22,6 @@ var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'MessageEvent',
statics: {
needsSenderProfile: function() {
return true;
}
},
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,

View File

@@ -24,12 +24,6 @@ import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'TextualEvent',
statics: {
needsSenderProfile: function() {
return false;
}
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var text = TextForEvent.textForEvent(this.props.mxEvent);
@@ -39,4 +33,3 @@ module.exports = React.createClass({
);
},
});

View File

@@ -1,7 +1,8 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import sdk from '../../../index';
import {getCompletions} from '../../../autocomplete/Autocompleter';
@@ -100,11 +101,27 @@ export default class Autocomplete extends React.Component {
this.setState({selectionOffset});
}
componentDidUpdate() {
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
if (selectedCompletion && this.container) {
const domNode = ReactDOM.findDOMNode(selectedCompletion);
const offsetTop = domNode && domNode.offsetTop;
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
offsetTop < this.container.scrollTop) {
this.container.scrollTop = offsetTop - this.container.offsetTop;
}
}
}
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 0;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
let className = classNames('mx_Autocomplete_Completion', {
const className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
@@ -116,40 +133,27 @@ export default class Autocomplete extends React.Component {
this.onConfirm();
};
return (
<div key={i}
className={className}
onMouseOver={onMouseOver}
onClick={onClick}>
{completion.component}
</div>
);
return React.cloneElement(completion.component, {
key: i,
ref: `completion${i}`,
className,
onMouseOver,
onClick,
});
});
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>
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
{completionResult.provider.renderCompletions(completions)}
</div>
) : null;
});
return (
<div className="mx_Autocomplete">
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{renderedCompletions}
</ReactCSSTransitionGroup>
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
{renderedCompletions}
</div>
);
}

View File

@@ -62,7 +62,7 @@ var MAX_READ_AVATARS = 5;
// '----------------------------------------------------------'
module.exports = React.createClass({
displayName: 'Event',
displayName: 'EventTile',
statics: {
haveTileForEvent: function(e) {
@@ -368,7 +368,7 @@ module.exports = React.createClass({
// room, or emote messages
var isInfoMessage = (msgtype === 'm.emote' || eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]);
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
@@ -395,25 +395,44 @@ module.exports = React.createClass({
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
</a>
var aux = null;
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
var readAvatars = this.getReadAvatars();
var avatar, sender;
if (!this.props.continuation && !isInfoMessage) {
if (this.props.mxEvent.sender) {
avatar = (
let avatarSize;
let needsSenderProfile;
if (isInfoMessage) {
// a small avatar, with no sender profile, for emotes and
// joins/parts/etc
avatarSize = 14;
needsSenderProfile = false;
} else if (this.props.continuation) {
// no avatar or sender profile for continuation messages
avatarSize = 0;
needsSenderProfile = false;
} else {
avatarSize = 30;
needsSenderProfile = true;
}
if (this.props.mxEvent.sender && avatarSize) {
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} width={30} height={30} onClick={ this.onMemberAvatarClick } />
<MemberAvatar member={this.props.mxEvent.sender}
width={avatarSize} height={avatarSize}
onClick={ this.onMemberAvatarClick }
/>
</div>
);
}
if (EventTileType.needsSenderProfile()) {
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
);
}
if (needsSenderProfile) {
let aux = null;
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
var editButton = (

View File

@@ -531,7 +531,7 @@ module.exports = React.createClass({
},
onMemberAvatarClick: function () {
var avatarUrl = this.props.member.user.avatarUrl;
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
if(!avatarUrl) return;
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);

View File

@@ -23,6 +23,7 @@ var Modal = require('../../../Modal');
var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
var ScalarAuthClient = require("../../../ScalarAuthClient");
var ScalarMessaging = require('../../../ScalarMessaging');
var UserSettingsStore = require('../../../UserSettingsStore');
// parse a string as an integer; if the input is undefined, or cannot be parsed
@@ -70,6 +71,7 @@ module.exports = React.createClass({
},
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().getRoomDirectoryVisibility(
this.props.room.roomId
).done((result) => {
@@ -93,6 +95,8 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
ScalarMessaging.stopListening();
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
@@ -422,6 +426,27 @@ module.exports = React.createClass({
}, "");
},
onLeaveClick() {
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
},
onForgetClick() {
// FIXME: duplicated with RoomTagContextualMenu (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
var errCode = err.errcode || "unknown error code";
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: `Failed to forget room (${errCode})`
});
});
},
_renderEncryptionSection: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
@@ -540,6 +565,25 @@ module.exports = React.createClass({
);
}
var leaveButton = null;
var myMember = this.props.room.getMember(user_id);
if (myMember) {
if (myMember.membership === "join") {
leaveButton = (
<div className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
Leave room
</div>
);
}
else if (myMember.membership === "leave") {
leaveButton = (
<div className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
Forget room
</div>
);
}
}
// TODO: support editing custom events_levels
// TODO: support editing custom user_levels
@@ -627,6 +671,8 @@ module.exports = React.createClass({
return (
<div className="mx_RoomSettings">
{ leaveButton }
{ tagsSection }
<div className="mx_RoomSettings_toggles">

View File

@@ -100,7 +100,7 @@ export default class DevicesPanelEntry extends React.Component {
deleteButton = <div className="error">{this.state.deleteError}</div>
} else {
deleteButton = (
<div className="textButton"
<div className="mx_textButton"
onClick={this._onDeleteClick}>
Delete
</div>