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:
commit
fe121126f5
88 changed files with 5170 additions and 1126 deletions
|
@ -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) !== '';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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} />,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<li data-scroll-token={eventId+"+"+j}>
|
||||
<li data-scroll-tokens={eventId+"+"+j}>
|
||||
{ret}
|
||||
</li>);
|
||||
},
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue