Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into forward_message

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2017-05-19 00:33:36 +01:00
commit a2ab36f598
58 changed files with 844 additions and 1044 deletions

View file

@ -59,7 +59,9 @@ module.exports = React.createClass({
ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl,
props.width, props.height, props.resizeMethod
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props),
@ -74,7 +76,9 @@ module.exports = React.createClass({
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
},
@ -103,14 +107,18 @@ module.exports = React.createClass({
}
return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
} else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
false
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
} else {
return null;

View file

@ -47,16 +47,6 @@ export default React.createClass({
children: React.PropTypes.node,
},
componentWillMount: function() {
this.priorActiveElement = document.activeElement;
},
componentWillUnmount: function() {
if (this.priorActiveElement !== null) {
this.priorActiveElement.focus();
}
},
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
@ -77,7 +67,7 @@ export default React.createClass({
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick}

View file

@ -47,12 +47,6 @@ export default React.createClass({
this.props.onFinished(false);
},
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
@ -69,7 +63,7 @@ export default React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}>
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button}
</button>
{this.props.extraButtons}

View file

@ -149,7 +149,7 @@ export default React.createClass({
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>
This room contains devices that you haven't seen before.
"{this.props.room.name}" contains devices that you haven't seen before.
</h4>
{ warning }
Unknown devices:

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.
@ -138,7 +139,7 @@ export default React.createClass({
onClick={this.onClick.bind(this, i)}
onMouseEnter={this.onMouseEnter.bind(this, i)}
onMouseLeave={this.onMouseLeave}
key={this.props.addressList[i].userId}
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
ref={(ref) => { this.addressListElement = ref; }}
>
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />

View file

@ -114,8 +114,11 @@ export default class Dropdown extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (!nextProps.children || nextProps.children.length === 0) {
return;
}
this._reindexChildren(nextProps.children);
const firstChild = React.Children.toArray(nextProps.children)[0];
const firstChild = nextProps.children[0];
this.setState({
highlightedOption: firstChild ? firstChild.key : null,
});
@ -149,10 +152,12 @@ export default class Dropdown extends React.Component {
}
_onInputClick(ev) {
this.setState({
expanded: !this.state.expanded,
});
ev.preventDefault();
if (!this.state.expanded) {
this.setState({
expanded: true,
});
ev.preventDefault();
}
}
_onMenuOptionClick(dropdownKey) {
@ -249,7 +254,7 @@ export default class Dropdown extends React.Component {
);
});
if (options.length === 0) {
return [<div className="mx_Dropdown_option">
return [<div key="0" className="mx_Dropdown_option">
No results
</div>];
}

View file

@ -221,6 +221,8 @@ module.exports = React.createClass({
"banned": beConjugated + " banned",
"unbanned": beConjugated + " unbanned",
"kicked": beConjugated + " kicked",
"changed_name": "changed name",
"changed_avatar": "changed avatar",
};
if (Object.keys(map).includes(t)) {
@ -267,7 +269,7 @@ module.exports = React.createClass({
);
});
return (
<span className="mx_MemberEventListSummary_avatars">
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
{avatars}
</span>
);
@ -289,7 +291,24 @@ module.exports = React.createClass({
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
case 'join': return 'joined';
case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !==
e.mxEvent.getPrevContent().displayname)
{
return 'changed_name';
}
else if (e.mxEvent.getContent().avatar_url !==
e.mxEvent.getPrevContent().avatar_url)
{
return 'changed_avatar';
}
// console.log("MELS ignoring duplicate membership join event");
return null;
}
else {
return 'joined';
}
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
@ -350,6 +369,7 @@ module.exports = React.createClass({
render: function() {
const eventsToRender = this.props.events;
const eventIds = eventsToRender.map(e => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
@ -360,7 +380,7 @@ module.exports = React.createClass({
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary">
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents}
</div>
);
@ -418,7 +438,7 @@ module.exports = React.createClass({
);
return (
<div className="mx_MemberEventListSummary">
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{toggleButton}
{summaryContainer}
{expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}

View file

@ -19,7 +19,6 @@ import React from 'react';
import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import { charactersToImageNode } from '../../../HtmlUtils';
const COUNTRIES_BY_ISO2 = new Object(null);
for (const c of COUNTRIES) {
@ -27,9 +26,14 @@ for (const c of COUNTRIES) {
}
function countryMatchesSearchQuery(query, country) {
// Remove '+' if present (when searching for a prefix)
if (query[0] === '+') {
query = query.slice(1);
}
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (country.iso2 == query.toUpperCase()) return true;
if (country.prefix == query) return true;
if (country.prefix.indexOf(query) !== -1) return true;
return false;
}
@ -37,10 +41,12 @@ export default class CountryDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._getShortOption = this._getShortOption.bind(this);
this.state = {
searchQuery: '',
}
};
}
componentWillMount() {
@ -48,7 +54,7 @@ export default class CountryDropdown extends React.Component {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
this.props.onOptionChange(COUNTRIES[0].iso2);
this.props.onOptionChange(COUNTRIES[0]);
}
}
@ -58,14 +64,26 @@ export default class CountryDropdown extends React.Component {
});
}
_onOptionChange(iso2) {
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
}
_flagImgForIso2(iso2) {
// Unicode Regional Indicator Symbol letter 'A'
const RIS_A = 0x1F1E6;
const ASCII_A = 65;
return charactersToImageNode(iso2, true,
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
);
return <img src={`flags/${iso2}.png`}/>;
}
_getShortOption(iso2) {
if (!this.props.isSmall) {
return undefined;
}
let countryPrefix;
if (this.props.showPrefix) {
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
}
return <span>
{ this._flagImgForIso2(iso2) }
{ countryPrefix }
</span>;
}
render() {
@ -94,7 +112,7 @@ export default class CountryDropdown extends React.Component {
const options = displayedCountries.map((country) => {
return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)}
{country.name}
{country.name} <span>(+{country.prefix})</span>
</div>;
});
@ -102,18 +120,21 @@ export default class CountryDropdown extends React.Component {
// values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2;
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._flagImgForIso2}
return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true}
>
{options}
</Dropdown>
</Dropdown>;
}
}
CountryDropdown.propTypes = {
className: React.PropTypes.string,
isSmall: React.PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
};

View file

@ -90,8 +90,11 @@ class PasswordLogin extends React.Component {
}
onPhoneCountryChanged(country) {
this.setState({phoneCountry: country});
this.props.onPhoneCountryChanged(country);
this.setState({
phoneCountry: country.iso2,
phonePrefix: country.prefix,
});
this.props.onPhoneCountryChanged(country.iso2);
}
onPhoneNumberChanged(ev) {
@ -121,16 +124,17 @@ class PasswordLogin extends React.Component {
const mxidInputClasses = classNames({
"mx_Login_field": true,
"mx_Login_username": true,
"mx_Login_field_has_prefix": true,
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain),
});
let suffix = null;
if (this.props.hsDomain) {
suffix = <div className="mx_Login_username_suffix">
suffix = <div className="mx_Login_field_suffix">
:{this.props.hsDomain}
</div>;
}
return <div className="mx_Login_username_group">
<div className="mx_Login_username_prefix">@</div>
return <div className="mx_Login_field_group">
<div className="mx_Login_field_prefix">@</div>
<input
className={mxidInputClasses}
key="username_input"
@ -147,13 +151,15 @@ class PasswordLogin extends React.Component {
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry"
className="mx_Login_phoneCountry mx_Login_field_prefix"
ref="phone_country"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input
className="mx_Login_phoneNumberField mx_Login_field"
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
ref="phoneNumber"
key="phone_input"
type="text"

View file

@ -270,7 +270,8 @@ module.exports = React.createClass({
_onPhoneCountryChange(newVal) {
this.setState({
phoneCountry: newVal,
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
@ -313,14 +314,19 @@ module.exports = React.createClass({
const phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry"
className="mx_Login_phoneCountry mx_Login_field_prefix"
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input type="text" ref="phoneNumber"
placeholder="Mobile phone number (optional)"
defaultValue={this.props.defaultPhoneNumber}
className={this._classForField(
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField',
'mx_Login_field',
'mx_Login_field_has_prefix'
)}
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
value={self.state.phoneNumber}

View file

@ -295,16 +295,6 @@ module.exports = WithMatrixClient(React.createClass({
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,
// rather than just HH:MM:SS.
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
dayAfterEvent.setHours(0);
dayAfterEvent.setMinutes(0);
dayAfterEvent.setSeconds(0);
let dayAfterEventTime = dayAfterEvent.getTime();
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) {
var receipt = receipts[i];
@ -340,7 +330,6 @@ module.exports = WithMatrixClient(React.createClass({
suppressAnimation={this._suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts}
showFullTimestamp={receipt.ts >= dayAfterEventTime}
/>
);
}
@ -401,8 +390,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]);
@ -430,7 +418,8 @@ 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,
});
@ -492,22 +481,22 @@ module.exports = WithMatrixClient(React.createClass({
var e2e;
// cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />;
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12" />;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Undecryptable" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12"/>;
}
else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by unverified device" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
}
}
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Unencrypted message" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;

View file

@ -100,7 +100,9 @@ module.exports = React.createClass({
render: function() {
var p = this.state.preview;
if (!p) return <div/>;
if (!p || Object.keys(p).length === 0) {
return <div/>;
}
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
var image = p["og:image"];

View file

@ -717,8 +717,16 @@ module.exports = WithMatrixClient(React.createClass({
const memberName = this.props.member.name;
if (this.props.member.user) {
var presenceState = this.props.member.user.presence;
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
var presenceLastTs = this.props.member.user.lastPresenceTs;
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
@ -736,6 +744,11 @@ module.exports = WithMatrixClient(React.createClass({
<div className="mx_MemberInfo_profileField">
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div>
</div>
{ adminTools }

View file

@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component {
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
@ -43,6 +44,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 +52,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 +66,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,10 +102,11 @@ export default class MessageComposer extends React.Component {
this.refs.uploadInput.click();
}
onUploadFileSelected(files, isPasted) {
if (!isPasted)
files = files.target.files;
onUploadFileSelected(files) {
this.uploadFiles(files.target.files);
}
uploadFiles(files) {
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -300,7 +312,7 @@ export default class MessageComposer extends React.Component {
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onUploadFileSelected={this.onUploadFileSelected}
onFilesPasted={this.uploadFiles}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,

View file

@ -84,7 +84,6 @@ export default class MessageComposerInput extends React.Component {
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);
@ -94,7 +93,7 @@ export default class MessageComposerInput extends React.Component {
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
@ -477,10 +476,6 @@ export default class MessageComposerInput extends React.Component {
return false;
}
handlePastedFiles(files) {
this.props.onUploadFileSelected(files, true);
}
handleReturn(ev) {
if (ev.shiftKey) {
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
@ -542,9 +537,9 @@ export default class MessageComposerInput extends React.Component {
let sendTextFn = this.client.sendTextMessage;
if (contentText.startsWith('/me')) {
contentText = contentText.replace('/me ', '');
contentText = contentText.substring(4);
// 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;
}
@ -734,7 +729,7 @@ export default class MessageComposerInput extends React.Component {
keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
handlePastedFiles={this.handlePastedFiles}
handlePastedFiles={this.props.onFilesPasted}
stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab}
onUpArrow={this.onUpArrow}
@ -764,7 +759,7 @@ MessageComposerInput.propTypes = {
onDownArrow: React.PropTypes.func,
onUploadFileSelected: React.PropTypes.func,
onFilesPasted: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,

View file

@ -69,6 +69,9 @@ export default React.createClass({
// The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired,
// callback to handle files pasted into the composer
onFilesPasted: React.PropTypes.func,
},
componentWillMount: function() {
@ -439,10 +442,27 @@ export default React.createClass({
this.refs.textarea.focus();
},
_onPaste: function(ev) {
const items = ev.clipboardData.items;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
}
}
if (files.length && this.props.onFilesPasted) {
this.props.onFilesPasted(files);
return true;
}
return false;
},
render: function() {
return (
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} />
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
onPaste={this._onPaste}
/>
</div>
);
}

View file

@ -24,6 +24,8 @@ var sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
import DateUtils from '../../../DateUtils';
var bounce = false;
try {
if (global.localStorage) {
@ -63,9 +65,6 @@ module.exports = React.createClass({
// Timestamp when the receipt was read
timestamp: React.PropTypes.number,
// True to show the full date/time rather than just the time
showFullTimestamp: React.PropTypes.bool,
},
getDefaultProps: function() {
@ -170,16 +169,8 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
const prefix = "Seen by " + this.props.member.userId + " at ";
let ts = new Date(this.props.timestamp);
if (this.props.showFullTimestamp) {
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleString();
}
else {
// "7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleTimeString();
}
title = "Seen by " + this.props.member.userId + " at " +
DateUtils.formatDate(new Date(this.props.timestamp));
}
return (

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require('classnames');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
@ -39,6 +40,7 @@ module.exports = React.createClass({
oobData: React.PropTypes.object,
editing: React.PropTypes.bool,
saving: React.PropTypes.bool,
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
@ -49,7 +51,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
editing: false,
onSettingsClick: function() {},
inRoom: false,
onSaveClick: function() {},
};
},
@ -228,10 +230,10 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText element="div" className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{roomName}</EmojiText>
<EmojiText element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
{ searchStatus }
</div>;
}
@ -302,6 +304,14 @@ module.exports = React.createClass({
</AccessibleButton>;
}
let search_button;
if (this.props.onSearchClick && this.props.inRoom) {
search_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
var rightPanel_buttons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
@ -316,9 +326,7 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>
{ search_button }
{ rightPanel_buttons }
</div>;
}

View file

@ -21,13 +21,13 @@ 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');
var HIDE_CONFERENCE_CHANS = true;
@ -37,19 +37,10 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
selectedRoom: React.PropTypes.string,
currentRoom: 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,
@ -59,6 +50,8 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this.mounted = false;
var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
@ -66,46 +59,23 @@ 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.members", this.onRoomStateMember);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
// lookup for which lists a given roomId is currently in.
this.listsForRoomId = {};
var s = this.getRoomLists();
this.setState(s);
// 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() {
this.dispatcherRef = dis.register(this.onAction);
// Initialise the stickyHeaders when the component is created
this._updateStickyHeaders(true);
this.mounted = true;
},
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) {
componentDidUpdate: function() {
// Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false);
@ -131,29 +101,17 @@ module.exports = React.createClass({
}
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 }
);
});
}
// Force an update because the notif count state is too deep to cause
// an update. This forces the local echo of reading notifs to be
// reflected by the RoomTiles.
this.forceUpdate();
break;
}
},
componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
@ -162,7 +120,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.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
@ -171,14 +129,10 @@ 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();
},
@ -201,10 +155,6 @@ 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);
@ -214,98 +164,41 @@ module.exports = React.createClass({
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
// 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, {}
);
this._delayedRefreshRoomList();
},
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)) {
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, {}
);
this._delayedRefreshRoomList();
}
},
onRoomName: function(room) {
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();
},
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, {}
);
}
onRoomTags: function(event, room) {
this._delayedRefreshRoomList();
},
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
},
onRoomMemberName: function(ev, member) {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
this._delayedRefreshRoomList();
},
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() {
// 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();
}
this.refreshRoomList();
}, 500),
refreshRoomList: function() {
@ -313,12 +206,14 @@ module.exports = React.createClass({
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
// 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.
// 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();
},
@ -333,26 +228,18 @@ module.exports = React.createClass({
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
this.listsForRoomId = {};
var otherTagNames = {};
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
// console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (!self.listsForRoomId[room.roomId]) {
self.listsForRoomId[room.roomId] = [];
}
if (me.membership == "invite") {
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
s.lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
@ -363,27 +250,23 @@ 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[tagName].push(room);
self.listsForRoomId[room.roomId].push(tagName);
otherTagNames[tagName] = 1;
s.lists[tagNames[i]].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
}
else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
s.lists["im.vector.fake.archived"].push(room);
}
else {
@ -404,10 +287,8 @@ module.exports = React.createClass({
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
} else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
@ -424,8 +305,6 @@ module.exports = React.createClass({
newMDirectEvent[otherPerson.userId] = roomList;
}
console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
// 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();
}
@ -434,32 +313,19 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
// 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 s;
},
_getScrollNode: function() {
if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
// 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!
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
_whenScrolling: function(e) {
@ -479,6 +345,7 @@ 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;
@ -502,10 +369,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 component from the window
// Use the offset of the top of the componet from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -605,16 +473,15 @@ module.exports = React.createClass({
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList">
<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 } />
@ -625,9 +492,9 @@ module.exports = React.createClass({
verb="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 } />
@ -638,9 +505,9 @@ module.exports = React.createClass({
verb="tag direct chat"
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 }
@ -648,18 +515,17 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms"
tagName="im.vector.fake.recent"
editable={ true }
verb="restore"
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).sort().map(function(tagName) {
{ Object.keys(self.state.lists).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 }
@ -668,9 +534,9 @@ module.exports = React.createClass({
verb={ "tag as " + 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 } />;
@ -684,20 +550,19 @@ module.exports = React.createClass({
verb="demote"
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"
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms }

View file

@ -47,7 +47,7 @@ module.exports = React.createClass({
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.object,
roomAlias: React.PropTypes.string,
},
getDefaultProps: function() {

View file

@ -926,7 +926,7 @@ module.exports = React.createClass({
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">To redact other users' messages, you must be a </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
</div>

View file

@ -27,8 +27,6 @@ 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',
@ -38,10 +36,12 @@ module.exports = React.createClass({
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,10 @@ module.exports = React.createClass({
getInitialState: function() {
return({
hover: false,
badgeHover: 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 +79,23 @@ module.exports = React.createClass({
}
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
componentWillMount: function() {
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
this.onRefresh();
MatrixClientPeg.get().on("accountData", this.onAccountData);
},
componentWillUnmount: function() {
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
},
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,
});
var cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
},
onClick: function(ev) {
@ -179,13 +169,13 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.state.unread,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
@ -231,7 +221,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
if (this.state.selected) {
if (this.props.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
@ -265,8 +255,7 @@ 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_container">
<RoomAvatar room={this.props.room} width={24} height={24} />

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

@ -38,7 +38,7 @@ module.exports = React.createClass({
title="Scroll to unread messages"/>
Jump to first unread message.
</div>
<img className="mx_TopUnreadMessagesBar_close"
<img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18"
alt="Close" title="Close"
onClick={this.props.onCloseClick} />

View file

@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({
},
_onPhoneCountryChange: function(phoneCountry) {
this.setState({ phoneCountry: phoneCountry });
this.setState({ phoneCountry: phoneCountry.iso2 });
},
_onPhoneNumberChange: function(ev) {
@ -147,12 +147,14 @@ export default WithMatrixClient(React.createClass({
return (
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
<div className="mx_UserSettings_profileLabelCell">
<label>Phone</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<div className="mx_Login_phoneSection">
<div className="mx_UserSettings_phoneSection">
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry"
className="mx_UserSettings_phoneCountry"
value={this.state.phoneCountry}
isSmall={true}
/>
<input type="text"
ref={this._collectAddMsisdnInput}