Merge branch 'develop' into rte-fixes

Conflicts:
	src/UserSettingsStore.js
	src/autocomplete/EmojiProvider.js
	src/components/views/rooms/MessageComposerInput.js
This commit is contained in:
Luke Barnard 2017-05-08 17:08:59 +01:00
commit fe121126f5
88 changed files with 5170 additions and 1126 deletions

View file

@ -25,18 +25,10 @@ var TextForEvent = require('../../../TextForEvent');
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher");
import dis from '../../../dispatcher';
var ObjectUtils = require('../../../ObjectUtils');
var bounce = false;
try {
if (global.localStorage) {
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
}
} catch (e) {
}
var eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.room.member' : 'messages.TextualEvent',
@ -48,6 +40,7 @@ var eventTileTypes = {
'm.room.third_party_invite' : 'messages.TextualEvent',
'm.room.history_visibility' : 'messages.TextualEvent',
'm.room.encryption' : 'messages.TextualEvent',
'm.room.power_levels' : 'messages.TextualEvent',
};
var MAX_READ_AVATARS = 5;
@ -73,6 +66,12 @@ module.exports = WithMatrixClient(React.createClass({
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
* references the same this.props.mxEvent.
*/
isRedacted: React.PropTypes.bool,
/* true if this is a continuation of the previous event (which has the
* effect of not showing another avatar/displayname
*/
@ -285,9 +284,16 @@ module.exports = WithMatrixClient(React.createClass({
},
getReadAvatars: function() {
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
var avatars = [];
var left = 0;
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars"></span>);
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
const avatars = [];
const receiptOffset = 15;
let left = 0;
// It's possible that the receipt was sent several days AFTER the event.
// If it is, we want to display the complete date along with the HH:MM:SS,
@ -307,6 +313,12 @@ module.exports = WithMatrixClient(React.createClass({
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
hidden = false;
}
// TODO: we keep the extra read avatars in the dom to make animation simpler
// we could optimise this to reduce the dom size.
// If hidden, set offset equal to the offset of the final visible avatar or
// else set it proportional to index
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
var userId = receipt.roomMember.userId;
var readReceiptInfo;
@ -318,11 +330,6 @@ module.exports = WithMatrixClient(React.createClass({
this.props.readReceiptMap[userId] = readReceiptInfo;
}
}
// TODO: we keep the extra read avatars in the dom to make animation simpler
// we could optimise this to reduce the dom size.
if (!hidden) {
left -= 15;
}
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
@ -343,7 +350,7 @@ module.exports = WithMatrixClient(React.createClass({
if (remainder > 0) {
remText = <span className="mx_EventTile_readAvatarRemainder"
onClick={this.toggleAllReadAvatars}
style={{ right: -(left - 15) }}>{ remainder }+
style={{ right: -(left - receiptOffset) }}>{ remainder }+
</span>;
}
}
@ -356,7 +363,7 @@ module.exports = WithMatrixClient(React.createClass({
onSenderProfileClick: function(event) {
var mxEvent = this.props.mxEvent;
dispatcher.dispatch({
dis.dispatch({
action: 'insert_displayname',
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
});
@ -372,6 +379,17 @@ module.exports = WithMatrixClient(React.createClass({
});
},
onPermalinkClicked: function(e) {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
room_id: this.props.mxEvent.getRoomId(),
});
},
render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -383,8 +401,7 @@ module.exports = WithMatrixClient(React.createClass({
var msgtype = content.msgtype;
var eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a
// room, or emote messages
// Info messages are basically information about commands processed on a room
var isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
@ -396,6 +413,7 @@ module.exports = WithMatrixClient(React.createClass({
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
var classes = classNames({
mx_EventTile: true,
@ -411,9 +429,14 @@ module.exports = WithMatrixClient(React.createClass({
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
var permalink = "https://matrix.to/#/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId();
const permalink = "https://matrix.to/#/" +
this.props.mxEvent.getRoomId() + "/" +
this.props.mxEvent.getId();
var readAvatars = this.getReadAvatars();
@ -486,6 +509,8 @@ module.exports = WithMatrixClient(React.createClass({
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") {
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
@ -493,15 +518,15 @@ module.exports = WithMatrixClient(React.createClass({
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={ permalink }>
<a href={ permalink } onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={ permalink }>
<a href={ permalink } onClick={this.onPermalinkClicked}>
{ sender }
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
{ timestamp }
</a>
</div>
<div className="mx_EventTile_line" >
@ -527,10 +552,14 @@ module.exports = WithMatrixClient(React.createClass({
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a className="mx_EventTile_senderDetailsLink" href={ permalink }>
<a
className="mx_EventTile_senderDetailsLink"
href={ permalink }
onClick={this.onPermalinkClicked}
>
<div className="mx_EventTile_senderDetails">
{ sender }
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
{ timestamp }
</div>
</a>
</div>
@ -545,8 +574,8 @@ module.exports = WithMatrixClient(React.createClass({
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={ permalink }>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<a href={ permalink } onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ e2e }
<EventTileType ref="tile"
@ -564,7 +593,8 @@ module.exports = WithMatrixClient(React.createClass({
}));
module.exports.haveTileForEvent = function(e) {
if (e.isRedacted()) return false;
// Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';

View file

@ -218,11 +218,13 @@ module.exports = WithMatrixClient(React.createClass({
},
onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member,
action: 'Kick',
askReason: true,
action: kickLabel,
askReason: membership == "join",
danger: true,
onFinished: (proceed, reason) => {
if (!proceed) return;
@ -237,9 +239,10 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Kick success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Kick error",
description: err.message
title: "Failed to kick",
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
).finally(()=>{
@ -278,9 +281,10 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Ban success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Ban error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Ban error",
description: err.message,
title: "Error",
description: "Failed to ban user",
});
}
).finally(()=>{
@ -327,9 +331,10 @@ module.exports = WithMatrixClient(React.createClass({
// get out of sync if we force setState here!
console.log("Mute toggle success");
}, function(err) {
console.error("Mute error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Mute error",
description: err.message
title: "Error",
description: "Failed to mute user",
});
}
).finally(()=>{
@ -375,9 +380,10 @@ module.exports = WithMatrixClient(React.createClass({
description: "This action cannot be performed by a guest user. Please register to be able to do this."
});
} else {
console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, {
title: "Moderator toggle error",
description: err.message
title: "Error",
description: "Failed to toggle moderator status",
});
}
}
@ -395,9 +401,10 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Power change success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err);
Modal.createDialog(ErrorDialog, {
title: "Failure to change power level",
description: err.message
title: "Error",
description: "Failed to change power level",
});
}
).finally(()=>{
@ -553,6 +560,13 @@ module.exports = WithMatrixClient(React.createClass({
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
},
_renderDevices: function() {
if (!this._enableDevices) {
return null;
@ -613,6 +627,7 @@ module.exports = WithMatrixClient(React.createClass({
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={me.membership == "invite"}
onClick={this.onRoomTileClick}
/>
);
}

View file

@ -43,6 +43,7 @@ export default class MessageComposer extends React.Component {
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this.onPageUnload = this.onPageUnload.bind(this);
this.state = {
autocompleteQuery: '',
@ -50,7 +51,7 @@ export default class MessageComposer extends React.Component {
inputState: {
style: [],
blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
wordCount: 0,
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
@ -64,12 +65,21 @@ export default class MessageComposer extends React.Component {
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
window.addEventListener('beforeunload', this.onPageUnload);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
}
window.removeEventListener('beforeunload', this.onPageUnload);
}
onPageUnload(event) {
if (this.messageComposerInput) {
this.messageComposerInput.sentHistory.saveLastTextEntry();
}
}
onEvent(event) {
@ -91,8 +101,9 @@ export default class MessageComposer extends React.Component {
this.refs.uploadInput.click();
}
onUploadFileSelected(ev) {
let files = ev.target.files;
onUploadFileSelected(files, isPasted) {
if (!isPasted)
files = files.target.files;
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -100,7 +111,7 @@ export default class MessageComposer extends React.Component {
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'}
</li>);
}
@ -171,7 +182,7 @@ export default class MessageComposer extends React.Component {
}
onUpArrow() {
return this.refs.autocomplete.onUpArrow();
return this.refs.autocomplete.onUpArrow();
}
onDownArrow() {
@ -299,6 +310,7 @@ export default class MessageComposer extends React.Component {
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onUploadFileSelected={this.onUploadFileSelected}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,

View file

@ -96,8 +96,20 @@ export default class MessageComposerInput extends React.Component {
constructor(props, context) {
super(props, context);
this.onAction = this.onAction.bind(this);
this.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handlePastedFiles = this.handlePastedFiles.bind(this);
this.onEditorContentChanged = this.onEditorContentChanged.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.onEscape = this.onEscape.bind(this);
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
this.state = {
// whether we're in rich text or markdown mode
@ -261,6 +273,7 @@ export default class MessageComposerInput extends React.Component {
}
sendTyping(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT,
@ -404,10 +417,14 @@ export default class MessageComposerInput extends React.Component {
}
return false;
};
}
handleReturn = (ev) => {
if(ev.shiftKey) {
handlePastedFiles(files) {
this.props.onUploadFileSelected(files, true);
}
handleReturn(ev) {
if (ev.shiftKey) {
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
return true;
}
@ -442,7 +459,7 @@ export default class MessageComposerInput extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: err.message,
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
});
});
} else if (cmd.error) {
@ -473,9 +490,9 @@ export default class MessageComposerInput extends React.Component {
let sendTextFn = this.client.sendTextMessage;
if (contentText.startsWith('/me')) {
contentText = contentText.replace('/me', '');
contentText = contentText.replace('/me ', '');
// bit of a hack, but the alternative would be quite complicated
if (contentHTML) contentHTML = contentHTML.replace('/me', '');
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
sendHtmlFn = this.client.sendHtmlEmote;
sendTextFn = this.client.sendEmoteMessage;
}
@ -686,6 +703,7 @@ export default class MessageComposerInput extends React.Component {
keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
handlePastedFiles={this.handlePastedFiles}
stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab}
onUpArrow={this.onUpArrow}
@ -697,3 +715,28 @@ export default class MessageComposerInput extends React.Component {
);
}
}
MessageComposerInput.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,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onUpArrow: React.PropTypes.func,
onDownArrow: React.PropTypes.func,
onUploadFileSelected: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,
onInputStateChanged: React.PropTypes.func,
};

View file

@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
@ -311,7 +312,7 @@ export default React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: err.message
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
});
});
}
@ -420,6 +421,7 @@ export default React.createClass({
},
sendTyping: function(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT

View file

@ -75,7 +75,7 @@ module.exports = React.createClass({
render: function() {
if (this.props.activeAgo >= 0) {
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago");
var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo));
// var ago = this.getDuration(this.props.activeAgo) + " ago";
// if (this.props.currentlyActive) ago += " (now?)";
return (

View file

@ -115,9 +115,10 @@ module.exports = React.createClass({
changeAvatar.onFileSelected(ev).catch(function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set avatar: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to set avatar. " + errMsg
description: "Failed to set avatar.",
});
}).done();
},

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,15 +22,23 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter");
var Unread = require('../../../Unread');
var dis = require("../../../dispatcher");
var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
import AccessibleButton from '../elements/AccessibleButton';
var HIDE_CONFERENCE_CHANS = true;
const HIDE_CONFERENCE_CHANS = true;
const VERBS = {
'm.favourite': 'favourite',
'im.vector.fake.direct': 'tag direct chat',
'im.vector.fake.recent': 'restore',
'm.lowpriority': 'demote',
};
module.exports = React.createClass({
displayName: 'RoomList',
@ -37,13 +46,23 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
currentRoom: React.PropTypes.string,
selectedRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
},
shouldComponentUpdate: function(nextProps, nextState) {
if (nextProps.collapsed !== this.props.collapsed) return true;
if (nextProps.searchFilter !== this.props.searchFilter) return true;
if (nextState.lists !== this.state.lists ||
nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
nextState.incomingCall !== this.state.incomingCall) return true;
return false;
},
getInitialState: function() {
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
lists: {},
incomingCall: null,
};
@ -57,12 +76,21 @@ module.exports = React.createClass({
cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
var s = this.getRoomLists();
this.setState(s);
// lookup for which lists a given roomId is currently in.
this.listsForRoomId = {};
this.refreshRoomList();
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
@ -71,7 +99,22 @@ module.exports = React.createClass({
this._updateStickyHeaders(true);
},
componentDidUpdate: function() {
componentWillReceiveProps: function(nextProps) {
// short-circuit react when the room changes
// to avoid rerendering all the sublists everywhere
if (nextProps.selectedRoom !== this.props.selectedRoom) {
if (this.props.selectedRoom) {
constantTimeDispatcher.dispatch(
"RoomTile.select", this.props.selectedRoom, {}
);
}
constantTimeDispatcher.dispatch(
"RoomTile.select", nextProps.selectedRoom, { selected: true }
);
}
},
componentDidUpdate: function(prevProps, prevState) {
// Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false);
@ -95,6 +138,26 @@ module.exports = React.createClass({
incomingCall: null
});
}
break;
case 'on_room_read':
// poke the right RoomTile to refresh, using the constantTimeDispatcher
// to avoid each and every RoomTile registering to the 'on_room_read' event
// XXX: if we like the constantTimeDispatcher we might want to dispatch
// directly from TimelinePanel rather than needlessly bouncing via here.
constantTimeDispatcher.dispatch(
"RoomTile.refresh", payload.room.roomId, {}
);
// also have to poke the right list(s)
var lists = this.listsForRoomId[payload.room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: payload.room }
);
});
}
break;
}
},
@ -108,7 +171,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
@ -117,10 +180,14 @@ module.exports = React.createClass({
},
onRoom: function(room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
onDeleteRoom: function(roomId) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
@ -143,6 +210,10 @@ module.exports = React.createClass({
}
},
_onMouseOver: function(ev) {
this._lastMouseOverTs = Date.now();
},
onSubListHeaderClick: function(isHidden, scrollToPosition) {
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
this._updateStickyHeaders(true, scrollToPosition);
@ -152,41 +223,98 @@ module.exports = React.createClass({
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
// rather than regenerate our full roomlists, which is very heavy, we poke the
// correct sublists to just re-sort themselves. This isn't enormously reacty,
// but is much faster than the default react reconciler, or having to do voodoo
// with shouldComponentUpdate and a pleaseRefresh property or similar.
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
this._delayedRefreshRoomList();
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: room }
);
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
}
},
onRoomName: function(room) {
this._delayedRefreshRoomList();
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
},
onRoomTags: function(event, room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
onRoomStateMember: function(ev, state, member) {
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
{
this._delayedRefreshRoomList();
}
else {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}
},
onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList();
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
}
else if (ev.getType() == 'm.push_rules') {
this._delayedRefreshRoomList();
}
},
_delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList();
// if the mouse has been moving over the RoomList in the last 500ms
// then delay the refresh further to avoid bouncing around under the
// cursor
if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
this.refreshRoomList();
this._delayedRefreshRoomListLoopCount = 0;
}
else {
this._delayedRefreshRoomListLoopCount++;
this._delayedRefreshRoomList();
}
}, 500),
refreshRoomList: function() {
@ -194,26 +322,36 @@ module.exports = React.createClass({
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
// TODO: rather than bluntly regenerating and re-sorting everything
// every time we see any kind of room change from the JS SDK
// we could do incremental updates on our copy of the state
// based on the room which has actually changed. This would stop
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists());
this._lastRefreshRoomListTs = Date.now();
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
// Alternatively we'd do something magical with Immutable.js or similar.
const lists = this.getRoomLists();
let totalRooms = 0;
for (const l of Object.values(lists)) {
totalRooms += l.length;
}
this.setState({
lists: this.getRoomLists(),
totalRoomCount: totalRooms,
});
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
var self = this;
var s = { lists: {} };
const lists = {};
s.lists["im.vector.fake.invite"] = [];
s.lists["m.favourite"] = [];
s.lists["im.vector.fake.recent"] = [];
s.lists["im.vector.fake.direct"] = [];
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
lists["im.vector.fake.direct"] = [];
lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = [];
this.listsForRoomId = {};
var otherTagNames = {};
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
@ -226,8 +364,13 @@ module.exports = React.createClass({
// ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (!self.listsForRoomId[room.roomId]) {
self.listsForRoomId[room.roomId] = [];
}
if (me.membership == "invite") {
s.lists["im.vector.fake.invite"].push(room);
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
@ -237,81 +380,62 @@ module.exports = React.createClass({
{
// Used to split rooms via tags
var tagNames = Object.keys(room.tags);
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagNames[i]].push(room);
lists[tagName] = lists[tagName] || [];
lists[tagName].push(room);
self.listsForRoomId[room.roomId].push(tagName);
otherTagNames[tagName] = 1;
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
s.lists["im.vector.fake.direct"].push(room);
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
lists["im.vector.fake.direct"].push(room);
}
else {
s.lists["im.vector.fake.recent"].push(room);
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
s.lists["im.vector.fake.archived"].push(room);
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
lists["im.vector.fake.archived"].push(room);
}
else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
}
});
if (s.lists["im.vector.fake.direct"].length == 0 &&
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
!MatrixClientPeg.get().isGuest())
{
// scan through the 'recents' list for any rooms which look like DM rooms
// and make them DM rooms
const oldRecents = s.lists["im.vector.fake.recent"];
s.lists["im.vector.fake.recent"] = [];
for (const room of oldRecents) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
s.lists["im.vector.fake.direct"].push(room);
} else {
s.lists["im.vector.fake.recent"].push(room);
}
}
// save these new guessed DM rooms into the account data
const newMDirectEvent = {};
for (const room of s.lists["im.vector.fake.direct"]) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const otherPerson = Rooms.getOnlyOtherMember(room, me);
if (!otherPerson) continue;
const roomList = newMDirectEvent[otherPerson.userId] || [];
roomList.push(room.roomId);
newMDirectEvent[otherPerson.userId] = roomList;
}
// if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
}
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
return s;
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return lists;
},
_getScrollNode: function() {
var panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
// empirically, if we have gm-prevented for some reason, the scroll node
// is still the 3rd child (i.e. the view child). This looks to be due
// to vdh's improved resize updater logic...?
return panel.children[2]; // XXX: Fragile!
},
_whenScrolling: function(e) {
@ -331,10 +455,11 @@ module.exports = React.createClass({
var incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -354,10 +479,11 @@ module.exports = React.createClass({
// properly through React
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -451,21 +577,74 @@ module.exports = React.createClass({
this.refs.gemscroll.forceUpdate();
},
_getEmptyContent: function(section) {
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
return <RoomDropTarget label="" />;
}
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
if (this.state.totalRoomCount === 0) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
Press
<StartChatButton size="16" />
to start a chat with someone
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
You're not in any rooms yet! Press
<CreateRoomButton size="16" />
to make a room or
<RoomDirectoryButton size="16" />
to browse the directory
</div>;
}
}
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
return <RoomDropTarget label={labelText} />;
},
_getHeaderItems: function(section) {
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
switch (section) {
case 'im.vector.fake.direct':
return <span className="mx_RoomList_headerButtons">
<StartChatButton size="16" />
</span>;
case 'im.vector.fake.recent':
return <span className="mx_RoomList_headerButtons">
<RoomDirectoryButton size="16" />
<CreateRoomButton size="16" />
</span>;
}
},
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList">
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites"
tagName="im.vector.fake.invite"
editable={ false }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
@ -473,51 +652,57 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites"
tagName="m.favourite"
verb="favourite"
emptyContent={this._getEmptyContent('m.favourite')}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label="People"
editable={ false }
tagName="im.vector.fake.direct"
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms"
tagName="im.vector.fake.recent"
editable={ true }
verb="restore"
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
headerItems={this._getHeaderItems('im.vector.fake.recent')}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).map(function(tagName) {
{ Object.keys(self.state.lists).sort().map(function(tagName) {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName }
label={ tagName }
tagName={ tagName }
verb={ "tag as " + tagName }
emptyContent={this._getEmptyContent(tagName)}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />;
@ -528,22 +713,23 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label="Low priority"
tagName="m.lowpriority"
verb="demote"
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical"
tagName="im.vector.fake.archived"
editable={ false }
order="recent"
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true }
startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms }

View file

@ -54,9 +54,10 @@ const BannedUser = React.createClass({
this.props.member.roomId, this.props.member.userId,
).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to unban",
description: err.message,
title: "Error",
description: "Failed to unban",
});
}).done();
},
@ -128,14 +129,17 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err);
});
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({
scalar_error: err
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({
scalar_error: err
});
});
});
}
dis.dispatch({
action: 'ui_opacity',
@ -489,7 +493,7 @@ module.exports = React.createClass({
ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createDialog(IntegrationsManager, {
src: this.scalarClient.hasCredentials() ?
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
null,
onFinished: ()=>{
@ -764,36 +768,39 @@ module.exports = React.createClass({
</div>;
}
var integrationsButton;
var integrationsError;
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
</span>
);
}
let integrationsButton;
let integrationsError;
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
if (this.scalarClient !== null) {
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
</span>
);
}
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
Manage Integrations
</div>
);
} else if (this.state.scalar_error) {
integrationsButton = (
Manage Integrations
</div>
);
} else if (this.state.scalar_error) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError }
</div>
);
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
Manage Integrations
</div>
);
Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError }
</div>
);
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
Manage Integrations
</div>
);
}
}
return (

View file

@ -19,7 +19,6 @@ limitations under the License.
var React = require('react');
var ReactDOM = require("react-dom");
var classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
import DMRoomMap from '../../../utils/DMRoomMap';
var sdk = require('../../../index');
@ -28,6 +27,8 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var Unread = require('../../../Unread');
module.exports = React.createClass({
displayName: 'RoomTile',
@ -35,13 +36,12 @@ module.exports = React.createClass({
propTypes: {
connectDragSource: React.PropTypes.func,
connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool,
selectedRoom: React.PropTypes.string,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object,
},
@ -54,11 +54,11 @@ module.exports = React.createClass({
getInitialState: function() {
return({
hover : false,
badgeHover : false,
notificationTagMenu: false,
roomTagMenu: false,
hover: false,
badgeHover: false,
menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
});
},
@ -80,32 +80,40 @@ module.exports = React.createClass({
}
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData);
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
this.onRefresh();
},
componentWillUnmount: function() {
var cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
},
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId,
componentWillReceiveProps: function(nextProps) {
this.onRefresh();
},
onRefresh: function(params) {
this.setState({
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
});
},
onSelect: function(params) {
this.setState({
selected: params.selected,
});
},
onClick: function(ev) {
if (this.props.onClick) {
this.props.onClick(this.props.room.roomId, ev);
}
},
onMouseEnter: function() {
this.setState( { hover : true });
this.badgeOnMouseEnter();
@ -137,62 +145,32 @@ module.exports = React.createClass({
this.setState({ hover: false });
}
var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu');
var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
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;
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
var self = this;
ContextualMenu.createMenu(NotificationStateMenu, {
menuWidth: 188,
menuHeight: 126,
chevronOffset: 45,
ContextualMenu.createMenu(RoomTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ notificationTagMenu: false });
self.setState({ menuDisplayed: false });
self.props.refreshSubList();
}
});
this.setState({ notificationTagMenu: true });
this.setState({ menuDisplayed: true });
}
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
},
onAvatarClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest() && !this.props.collapsed) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var RoomTagMenu = sdk.getComponent('context_menus.RoomTagContextMenu');
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) - 19;
var self = this;
ContextualMenu.createMenu(RoomTagMenu, {
chevronOffset: 10,
// XXX: fix horrid hardcoding
menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF",
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ roomTagMenu: false });
}
});
this.setState({ roomTagMenu: true });
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
}
},
render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
@ -201,17 +179,17 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.state.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu,
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges,
});
@ -219,14 +197,9 @@ module.exports = React.createClass({
'mx_RoomTile_avatar': true,
});
var avatarContainerClasses = classNames({
'mx_RoomTile_avatar_container': true,
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
});
var badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.notificationTagMenu,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed,
});
// XXX: We should never display raw room IDs, but sometimes the
@ -237,7 +210,7 @@ module.exports = React.createClass({
var badge;
var badgeContent;
if (this.state.badgeHover || this.state.notificationTagMenu) {
if (this.state.badgeHover || this.state.menuDisplayed) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (badges) {
var limitedCount = FormattingUtils.formatCount(notificationCount);
@ -255,10 +228,10 @@ module.exports = React.createClass({
var nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
if (this.props.selected) {
if (this.state.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
@ -292,13 +265,12 @@ module.exports = React.createClass({
let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */}
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
<div className={avatarContainerClasses}>
<RoomAvatar room={this.props.room} width={24} height={24} />
{directMessageIndicator}
</div>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{directMessageIndicator}
</div>
</div>
<div className="mx_RoomTile_nameContainer">

View file

@ -60,7 +60,7 @@ module.exports = React.createClass({
}
}
return (
<li data-scroll-token={eventId+"+"+j}>
<li data-scroll-tokens={eventId+"+"+j}>
{ret}
</li>);
},

View file

@ -19,6 +19,7 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
@ -45,6 +46,9 @@ export default React.createClass({
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
},
onShowRhsClick: function(ev) {
@ -53,9 +57,17 @@ export default React.createClass({
render: function() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
/>;
}
let showRhsButton;
/* // don't bother cluttering things up with this for now.
@ -73,6 +85,7 @@ export default React.createClass({
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ showRhsButton }
{ cancelButton }

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -32,10 +33,10 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar">
<div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}>
<img src="img/scrollup.svg" width="24" height="24"
<img src="img/scrollto.svg" width="24" height="24"
alt="Scroll to unread messages"
title="Scroll to unread messages"/>
Unread messages. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
Jump to first unread message.
</div>
<img className="mx_TopUnreadMessagesBar_close"
src="img/cancel.svg" width="18" height="18"