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

This commit is contained in:
Richard Lewis 2018-02-23 15:37:33 +00:00
commit b2bf4d4709
219 changed files with 9309 additions and 3694 deletions

View file

@ -20,6 +20,7 @@ limitations under the License.
const classNames = require('classnames');
const React = require('react');
const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -29,11 +30,11 @@ module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: {
menuWidth: React.PropTypes.number,
menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number,
menuColour: React.PropTypes.string,
chevronFace: React.PropTypes.string, // top, bottom, left, right
menuWidth: PropTypes.number,
menuHeight: PropTypes.number,
chevronOffset: PropTypes.number,
menuColour: PropTypes.string,
chevronFace: PropTypes.string, // top, bottom, left, right
},
getOrCreateContainer: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
@ -30,8 +31,8 @@ module.exports = React.createClass({
displayName: 'CreateRoom',
propTypes: {
onRoomCreated: React.PropTypes.func,
collapsedRhs: React.PropTypes.bool,
onRoomCreated: PropTypes.func,
collapsedRhs: PropTypes.bool,
},
phases: {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
@ -28,7 +29,7 @@ const FilePanel = React.createClass({
displayName: 'FilePanel',
propTypes: {
roomId: React.PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
},
getInitialState: function() {

View file

@ -31,6 +31,7 @@ import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GeminiScrollbar from 'react-gemini-scrollbar';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({
let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias);
}
let roomNameNode = null;
@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
const httpUrl = MatrixClientPeg.get()
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({
});
const GroupContext = {
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
};
CategoryRoomList.contextTypes = GroupContext;
@ -408,7 +409,7 @@ export default React.createClass({
},
childContextTypes: {
groupStore: React.PropTypes.instanceOf(GroupStore),
groupStore: PropTypes.instanceOf(GroupStore),
},
getChildContext: function() {

View file

@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react';
import PropTypes from 'prop-types';
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
@ -26,18 +27,18 @@ export default React.createClass({
propTypes: {
// matrix client to use for UI auth requests
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
flows: React.PropTypes.array,
params: React.PropTypes.object,
session: React.PropTypes.string,
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback
makeRequest: React.PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
// callback called when the auth process has finished,
// successfully or unsuccessfully.
@ -51,22 +52,22 @@ export default React.createClass({
// the auth session.
// * clientSecret {string} The client secret used in auth
// sessions with the ID server.
onAuthFinished: React.PropTypes.func.isRequired,
onAuthFinished: PropTypes.func.isRequired,
// Inputs provided by the user to the auth process
// and used by various stages. As passed to js-sdk
// interactive-auth
inputs: React.PropTypes.object,
inputs: PropTypes.object,
// As js-sdk interactive-auth
makeRegistrationUrl: React.PropTypes.func,
sessionId: React.PropTypes.string,
clientSecret: React.PropTypes.string,
emailSid: React.PropTypes.string,
makeRegistrationUrl: PropTypes.func,
sessionId: PropTypes.string,
clientSecret: PropTypes.string,
emailSid: PropTypes.string,
// If true, poll to see if the auth flow has been completed
// out-of-band
poll: React.PropTypes.bool,
poll: PropTypes.bool,
},
getInitialState: function() {

View file

@ -18,8 +18,8 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk';
import React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import Notifier from '../../Notifier';
@ -31,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property.
@ -44,23 +47,23 @@ const LoggedInView = React.createClass({
displayName: 'LoggedInView',
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired,
onRoomCreated: React.PropTypes.func,
onUserSettingsClose: React.PropTypes.func,
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
onUserSettingsClose: PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: React.PropTypes.func,
onRegistered: PropTypes.func,
teamToken: React.PropTypes.string,
teamToken: PropTypes.string,
// and lots and lots of other stuff.
},
childContextTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
authCache: React.PropTypes.object,
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
authCache: PropTypes.object,
},
getChildContext: function() {
@ -208,8 +211,51 @@ const LoggedInView = React.createClass({
}
},
_onDragEnd: function(result) {
// Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
}
const dest = result.destination.droppableId;
if (dest === 'tag-panel-droppable') {
// Could be "GroupTile +groupId:domain"
const draggableId = result.draggableId.split(' ').pop();
// Dispatch synchronously so that the TagPanel receives an
// optimistic update from TagOrderStore before the previous
// state is shown.
dis.dispatch(TagOrderActions.moveTag(
this._matrixClient,
draggableId,
result.destination.index,
), true);
} else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result);
}
},
_onRoomTileEndDrag: function(result) {
let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined;
if (prevTag === 'undefined') prevTag = undefined;
const roomId = result.draggableId.split('_')[1];
const oldIndex = result.source.index;
const newIndex = result.destination.index;
dis.dispatch(RoomListActions.tagRoom(
this._matrixClient,
this._matrixClient.getRoom(roomId),
prevTag, newTag,
oldIndex, newIndex,
), true);
},
render: function() {
const TagPanel = sdk.getComponent('structures.TagPanel');
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView');
@ -330,21 +376,21 @@ const LoggedInView = React.createClass({
return (
<div className='mx_MatrixChat_wrapper'>
{ topBar }
<div className={bodyClasses}>
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<main className='mx_MatrixChat_middlePanel'>
{ page_element }
</main>
{ right_panel }
</div>
<DragDropContext onDragEnd={this._onDragEnd}>
<div className={bodyClasses}>
<LeftPanel
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<main className='mx_MatrixChat_middlePanel'>
{ page_element }
</main>
{ right_panel }
</div>
</DragDropContext>
</div>
);
},
});
export default DragDropContext(HTML5Backend)(LoggedInView);
export default LoggedInView;

View file

@ -19,6 +19,7 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics";
@ -92,38 +93,38 @@ export default React.createClass({
displayName: 'MatrixChat',
propTypes: {
config: React.PropTypes.object,
ConferenceHandler: React.PropTypes.any,
onNewScreen: React.PropTypes.func,
registrationUrl: React.PropTypes.string,
enableGuest: React.PropTypes.bool,
config: PropTypes.object,
ConferenceHandler: PropTypes.any,
onNewScreen: PropTypes.func,
registrationUrl: PropTypes.string,
enableGuest: PropTypes.bool,
// the queryParams extracted from the [real] query-string of the URI
realQueryParams: React.PropTypes.object,
realQueryParams: PropTypes.object,
// the initial queryParams extracted from the hash-fragment of the URI
startingFragmentQueryParams: React.PropTypes.object,
startingFragmentQueryParams: PropTypes.object,
// called when we have completed a token login
onTokenLoginCompleted: React.PropTypes.func,
onTokenLoginCompleted: PropTypes.func,
// Represents the screen to display as a result of parsing the initial
// window.location
initialScreenAfterLogin: React.PropTypes.shape({
screen: React.PropTypes.string.isRequired,
params: React.PropTypes.object,
initialScreenAfterLogin: PropTypes.shape({
screen: PropTypes.string.isRequired,
params: PropTypes.object,
}),
// displayname, if any, to set on the device when logging
// in/registering.
defaultDeviceDisplayName: React.PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// A function that makes a registration URL
makeRegistrationUrl: React.PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
childContextTypes: {
appConfig: React.PropTypes.object,
appConfig: PropTypes.object,
},
AuxPanel: {
@ -617,18 +618,26 @@ export default React.createClass({
},
_startRegistration: function(params) {
this.setStateForNewView({
const newState = {
view: VIEWS.REGISTER,
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
};
// Only honour params if they are all present, otherwise we reset
// HS and IS URLs when switching to registration.
if (params.client_secret &&
params.session_id &&
params.hs_url &&
params.is_url &&
params.sid
) {
newState.register_client_secret = params.client_secret;
newState.register_session_id = params.session_id;
newState.register_hs_url = params.hs_url;
newState.register_is_url = params.is_url;
newState.register_id_sid = params.sid;
}
this.setStateForNewView(newState);
this.notifyNewScreen('register');
},
@ -846,16 +855,36 @@ export default React.createClass({
}).close;
},
_leaveRoomWarnings: function(roomId) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
// Show a warning if there are additional complications.
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
const warnings = [];
if (joinRules) {
const rule = joinRules.getContent().join_rule;
if (rule !== "public") {
warnings.push((
<span className="warning" key="non_public_warning">
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
</span>
));
}
}
return warnings;
},
_leaveRoom: function(roomId) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this._leaveRoomWarnings(roomId);
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
title: _t("Leave room"),
description: (
<span>
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ warnings }
</span>
),
onFinished: (shouldLeave) => {
@ -1065,10 +1094,10 @@ export default React.createClass({
// this if we are not scrolled up in the view. To find out, delegate to
// the timeline panel. If the timeline panel doesn't exist, then we assume
// it is safe to reset the timeline.
if (!self.refs.loggedInView) {
if (!self._loggedInView || !self._loggedInView.child) {
return true;
}
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
return self._loggedInView.child.canResetTimelineInRoom(roomId);
});
cli.on('sync', function(state, prevState) {
@ -1480,6 +1509,17 @@ export default React.createClass({
}
},
onServerConfigChange(config) {
const newState = {};
if (config.hsUrl) {
newState.register_hs_url = config.hsUrl;
}
if (config.isUrl) {
newState.register_is_url = config.isUrl;
}
this.setState(newState);
},
_makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer;
@ -1487,6 +1527,10 @@ export default React.createClass({
return this.props.makeRegistrationUrl(params);
},
_collectLoggedInView: function(ref) {
this._loggedInView = ref;
},
render: function() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
@ -1519,7 +1563,7 @@ export default React.createClass({
*/
const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
@ -1564,6 +1608,7 @@ export default React.createClass({
onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
onServerConfigChange={this.onServerConfigChange}
/>
);
}
@ -1598,6 +1643,7 @@ export default React.createClass({
onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
onServerConfigChange={this.onServerConfigChange}
/>
);
}

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import dis from "../../dispatcher";
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const MILLIS_IN_DAY = 86400000;
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
module.exports = React.createClass({
@ -32,63 +32,63 @@ module.exports = React.createClass({
propTypes: {
// true to give the component a 'display: none' style.
hidden: React.PropTypes.bool,
hidden: PropTypes.bool,
// true to show a spinner at the top of the timeline to indicate
// back-pagination in progress
backPaginating: React.PropTypes.bool,
backPaginating: PropTypes.bool,
// true to show a spinner at the end of the timeline to indicate
// forward-pagination in progress
forwardPaginating: React.PropTypes.bool,
forwardPaginating: PropTypes.bool,
// the list of MatrixEvents to display
events: React.PropTypes.array.isRequired,
events: PropTypes.array.isRequired,
// ID of an event to highlight. If undefined, no event will be highlighted.
highlightedEventId: React.PropTypes.string,
highlightedEventId: PropTypes.string,
// Should we show URL Previews
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
// event after which we should show a read marker
readMarkerEventId: React.PropTypes.string,
readMarkerEventId: PropTypes.string,
// whether the read marker should be visible
readMarkerVisible: React.PropTypes.bool,
readMarkerVisible: PropTypes.bool,
// the userid of our user. This is used to suppress the read marker
// for pending messages.
ourUserId: React.PropTypes.string,
ourUserId: PropTypes.string,
// true to suppress the date at the start of the timeline
suppressFirstDateSeparator: React.PropTypes.bool,
suppressFirstDateSeparator: PropTypes.bool,
// whether to show read receipts
showReadReceipts: React.PropTypes.bool,
showReadReceipts: PropTypes.bool,
// true if updates to the event list should cause the scroll panel to
// scroll down when we are at the bottom of the window. See ScrollPanel
// for more details.
stickyBottom: React.PropTypes.bool,
stickyBottom: PropTypes.bool,
// callback which is called when the panel is scrolled.
onScroll: React.PropTypes.func,
onScroll: PropTypes.func,
// callback which is called when more content is needed.
onFillRequest: React.PropTypes.func,
onFillRequest: PropTypes.func,
// className for the panel
className: React.PropTypes.string.isRequired,
className: PropTypes.string.isRequired,
// shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
isTwelveHour: PropTypes.bool,
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
alwaysShowTimestamps: PropTypes.bool,
},
componentWillMount: function() {
@ -325,7 +325,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator);
}
@ -479,7 +479,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator);
continuation = false;
}
@ -522,17 +522,7 @@ module.exports = React.createClass({
// here.
return !this.props.suppressFirstDateSeparator;
}
const prevEventDate = prevEvent.getDate();
if (!nextEventDate || !prevEventDate) {
return false;
}
// Return early for events that are > 24h apart
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
return true;
}
// Compare weekdays
return prevEventDate.getDay() !== nextEventDate.getDay();
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
},
// get a list of read receipts that should be shown next to this event

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../index';
import { _t } from '../../languageHandler';
@ -26,7 +27,7 @@ export default withMatrixClient(React.createClass({
displayName: 'MyGroups',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
},
getInitialState: function() {
@ -72,8 +73,10 @@ export default withMatrixClient(React.createClass({
});
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
content = groupNodes.length > 0 ?
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
{ groupNodes }
<GeminiScrollbar>
<div className="mx_MyGroups_joinedGroups">
{ groupNodes }
</div>
</GeminiScrollbar> :
<div className="mx_MyGroups_placeholder">
{ _t(

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import { _t } from '../../languageHandler';
import sdk from '../../index';
@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
import Resend from '../../Resend';
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
import * as cryptodevices from '../../cryptodevices';
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -41,59 +42,59 @@ module.exports = React.createClass({
propTypes: {
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
numUnreadMessages: PropTypes.number,
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool,
atEndOfLiveTimeline: PropTypes.bool,
// This is true when the user is alone in the room, but has also sent a message.
// Used to suggest to the user to invite someone
sentMessageAndIsAlone: React.PropTypes.bool,
sentMessageAndIsAlone: PropTypes.bool,
// true if there is an active call in this room (means we show
// the 'Active Call' text in the status bar if there is nothing
// more interesting)
hasActiveCall: React.PropTypes.bool,
hasActiveCall: PropTypes.bool,
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: React.PropTypes.number,
whoIsTypingLimit: PropTypes.number,
// callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar
onResendAllClick: React.PropTypes.func,
onResendAllClick: PropTypes.func,
// callback for when the user clicks on the 'cancel all' button in the
// 'unsent messages' bar
onCancelAllClick: React.PropTypes.func,
onCancelAllClick: PropTypes.func,
// callback for when the user clicks on the 'invite others' button in the
// 'you are alone' bar
onInviteClick: React.PropTypes.func,
onInviteClick: PropTypes.func,
// callback for when the user clicks on the 'stop warning me' button in the
// 'you are alone' bar
onStopWarningClick: React.PropTypes.func,
onStopWarningClick: PropTypes.func,
// callback for when the user clicks on the 'scroll to bottom' button
onScrollToBottomClick: React.PropTypes.func,
onScrollToBottomClick: PropTypes.func,
// callback for when we do something that changes the size of the
// status bar. This is used to trigger a re-layout in the parent
// component.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// callback for when the status bar can be hidden from view, as it is
// not displaying anything
onHidden: React.PropTypes.func,
onHidden: PropTypes.func,
// callback for when the status bar is displaying something and should
// be visible
onVisible: React.PropTypes.func,
onVisible: PropTypes.func,
},
getDefaultProps: function() {
@ -147,6 +148,13 @@ module.exports = React.createClass({
});
},
_onSendWithoutVerifyingClick: function() {
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
Resend.resendUnsentEvents(this.props.room);
});
},
_onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room);
},
@ -156,7 +164,7 @@ module.exports = React.createClass({
},
_onShowDevicesClick: function() {
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
},
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
@ -169,8 +177,10 @@ module.exports = React.createClass({
// Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function() {
if (this.props.onVisible && this._getSize()) {
this.props.onVisible();
if (this._getSize()) {
if (this.props.onVisible) this.props.onVisible();
} else {
if (this.props.onHidden) this.props.onHidden();
}
},
@ -286,10 +296,11 @@ module.exports = React.createClass({
if (hasUDE) {
title = _t("Message not sent due to unknown devices being present");
content = _t(
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
{},
{
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
@ -302,11 +313,11 @@ module.exports = React.createClass({
) {
title = unsentMessages[0].error.data.error;
} else {
title = _t("Some of your messages have not been sent.");
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
}
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
{},
{ count: unsentMessages.length },
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,

View file

@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent";
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import Promise from 'bluebird';
const classNames = require("classnames");
import { _t } from '../../languageHandler';
@ -58,18 +59,18 @@ if (DEBUG) {
module.exports = React.createClass({
displayName: 'RoomView',
propTypes: {
ConferenceHandler: React.PropTypes.any,
ConferenceHandler: PropTypes.any,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: React.PropTypes.func,
onRegistered: PropTypes.func,
// An object representing a third party invite to join this room
// Fields:
// * inviteSignUrl (string) The URL used to join this room from an email invite
// (given as part of the link in the invite email)
// * invitedEmail (string) The email address that was invited to this room
thirdPartyInvite: React.PropTypes.object,
thirdPartyInvite: PropTypes.object,
// Any data about the room that would normally come from the Home Server
// but has been passed out-of-band, eg. the room name and avatar URL
@ -80,10 +81,10 @@ module.exports = React.createClass({
// * avatarUrl (string) The mxc:// avatar URL for the room
// * inviterName (string) The display name of the person who
// * invited us tovthe room
oobData: React.PropTypes.object,
oobData: PropTypes.object,
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
collapsedRhs: PropTypes.bool,
},
getInitialState: function() {
@ -263,12 +264,19 @@ module.exports = React.createClass({
isPeeking: true, // this will change to false if peeking fails
});
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
if (this.unmounted) {
return;
}
this.setState({
room: room,
peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
if (this.unmounted) {
return;
}
// Stop peeking if anything went wrong
this.setState({
isPeeking: false,
@ -285,7 +293,7 @@ module.exports = React.createClass({
} else {
throw err;
}
}).done();
});
}
} else if (room) {
// Stop peeking because we have joined this room previously
@ -628,8 +636,8 @@ module.exports = React.createClass({
const room = this.state.room;
if (!room) return;
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
console.log("Tinter.tint from updateTint");
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
},
@ -678,23 +686,7 @@ module.exports = React.createClass({
// a member state changed in this room
// refresh the conf call notification state
this._updateConfCallNotification();
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
// into.
const me = MatrixClientPeg.get().credentials.userId;
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
// Having just joined a room, check to see if it looks like a DM room, and if so,
// mark it as one. This is to work around the fact that some clients don't support
// is_direct. We should remove this once they do.
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
// XXX: There's not a whole lot we can really do if this fails: at best
// perhaps we could try a couple more times, but since it's a temporary
// compatability workaround, let's not bother.
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
}
}
this._updateDMState();
}, 500),
_checkIfAlone: function(room) {
@ -735,6 +727,44 @@ module.exports = React.createClass({
});
},
_updateDMState() {
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me || me.membership !== "join") {
return;
}
// The user may have accepted an invite with is_direct set
if (me.events.member.getPrevContent().membership === "invite" &&
me.events.member.getPrevContent().is_direct
) {
// This is a DM with the sender of the invite event (which we assume
// preceded the join event)
Rooms.setDMRoom(
this.state.room.roomId,
me.events.member.getUnsigned().prev_sender,
);
return;
}
const invitedMembers = this.state.room.getMembersWithMembership("invite");
const joinedMembers = this.state.room.getMembersWithMembership("join");
// There must be one invited member and one joined member
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
return;
}
// The user may have sent an invite with is_direct sent
const other = invitedMembers[0];
if (other &&
other.membership === "invite" &&
other.events.member.getContent().is_direct
) {
Rooms.setDMRoom(this.state.room.roomId, other.userId);
return;
}
},
onSearchResultsResize: function() {
dis.dispatch({ action: 'timeline_resize' }, true);
},
@ -827,18 +857,6 @@ module.exports = React.createClass({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
});
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
if (this.state.room) {
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership == 'invite') {
if (me.events.member.getContent().is_direct) {
// The 'direct' hint is there, so declare that this is a DM room for
// whoever invited us.
return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
}
}
}
return Promise.resolve();
});
},
@ -863,9 +881,13 @@ module.exports = React.createClass({
ev.dataTransfer.dropEffect = 'none';
const items = ev.dataTransfer.items;
if (items.length == 1) {
if (items[0].kind == 'file') {
const items = [...ev.dataTransfer.items];
if (items.length >= 1) {
const isDraggingFiles = items.every(function(item) {
return item.kind == 'file';
});
if (isDraggingFiles) {
this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy';
}
@ -876,10 +898,8 @@ module.exports = React.createClass({
ev.stopPropagation();
ev.preventDefault();
this.setState({ draggingFile: false });
const files = ev.dataTransfer.files;
if (files.length == 1) {
this.uploadFile(files[0]);
}
const files = [...ev.dataTransfer.files];
files.forEach(this.uploadFile);
},
onDragLeaveOrEnd: function(ev) {
@ -1369,10 +1389,12 @@ module.exports = React.createClass({
},
onStatusBarHidden: function() {
if (this.unmounted) return;
// This is currently not desired as it is annoying if it keeps expanding and collapsing
// TODO: Find a less annoying way of hiding the status bar
/*if (this.unmounted) return;
this.setState({
statusBarVisible: false,
});
});*/
},
showSettings: function(show) {

View file

@ -16,6 +16,7 @@ limitations under the License.
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird';
import { KeyCode } from '../../Keyboard';
@ -86,7 +87,7 @@ module.exports = React.createClass({
* scroll down to show the new element, rather than preserving the
* existing view.
*/
stickyBottom: React.PropTypes.bool,
stickyBottom: PropTypes.bool,
/* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
@ -95,7 +96,7 @@ module.exports = React.createClass({
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
*/
startAtBottom: React.PropTypes.bool,
startAtBottom: PropTypes.bool,
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
@ -110,7 +111,7 @@ module.exports = React.createClass({
* directon (at this time) - which will stop the pagination cycle until
* the user scrolls again.
*/
onFillRequest: React.PropTypes.func,
onFillRequest: PropTypes.func,
/* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed
@ -121,24 +122,24 @@ module.exports = React.createClass({
* first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top.
*/
onUnfillRequest: React.PropTypes.func,
onUnfillRequest: PropTypes.func,
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll: React.PropTypes.func,
onScroll: PropTypes.func,
/* onResize: a callback which is called whenever the Gemini scroll
* panel is resized
*/
onResize: React.PropTypes.func,
onResize: PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: React.PropTypes.string,
className: PropTypes.string,
/* style: styles to add to the top-level div
*/
style: React.PropTypes.object,
style: PropTypes.object,
},
getDefaultProps: function() {

View file

@ -17,15 +17,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import FilterStore from '../../stores/FilterStore';
import FlairStore from '../../stores/FlairStore';
import GeminiScrollbar from 'react-gemini-scrollbar';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index';
import dis from '../../dispatcher';
import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
const TagPanel = React.createClass({
displayName: 'TagPanel',
@ -36,17 +37,7 @@ const TagPanel = React.createClass({
getInitialState() {
return {
// A list of group profiles for tags that are group IDs. The intention in future
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
// For now, it suffices to maintain a list of ordered group profiles.
orderedGroupTagProfiles: [
// {
// groupId: '+awesome:foo.bar',{
// name: 'My Awesome Community',
// avatarUrl: 'mxc://...',
// shortDescription: 'Some description...',
// },
],
orderedTags: [],
selectedTags: [],
};
},
@ -54,28 +45,15 @@ const TagPanel = React.createClass({
componentWillMount: function() {
this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.on("sync", this.onClientSync);
this._filterStoreToken = FilterStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
selectedTags: FilterStore.getSelectedTags(),
});
});
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
}
const orderedTags = TagOrderStore.getOrderedTags() || [];
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
// XXX: One profile lookup failing will bring the whole lot down
Promise.all(orderedGroupTags.map(
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
)).then((orderedGroupTagProfiles) => {
if (this.unmounted) return;
this.setState({orderedGroupTagProfiles});
this.setState({
orderedTags: TagOrderStore.getOrderedTags() || [],
selectedTags: TagOrderStore.getSelectedTags(),
});
});
// This could be done by anything with a matrix client
@ -85,6 +63,7 @@ const TagPanel = React.createClass({
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.removeListener("sync", this.onClientSync);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
@ -95,7 +74,17 @@ const TagPanel = React.createClass({
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
onClick() {
onClientSync(syncState, prevState) {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING or PREPARED.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
// Load joined groups
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
}
},
onClick(e) {
dis.dispatch({action: 'deselect_tags'});
},
@ -104,30 +93,62 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'view_create_group'});
},
onTagTileEndDrag() {
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
onClearFilterClick(ev) {
dis.dispatch({action: 'deselect_tags'});
},
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
key={groupProfile.groupId + '_' + index}
groupProfile={groupProfile}
selected={this.state.selectedTags.includes(groupProfile.groupId)}
onEndDrag={this.onTagTileEndDrag}
key={tag}
tag={tag}
index={index}
selected={this.state.selectedTags.includes(tag)}
/>;
});
return <div className="mx_TagPanel" onClick={this.onClick}>
<div className="mx_TagPanel_tagTileContainer">
{ tags }
</div>
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
const clearButton = this.state.selectedTags.length > 0 ?
<img
src="img/icons-close.svg"
alt={_t("Clear filter")}
title={_t("Clear filter")}
width="24"
height="24" /> :
<div />;
return <div className="mx_TagPanel">
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
{ clearButton }
</AccessibleButton>
<div className="mx_TagPanel_divider" />
<GeminiScrollbar
className="mx_TagPanel_scroller"
autoshow={true}
onClick={this.onClick}
>
<Droppable
droppableId="tag-panel-droppable"
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div
className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef}
>
{ tags }
{ provided.placeholder }
</div>
) }
</Droppable>
</GeminiScrollbar>
<div className="mx_TagPanel_divider" />
<div className="mx_TagPanel_createGroupButton">
<GroupsButton tooltip={true} />
</div>
</div>;
},
});

View file

@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore";
const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import Promise from 'bluebird';
const Matrix = require("matrix-js-sdk");
@ -58,49 +59,49 @@ var TimelinePanel = React.createClass({
// representing. This may or may not have a room, depending on what it's
// a timeline representing. If it has a room, we maintain RRs etc for
// that room.
timelineSet: React.PropTypes.object.isRequired,
timelineSet: PropTypes.object.isRequired,
showReadReceipts: React.PropTypes.bool,
showReadReceipts: PropTypes.bool,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool,
manageReadMarkers: React.PropTypes.bool,
manageReadReceipts: PropTypes.bool,
manageReadMarkers: PropTypes.bool,
// true to give the component a 'display: none' style.
hidden: React.PropTypes.bool,
hidden: PropTypes.bool,
// ID of an event to highlight. If undefined, no event will be highlighted.
// typically this will be either 'eventId' or undefined.
highlightedEventId: React.PropTypes.string,
highlightedEventId: PropTypes.string,
// id of an event to jump to. If not given, will go to the end of the
// live timeline.
eventId: React.PropTypes.string,
eventId: PropTypes.string,
// where to position the event given by eventId, in pixels from the
// bottom of the viewport. If not given, will try to put the event
// half way down the viewport.
eventPixelOffset: React.PropTypes.number,
eventPixelOffset: PropTypes.number,
// Should we show URL Previews
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
// callback which is called when the panel is scrolled.
onScroll: React.PropTypes.func,
onScroll: PropTypes.func,
// callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: React.PropTypes.func,
onReadMarkerUpdated: PropTypes.func,
// maximum number of events to show in a timeline
timelineCap: React.PropTypes.number,
timelineCap: PropTypes.number,
// classname to use for the messagepanel
className: React.PropTypes.string,
className: PropTypes.string,
// shape property to be passed to EventTiles
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
// placeholder text to use if the timeline is empty
empty: React.PropTypes.string,
empty: PropTypes.string,
},
statics: {
@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
if (!this._shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
@ -1090,6 +1093,17 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated);
},
_shouldPaginate: function() {
// don't try to paginate while events in the timeline are
// still being decrypted. We don't render events while they're
// being decrypted, so they don't take up space in the timeline.
// This means we can pull quite a lot of events into the timeline
// and end up trying to render a lot of events.
return !this.state.events.some((e) => {
return e.isBeingDecrypted();
});
},
render: function() {
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
@ -1107,9 +1121,9 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<Loader />
</div>
<div className="mx_RoomView_messagePanelSpinner">
<Loader />
</div>
);
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
const React = require('react');
import PropTypes from 'prop-types';
const ContentMessages = require('../../ContentMessages');
const dis = require('../../dispatcher');
const filesize = require('filesize');
@ -22,7 +23,7 @@ import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar',
propTypes: {
room: React.PropTypes.object,
room: PropTypes.object,
},
componentDidMount: function() {

View file

@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
const React = require('react');
const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
const PlatformPeg = require("../../PlatformPeg");
@ -125,8 +126,8 @@ const THEMES = [
const IgnoredUser = React.createClass({
propTypes: {
userId: React.PropTypes.string.isRequired,
onUnignored: React.PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
onUnignored: PropTypes.func.isRequired,
},
_onUnignoreClick: function() {
@ -155,16 +156,16 @@ module.exports = React.createClass({
displayName: 'UserSettings',
propTypes: {
onClose: React.PropTypes.func,
onClose: PropTypes.func,
// The brand string given when creating email pushers
brand: React.PropTypes.string,
brand: PropTypes.string,
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
referralBaseUrl: PropTypes.string,
// Team token for the referral link. If falsy, the referral section will
// not appear
teamToken: React.PropTypes.string,
teamToken: PropTypes.string,
},
getDefaultProps: function() {
@ -375,7 +376,7 @@ module.exports = React.createClass({
{ _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.") }.
"please export your room keys for safe-keeping.") }
</div>,
button: _t("Sign out"),
extraButtons: [
@ -811,6 +812,12 @@ module.exports = React.createClass({
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
<br />
{ _t('Privacy is important to us, so we don\'t collect any personal'
+ ' or identifiable data for our analytics.') }
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
{ _t('Learn more about how we use analytics.') }
</div>
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
</div>
</div>;

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from "../../../Modal";
@ -29,13 +30,13 @@ module.exports = React.createClass({
displayName: 'ForgotPassword',
propTypes: {
defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string,
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
onLoginClick: React.PropTypes.func,
onRegisterClick: React.PropTypes.func,
onComplete: React.PropTypes.func.isRequired,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
onLoginClick: PropTypes.func,
onRegisterClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index';
@ -36,27 +37,28 @@ module.exports = React.createClass({
displayName: 'Login',
propTypes: {
onLoggedIn: React.PropTypes.func.isRequired,
onLoggedIn: PropTypes.func.isRequired,
enableGuest: React.PropTypes.bool,
enableGuest: PropTypes.bool,
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
// Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a
// different home server without confusing users.
fallbackHsUrl: React.PropTypes.string,
fallbackHsUrl: PropTypes.string,
defaultDeviceDisplayName: React.PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done.
onRegisterClick: React.PropTypes.func.isRequired,
onRegisterClick: PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
onForgotPasswordClick: PropTypes.func,
onCancelClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -217,6 +219,8 @@ module.exports = React.createClass({
if (config.isUrl !== undefined) {
newState.enteredIdentityServerUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl);
});
@ -427,10 +431,10 @@ module.exports = React.createClass({
// FIXME: remove status.im theme tweaks
const theme = SettingsStore.getValue("theme");
if (theme !== "status") {
header = <h2>{ _t('Sign in') }</h2>;
header = <h2>{ _t('Sign in') } { loader }</h2>;
} else {
if (!this.state.errorText) {
header = <h2>{ _t('Sign in to get started') }</h2>;
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'PostRegistration',
propTypes: {
onComplete: React.PropTypes.func.isRequired,
onComplete: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import ServerConfig from '../../views/login/ServerConfig';
@ -35,31 +36,32 @@ module.exports = React.createClass({
displayName: 'Registration',
propTypes: {
onLoggedIn: React.PropTypes.func.isRequired,
clientSecret: React.PropTypes.string,
sessionId: React.PropTypes.string,
makeRegistrationUrl: React.PropTypes.func.isRequired,
idSid: React.PropTypes.string,
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
defaultHsUrl: React.PropTypes.string,
defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string,
email: React.PropTypes.string,
referrer: React.PropTypes.string,
teamServerConfig: React.PropTypes.shape({
onLoggedIn: PropTypes.func.isRequired,
clientSecret: PropTypes.string,
sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired,
idSid: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
brand: PropTypes.string,
email: PropTypes.string,
referrer: PropTypes.string,
teamServerConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string.isRequired,
supportEmail: PropTypes.string.isRequired,
// URL of the riot-team-server to get team configurations and track referrals
teamServerURL: React.PropTypes.string.isRequired,
teamServerURL: PropTypes.string.isRequired,
}),
teamSelected: React.PropTypes.object,
teamSelected: PropTypes.object,
defaultDeviceDisplayName: React.PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired,
onCancelClick: React.PropTypes.func,
onLoginClick: PropTypes.func.isRequired,
onCancelClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -130,6 +132,7 @@ module.exports = React.createClass({
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, function() {
this._replaceClient();
});

View file

@ -15,6 +15,8 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
@ -23,16 +25,20 @@ module.exports = React.createClass({
displayName: 'BaseAvatar',
propTypes: {
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
idName: React.PropTypes.string, // ID for generating hash colours
title: React.PropTypes.string, // onHover title text
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number,
height: React.PropTypes.number,
name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: PropTypes.string, // ID for generating hash colours
title: PropTypes.string, // onHover title text
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: PropTypes.number,
height: PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
@ -48,6 +54,16 @@ module.exports = React.createClass({
return this._getState(this.props);
},
componentWillMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync);
},
componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
@ -66,6 +82,23 @@ module.exports = React.createClass({
}
},
onClientSync(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING or PREPARED.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected &&
// Did we fall back?
this.state.urlsIndex > 0
) {
// Start from the highest priority URL again
this.setState({
urlsIndex: 0,
});
}
},
_getState: function(props) {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ]

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const Avatar = require('../../../Avatar');
const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
@ -25,15 +26,15 @@ module.exports = React.createClass({
displayName: 'MemberAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
// The onClick to give the avatar
onClick: React.PropTypes.func,
onClick: PropTypes.func,
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
viewUserOnClick: React.PropTypes.bool,
title: React.PropTypes.string,
viewUserOnClick: PropTypes.bool,
title: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@
'use strict';
import React from "react";
import PropTypes from 'prop-types';
import * as sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
@ -30,10 +31,10 @@ module.exports = React.createClass({
displayName: 'MemberPresenceAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from "../../../index";
@ -25,11 +26,11 @@ module.exports = React.createClass({
// oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from)
propTypes: {
room: React.PropTypes.object,
oobData: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
room: PropTypes.object,
oobData: PropTypes.object,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
},
getDefaultProps: function() {

View file

@ -17,11 +17,12 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: React.PropTypes.func,
onCreateRoom: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const Presets = {
@ -28,8 +29,8 @@ const Presets = {
module.exports = React.createClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: React.PropTypes.func,
preset: React.PropTypes.string,
onChange: PropTypes.func,
preset: PropTypes.string,
},
Presets: Presets,

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -22,9 +23,9 @@ module.exports = React.createClass({
propTypes: {
// Specifying a homeserver will make magical things happen when you,
// e.g. start typing in the room alias box.
homeserver: React.PropTypes.string,
alias: React.PropTypes.string,
onChange: React.PropTypes.func,
homeserver: PropTypes.string,
alias: PropTypes.string,
onChange: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
@ -507,7 +506,8 @@ module.exports = React.createClass({
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
@ -580,14 +580,8 @@ module.exports = React.createClass({
}
return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{ this.props.title }
</div>
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished} title={this.props.title}>
<div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>
@ -597,12 +591,10 @@ module.exports = React.createClass({
{ addressSelector }
{ this.props.extraNode }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
{ this.props.button }
</button>
</div>
</div>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onButtonClick}
onCancel={this.onCancel} />
</BaseDialog>
);
},
});

View file

@ -15,10 +15,14 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* Basic container for modal dialogs.
@ -31,23 +35,43 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: React.PropTypes.func,
onEnterPressed: PropTypes.func,
// called when a key is pressed
onKeyDown: PropTypes.func,
// CSS class to apply to dialog div
className: React.PropTypes.string,
className: PropTypes.string,
// Title for the dialog.
// (could probably actually be something more complicated than a string if desired)
title: React.PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
// children should be the content of the dialog
children: React.PropTypes.node,
children: PropTypes.node,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
};
},
componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
_onKeyDown: function(e) {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
@ -75,7 +99,7 @@ export default React.createClass({
>
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<div className='mx_Dialog_title'>
<div className={'mx_Dialog_title ' + this.props.titleClass}>
{ this.props.title }
</div>
{ this.props.children }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -137,6 +138,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
@ -166,11 +168,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
<DialogButtons primaryButton={_t('Start Chatting')}
onPrimaryButtonClick={this.props.onNewDMClick} />
</div>;
}
@ -187,9 +186,9 @@ export default class ChatCreateOrReuseDialog extends React.Component {
}
ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
onNewDMClick: PropTypes.func.isRequired,
onExistingRoomSelected: PropTypes.func.isRequired,
};

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
import { GroupMemberType } from '../../../groups';
/*
@ -33,20 +33,20 @@ export default React.createClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: React.PropTypes.object,
member: PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
// needed if a group member is specified
matrixClient: React.PropTypes.instanceOf(MatrixClient),
action: React.PropTypes.string.isRequired, // eg. 'Ban'
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
matrixClient: PropTypes.instanceOf(MatrixClient),
action: PropTypes.string.isRequired, // eg. 'Ban'
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason: React.PropTypes.bool,
danger: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
defaultProps: {
@ -76,13 +76,11 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': this.props.danger,
});
const confirmButtonClass = this.props.danger ? 'danger' : '';
let reasonBox;
if (this.props.askReason) {
@ -127,17 +125,11 @@ export default React.createClass({
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
</div>
{ reasonBox }
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass}
onClick={this.onOk} autoFocus={!this.props.askReason}
>
{ this.props.action }
</button>
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
<DialogButtons primaryButton={this.props.action}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={confirmButtonClass}
focus={!this.props.askReason}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -55,11 +55,15 @@ export default React.createClass({
_checkGroupId: function(e) {
let error = null;
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
if (!this.state.groupId) {
error = _t("Community IDs cannot not be empty.");
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({
groupIdError: error,
// Reset createError to get rid of now stale error message
createError: null,
});
return error;
},
@ -159,10 +163,10 @@ export default React.createClass({
{ createErrorNode }
</div>
<div className="mx_Dialog_buttons">
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
<button onClick={this._onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
</div>
</form>
</BaseDialog>

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
componentDidMount: function() {
@ -41,6 +42,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
@ -67,14 +69,9 @@ export default React.createClass({
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ _t('Create Room') }
</button>
</div>
<DialogButtons primaryButton={_t('Create Room')}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Analytics from '../../../Analytics';
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
}
return (
<div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger">
{ _t("Deactivate Account") }
</div>
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}>
<div className="mx_Dialog_content">
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
{ cancelButton }
</div>
</div>
</BaseDialog>
);
}
}
DeactivateAccountDialog.propTypes = {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
}
DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -26,20 +26,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'ErrorDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -27,22 +28,22 @@ export default React.createClass({
propTypes: {
// matrix client to use for UI auth requests
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
flows: React.PropTypes.array,
params: React.PropTypes.object,
session: React.PropTypes.string,
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback
makeRequest: React.PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
title: React.PropTypes.string,
title: PropTypes.string,
},
getInitialState: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
import Modal from '../../../Modal';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
*/
export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
deviceId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -16,20 +16,20 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({
displayName: 'QuestionDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.node,
extraButtons: React.PropTypes.node,
button: React.PropTypes.string,
danger: React.PropTypes.bool,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
title: PropTypes.string,
description: PropTypes.node,
extraButtons: PropTypes.node,
button: PropTypes.string,
danger: PropTypes.bool,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -53,15 +53,11 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
) : null;
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = "";
if (this.props.danger) {
primaryButtonClass = "danger";
}
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
@ -70,13 +66,14 @@ export default React.createClass({
<div className="mx_Dialog_content">
{ this.props.description }
</div>
<div className="mx_Dialog_buttons">
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
<DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={primaryButtonClass}
focus={this.props.focus}
onCancel={this.onCancel}
>
{ this.props.extraButtons }
{ cancelButton }
</div>
</DialogButtons>
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
@ -25,8 +26,8 @@ export default React.createClass({
displayName: 'SessionRestoreErrorDialog',
propTypes: {
error: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
error: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
_sendBugReport: function() {
@ -40,6 +41,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bugreport;
if (SdkConfig.get().bug_report_endpoint_url) {
@ -68,11 +70,9 @@ export default React.createClass({
{ bugreport }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
{ _t("Continue anyway") }
</button>
</div>
<DialogButtons primaryButton={_t("Continue anyway")}
onPrimaryButtonClick={this._continueClicked}
onCancel={this.props.onFinished} />
</BaseDialog>
);
},

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
export default React.createClass({
displayName: 'SetEmailDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
onDifferentServerClicked: PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
onLoginClick: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -15,21 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'TextInputDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
value: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
value: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -58,6 +58,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
@ -71,14 +72,9 @@ export default React.createClass({
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ this.props.button }
</button>
</div>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog>
);
},

View file

@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function markAllDevicesKnown(devices) {
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
}
import { markAllDevicesKnown } from '../../../cryptodevices';
function DeviceListEntry(props) {
const {userId, device} = props;
@ -141,7 +134,7 @@ export default React.createClass({
},
_onSendAnywayClicked: function() {
markAllDevicesKnown(this.props.devices);
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
this.props.onFinished();
this.props.onSend();
@ -187,18 +180,11 @@ export default React.createClass({
}
});
});
let sendButton;
if (haveUnknownDevices) {
sendButton = <button onClick={this._onSendAnywayClicked}>
{ this.props.sendAnywayLabel }
</button>;
} else {
sendButton = <button onClick={this._onSendClicked}>
{ this.props.sendLabel }
</button>;
}
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
@ -213,14 +199,9 @@ export default React.createClass({
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
{sendButton}
<button className="mx_Dialog_primary" autoFocus={true}
onClick={this._onDismissClicked}
>
{_t("Dismiss")}
</button>
</div>
<DialogButtons primaryButton={sendButtonLabel}
onPrimaryButtonClick={sendButtonOnClick}
onCancel={this._onDismissClicked} />
</BaseDialog>
);
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?

View file

@ -15,6 +15,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@ -44,9 +45,9 @@ export default function AccessibleButton(props) {
* implemented exactly like a normal onClick handler.
*/
AccessibleButton.propTypes = {
children: React.PropTypes.node,
element: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
children: PropTypes.node,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
AccessibleButton.defaultProps = {

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
@ -26,17 +27,17 @@ export default React.createClass({
displayName: 'AddressSelector',
propTypes: {
onSelected: React.PropTypes.func.isRequired,
onSelected: PropTypes.func.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
// Whether to show the address on the address tiles
showAddress: React.PropTypes.bool,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
showAddress: PropTypes.bool,
truncateAt: PropTypes.number.isRequired,
selected: PropTypes.number,
// Element to put as a header on top of the list
header: React.PropTypes.node,
header: PropTypes.node,
},
getInitialState: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
@ -28,9 +29,9 @@ export default React.createClass({
propTypes: {
address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,
canDismiss: PropTypes.bool,
onDismissed: PropTypes.func,
justified: PropTypes.bool,
},
getDefaultProps: function() {

View file

@ -19,6 +19,7 @@ limitations under the License.
import url from 'url';
import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
@ -463,6 +464,10 @@ export default class AppTile extends React.Component {
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation";
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media;";
if (this.props.show) {
const loadingElement = (
<div className='mx_AppTileBody mx_AppLoading'>
@ -482,7 +487,13 @@ export default class AppTile extends React.Component {
appTileBody = (
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
{ this.state.loading && loadingElement }
{ /*
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
"allow" attribute, which is unknown to react 15.
*/ }
<iframe
is
allow={iframeFeatures}
ref="appFrame"
src={this._getSafeUrl()}
allowFullScreen="true"

View file

@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DragSource, DropTarget } from 'react-dnd';
import TagTile from './TagTile';
import dis from '../../../dispatcher';
import { findDOMNode } from 'react-dom';
const tagTileSource = {
canDrag: function(props, monitor) {
return true;
},
import { Draggable } from 'react-beautiful-dnd';
beginDrag: function(props) {
// Return the data describing the dragged item
return {
tag: props.groupProfile.groupId,
};
},
endDrag: function(props, monitor, component) {
const dropResult = monitor.getDropResult();
if (!monitor.didDrop() || !dropResult) {
return;
}
props.onEndDrag();
},
};
const tagTileTarget = {
canDrop(props, monitor) {
return true;
},
hover(props, monitor, component) {
if (!monitor.canDrop()) return;
const draggedY = monitor.getClientOffset().y;
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
const targetY = (top + bottom) / 2;
dis.dispatch({
action: 'order_tag',
tag: monitor.getItem().tag,
targetTag: props.groupProfile.groupId,
// Note: we indicate that the tag should be after the target when
// it's being dragged over the top half of the target.
after: draggedY < targetY,
});
},
drop(props) {
// Return the data to be returned by getDropResult
return {
tag: props.groupProfile.groupId,
};
},
};
export default
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
}))((props) => {
const { connectDropTarget, connectDragSource, ...otherProps } = props;
return connectDropTarget(connectDragSource(
<div>
<TagTile {...otherProps} />
</div>,
));
}));
export default function DNDTagTile(props) {
return <div>
<Draggable
key={props.tag}
draggableId={props.tag}
index={props.index}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile {...props} />
</div>
{ provided.placeholder }
</div>
) }
</Draggable>
</div>;
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
@ -24,8 +25,8 @@ export default React.createClass({
displayName: 'DeviceVerifyButtons',
propTypes: {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
},
getInitialState: function() {

View file

@ -0,0 +1,62 @@
/*
Copyright 2017 Aidan Gauland
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
/**
* Basic container for buttons in modal dialogs.
*/
module.exports = React.createClass({
displayName: "DialogButtons",
propTypes: {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
// onClick handler for the cancel button.
onCancel: PropTypes.func.isRequired,
focus: PropTypes.bool,
},
render: function() {
let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
return (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
</div>
);
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class DirectorySearchBox extends React.Component {
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
}
DirectorySearchBox.propTypes = {
className: React.PropTypes.string,
onChange: React.PropTypes.func,
onClear: React.PropTypes.func,
onJoinClick: React.PropTypes.func,
placeholder: React.PropTypes.string,
showJoinButton: React.PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
}
MenuOption.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node,
children: PropTypes.oneOfType([
PropTypes.arrayOf(React.PropTypes.node),
PropTypes.node,
]),
highlighted: React.PropTypes.bool,
dropdownKey: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
onMouseEnter: React.PropTypes.func.isRequired,
highlighted: PropTypes.bool,
dropdownKey: PropTypes.string,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
};
/*
@ -322,20 +323,20 @@ Dropdown.propTypes = {
// The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this
// width.
menuWidth: React.PropTypes.number,
menuWidth: PropTypes.number,
// Called when the selected option changes
onOptionChange: React.PropTypes.func.isRequired,
onOptionChange: PropTypes.func.isRequired,
// Called when the value of the search field changes
onSearchChange: React.PropTypes.func,
searchEnabled: React.PropTypes.bool,
onSearchChange: PropTypes.func,
searchEnabled: PropTypes.bool,
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as
// in the dropped-down menu.
getShortOption: React.PropTypes.func,
value: React.PropTypes.string,
getShortOption: PropTypes.func,
value: PropTypes.string,
// negative for consistency with HTML
disabled: React.PropTypes.bool,
disabled: PropTypes.bool,
};

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const KEY_TAB = 9;
const KEY_SHIFT = 16;
@ -26,18 +27,18 @@ module.exports = React.createClass({
displayName: 'EditableText',
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
label: React.PropTypes.string,
placeholder: React.PropTypes.string,
className: React.PropTypes.string,
labelClassName: React.PropTypes.string,
placeholderClassName: React.PropTypes.string,
onValueChanged: PropTypes.func,
initialValue: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
className: PropTypes.string,
labelClassName: PropTypes.string,
placeholderClassName: PropTypes.string,
// Overrides blurToSubmit if true
blurToCancel: React.PropTypes.bool,
blurToCancel: PropTypes.bool,
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: React.PropTypes.bool,
editable: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
editable: PropTypes.bool,
},
Phases: {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Promise from 'bluebird';
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func,
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: React.PropTypes.string,
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: React.PropTypes.string,
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: React.PropTypes.func,
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
};

View file

@ -16,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
@ -32,8 +33,8 @@ export default function EmojiText(props) {
}
EmojiText.propTypes = {
element: React.PropTypes.string,
children: React.PropTypes.string.isRequired,
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {

View file

@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
};
FlairAvatar.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
export default class Flair extends React.Component {
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
}
const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) {
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
this.setState({
profiles: profiles.filter((profile) => {
return profile ? profile.avatarUrl : false;
}),
});
}
}
@ -134,5 +138,5 @@ Flair.propTypes = {
// this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
@ -23,19 +24,19 @@ module.exports = React.createClass({
propTypes: {
// An array of member events to summarise
events: React.PropTypes.array.isRequired,
events: PropTypes.array.isRequired,
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
children: PropTypes.array.isRequired,
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength: React.PropTypes.number,
summaryLength: PropTypes.number,
// The maximum number of avatars to display in the summary
avatarsMaxLength: React.PropTypes.number,
avatarsMaxLength: PropTypes.number,
// The minimum number of events needed to trigger summarisation
threshold: React.PropTypes.number,
threshold: PropTypes.number,
// Called when the MELS expansion is toggled
onToggle: React.PropTypes.func,
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: React.PropTypes.bool,
startExpanded: PropTypes.bool,
},
getInitialState: function() {

View file

@ -17,7 +17,7 @@ import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
@ -61,6 +61,17 @@ const Pill = React.createClass({
shouldShowPillAvatar: PropTypes.bool,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() {
return {
// ID/alias of the room/user
@ -135,6 +146,7 @@ const Pill = React.createClass({
componentWillMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props);
},

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
@ -24,23 +25,23 @@ module.exports = React.createClass({
displayName: 'PowerSelector',
propTypes: {
value: React.PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
maxValue: PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
usersDefault: PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled.
//
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
controlled: PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
disabled: PropTypes.bool,
onChange: PropTypes.func,
},
getInitialState: function() {

View file

@ -17,12 +17,13 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
module.exports = React.createClass({
displayName: 'ProgressBar',
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number,
value: PropTypes.number,
max: PropTypes.number,
},
render: function() {

View file

@ -0,0 +1,188 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink} from "../../../matrix-to";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component {
static isMessageUrl(url) {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
}
static childContextTypes = {
matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
};
static propTypes = {
// The matrix.to url of the event
url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event
parentEv: PropTypes.instanceOf(MatrixEvent),
};
constructor(props, context) {
super(props, context);
this.state = {
// The event related to this quote and their nested rich quotes
events: [],
// Whether the top (oldest) event should be shown or spoilered
show: true,
// Whether an error was encountered fetching nested older event, show node if it does
err: false,
};
this.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
};
}
parseUrl(url) {
if (!url) return;
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
const [, roomIdentifier, eventId] = matrixToMatch;
return {roomIdentifier, eventId};
}
componentWillReceiveProps(nextProps) {
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
if (!roomIdentifier || !eventId) return;
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
this.setState({ events: [] });
if (room) this.getEvent(room, eventId, true);
}
componentWillMount() {
this.componentWillReceiveProps(this.props);
}
getRoom(id) {
const cli = MatrixClientPeg.get();
if (id[0] === '!') return cli.getRoom(id);
return cli.getRooms().find((r) => {
return r.getAliases().includes(id);
});
}
async getEvent(room, eventId, show) {
const event = room.findEventById(eventId);
if (event) {
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
this.addEvent(room.findEventById(eventId), show);
}
addEvent(event, show) {
const events = [event].concat(this.state.events);
this.setState({events, show});
}
// addRichQuote(roomId, eventId) {
addRichQuote(href) {
const {roomIdentifier, eventId} = this.parseUrl(href);
if (!roomIdentifier || !eventId) {
this.setState({ err: true });
return;
}
const room = this.getRoom(roomIdentifier);
if (!room) {
this.setState({ err: true });
return;
}
this.getEvent(room, eventId, false);
}
onQuoteClick() {
this.setState({ show: true });
}
render() {
const events = this.state.events.slice();
if (events.length) {
const evTiles = [];
if (!this.state.show) {
const oldestEv = events.shift();
const Pill = sdk.getComponent('elements.Pill');
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
evTiles.push(<blockquote className="mx_Quote" key="load">
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
})
}
</blockquote>);
}
const EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
}
// Deliberately render nothing if the URL isn't recognised
// in case we get an undefined/falsey node, replace it with null to make React happy
return this.props.node || null;
}
}

View file

@ -15,23 +15,24 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
name: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
roomId: PropTypes.string, // for per-room settings
label: PropTypes.string, // untranslated
onChange: PropTypes.func,
isExplicit: PropTypes.bool,
manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
group: PropTypes.string,
value: PropTypes.any, // the value for the radio button
},
getInitialState: function() {

View file

@ -20,36 +20,97 @@ import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
displayName: 'TagTile',
propTypes: {
groupProfile: PropTypes.object,
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
componentWillMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
}
},
componentWillUnmount() {
this.unmounted = true;
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.groupProfile.groupId,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onContextButtonClick: function(e) {
e.preventDefault();
e.stopPropagation();
// Hide the (...) immediately
this.setState({ hover: false });
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
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
const self = this;
ContextualMenu.createMenu(TagTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
top: y,
tag: this.props.tag,
onFinished: function() {
self.setState({ menuDisplayed: false });
},
});
this.setState({ menuDisplayed: true });
},
onMouseOver: function() {
this.setState({hover: true});
},
@ -62,8 +123,8 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.props.groupProfile || {};
const name = profile.name || profile.groupId;
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
@ -78,10 +139,15 @@ export default React.createClass({
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
{ "\u00B7\u00B7\u00B7" }
</div> : <div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
{ contextButton }
</div>
</AccessibleButton>;
},

View file

@ -18,16 +18,17 @@ limitations under the License.
const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const Tinter = require("../../../Tinter");
var TintableSvg = React.createClass({
displayName: 'TintableSvg',
propTypes: {
src: React.PropTypes.string.isRequired,
width: React.PropTypes.string.isRequired,
height: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
},
statics: {

View file

@ -17,14 +17,15 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'UserSelector',
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
onChange: PropTypes.func,
selected_users: PropTypes.arrayOf(React.PropTypes.string),
},
getDefaultProps: function() {

View file

@ -132,7 +132,9 @@ module.exports = React.createClass({
render: function() {
if (this.state.removingUser) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
return <div className="mx_MemberInfo">
<Spinner />
</div>;
}
let adminTools;

View file

@ -66,7 +66,7 @@ const GroupRoomTile = React.createClass({
});
GroupRoomTile.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -17,10 +17,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore';
const GroupTile = React.createClass({
displayName: 'GroupTile',
@ -33,7 +35,7 @@ const GroupTile = React.createClass({
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
@ -78,9 +80,39 @@ const GroupTile = React.createClass({
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div>
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
{ (droppableProvided, droppableSnapshot) => (
<div ref={droppableProvided.innerRef}>
<Draggable
key={"GroupTile " + this.props.groupId}
draggableId={"GroupTile " + this.props.groupId}
index={this.props.groupId}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div>
</div>
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
{ provided.placeholder ?
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div> :
<div />
}
</div>
) }
</Draggable>
</div>
) }
</Droppable>
<div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div>
{ descElement }

View file

@ -18,6 +18,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha';
@ -29,10 +30,10 @@ module.exports = React.createClass({
displayName: 'CaptchaForm',
propTypes: {
sitePublicKey: React.PropTypes.string,
sitePublicKey: PropTypes.string,
// called with the captcha response
onCaptchaResponse: React.PropTypes.func,
onCaptchaResponse: PropTypes.func,
},
getDefaultProps: function() {

View file

@ -17,13 +17,14 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CasLogin',
propTypes: {
onSubmit: React.PropTypes.func, // fn()
onSubmit: PropTypes.func, // fn()
},
render: function() {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
@ -131,11 +132,11 @@ export default class CountryDropdown extends React.Component {
}
CountryDropdown.propTypes = {
className: React.PropTypes.string,
isSmall: React.PropTypes.bool,
className: PropTypes.string,
isSmall: PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
disabled: React.PropTypes.bool,
showPrefix: PropTypes.bool,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
disabled: PropTypes.bool,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import classnames from 'classnames';
@ -69,12 +70,12 @@ export const PasswordAuthEntry = React.createClass({
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: React.PropTypes.bool,
busy: PropTypes.bool,
},
getInitialState: function() {
@ -158,10 +159,10 @@ export const RecaptchaAuthEntry = React.createClass({
},
propTypes: {
submitAuthDict: React.PropTypes.func.isRequired,
stageParams: React.PropTypes.object.isRequired,
errorText: React.PropTypes.string,
busy: React.PropTypes.bool,
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
},
_onCaptchaResponse: function(response) {
@ -200,15 +201,15 @@ export const EmailIdentityAuthEntry = React.createClass({
},
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
authSessionId: React.PropTypes.string.isRequired,
clientSecret: React.PropTypes.string.isRequired,
inputs: React.PropTypes.object.isRequired,
stageState: React.PropTypes.object.isRequired,
fail: React.PropTypes.func.isRequired,
setEmailSid: React.PropTypes.func.isRequired,
makeRegistrationUrl: React.PropTypes.func.isRequired,
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
inputs: PropTypes.object.isRequired,
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
makeRegistrationUrl: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -275,15 +276,15 @@ export const MsisdnAuthEntry = React.createClass({
},
propTypes: {
inputs: React.PropTypes.shape({
phoneCountry: React.PropTypes.string,
phoneNumber: React.PropTypes.string,
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
}),
fail: React.PropTypes.func,
clientSecret: React.PropTypes.func,
submitAuthDict: React.PropTypes.func.isRequired,
matrixClient: React.PropTypes.object,
submitAuthDict: React.PropTypes.func,
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
submitAuthDict: PropTypes.func,
},
getInitialState: function() {
@ -374,7 +375,7 @@ export const MsisdnAuthEntry = React.createClass({
return (
<div>
<p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>this._msisdn</i> },
{ msisdn: <i>{ this._msisdn }</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p>
@ -405,11 +406,11 @@ export const FallbackAuthEntry = React.createClass({
displayName: 'FallbackAuthEntry',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
authSessionId: React.PropTypes.string.isRequired,
loginType: React.PropTypes.string.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
},
componentWillMount: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -256,17 +257,17 @@ PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: React.PropTypes.func, // fn()
initialUsername: React.PropTypes.string,
initialPhoneCountry: React.PropTypes.string,
initialPhoneNumber: React.PropTypes.string,
initialPassword: React.PropTypes.string,
onUsernameChanged: React.PropTypes.func,
onPhoneCountryChanged: React.PropTypes.func,
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { field_input_incorrect } from '../../../UiEffects';
import sdk from '../../../index';
import Email from '../../../email';
@ -40,25 +41,25 @@ module.exports = React.createClass({
propTypes: {
// Values pre-filled in the input boxes when the component loads
defaultEmail: React.PropTypes.string,
defaultPhoneCountry: React.PropTypes.string,
defaultPhoneNumber: React.PropTypes.string,
defaultUsername: React.PropTypes.string,
defaultPassword: React.PropTypes.string,
teamsConfig: React.PropTypes.shape({
defaultEmail: PropTypes.string,
defaultPhoneCountry: PropTypes.string,
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
teamsConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string,
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
supportEmail: PropTypes.string,
teams: PropTypes.arrayOf(React.PropTypes.shape({
// The displayed name of the team
"name": React.PropTypes.string,
"name": PropTypes.string,
// The domain of team email addresses
"domain": React.PropTypes.string,
"domain": PropTypes.string,
})).required,
}),
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
@ -28,24 +29,24 @@ module.exports = React.createClass({
displayName: 'ServerConfig',
propTypes: {
onServerConfigChange: React.PropTypes.func,
onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: React.PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: React.PropTypes.string, // e.g. https://vector.im
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
withToggleButton: React.PropTypes.bool,
delayTimeMs: React.PropTypes.number, // time to wait before invoking onChanged
withToggleButton: PropTypes.bool,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
},
getDefaultProps: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
@ -81,7 +82,7 @@ Tinter.registerTintable(updateTintedDownloadImage);
// downloaded. This limit does not seem to apply when the url is used as
// the source attribute of an image tag.
//
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
// for our purposes they inherit the origin of the page that created them.
// This means that any scripts that run when the URL is viewed will be able
// to access local storage.
@ -191,7 +192,7 @@ module.exports = React.createClass({
},
contextTypes: {
appConfig: React.PropTypes.object,
appConfig: PropTypes.object,
},
/**
@ -294,7 +295,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody" ref="body">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
</a>
@ -326,7 +327,7 @@ module.exports = React.createClass({
}
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<div style={{display: "none"}}>
{ /*
* Add dummy copy of the "a" tag
@ -346,7 +347,7 @@ module.exports = React.createClass({
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName }
</a>
@ -359,7 +360,7 @@ module.exports = React.createClass({
} else {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<div className="mx_MFileBody_download">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
{ _t("Download %(text)s", { text: text }) }

View file

@ -17,8 +17,10 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ImageUtils from '../../../ImageUtils';
import Modal from '../../../Modal';
import sdk from '../../../index';
@ -33,10 +35,14 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* called when the image has loaded */
onWidgetLoad: React.PropTypes.func.isRequired,
onWidgetLoad: PropTypes.func.isRequired,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState: function() {
@ -45,9 +51,27 @@ module.exports = React.createClass({
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
imgError: false,
};
},
componentWillMount() {
this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync);
},
onClientSync(syncState, prevState) {
if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING or PREPARED.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected && this.state.imgError) {
// Load the image again
this.setState({
imgError: false,
});
}
},
onClick: function onClick(ev) {
if (ev.button == 0 && !ev.metaKey) {
@ -96,12 +120,18 @@ module.exports = React.createClass({
imgElement.src = this._getThumbUrl();
},
onImageError: function() {
this.setState({
imgError: true,
});
},
_getContentUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
return this.context.matrixClient.mxcUrlToHttp(content.url);
}
},
@ -114,7 +144,7 @@ module.exports = React.createClass({
}
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
}
},
@ -155,7 +185,9 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
this.unmounted = true;
dis.unregister(this.dispatcherRef);
this.context.matrixClient.removeListener('sync', this.onClientSync);
},
onAction: function(payload) {
@ -216,6 +248,14 @@ module.exports = React.createClass({
);
}
if (this.state.imgError) {
return (
<span className="mx_MImageBody">
{ _t("This image cannot be displayed.") }
</span>
);
}
const contentUrl = this._getContentUrl();
let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
@ -230,6 +270,7 @@ module.exports = React.createClass({
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
@ -29,10 +30,10 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* called when the video has loaded */
onWidgetLoad: React.PropTypes.func.isRequired,
onWidgetLoad: PropTypes.func.isRequired,
},
getInitialState: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
module.exports = React.createClass({
@ -24,22 +25,22 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* a list of words to highlight */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* the shsape of the tile, used */
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
},
getEventTileOps: function() {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
@ -27,7 +28,7 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
},
onAvatarClick: function(name) {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import * as linkify from 'linkifyjs';
@ -31,8 +33,6 @@ import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
import SettingsStore from "../../../settings/SettingsStore";
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
@ -43,19 +43,26 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
/* a list of words to highlight */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* callback for when our widget has loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
},
contextTypes: {
addRichQuote: PropTypes.func,
},
getInitialState: function() {
@ -179,6 +186,7 @@ module.exports = React.createClass({
// If the link is a (localised) matrix.to link, replace it with a pill
const Pill = sdk.getComponent('elements.Pill');
const Quote = sdk.getComponent('elements.Quote');
if (Pill.isMessagePillUrl(href)) {
const pillContainer = document.createElement('span');
@ -197,6 +205,21 @@ module.exports = React.createClass({
// update the current node with one that's now taken its place
node = pillContainer;
} else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
this.context.addRichQuote(href);
node.remove();
} else { // We're the first in the chain
const quoteContainer = document.createElement('span');
const quote =
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node);
node = quoteContainer;
}
pillified = true;
}
} else if (node.nodeType == Node.TEXT_NODE) {
const Pill = sdk.getComponent('elements.Pill');

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const TextForEvent = require('../../../TextForEvent');
import sdk from '../../../index';
@ -26,7 +27,7 @@ module.exports = React.createClass({
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
},
render: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
import Promise from 'bluebird';
const React = require('react');
import PropTypes from 'prop-types';
const ObjectUtils = require("../../../ObjectUtils");
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index");
@ -26,11 +27,11 @@ module.exports = React.createClass({
displayName: 'AliasSettings',
propTypes: {
roomId: React.PropTypes.string.isRequired,
canSetCanonicalAlias: React.PropTypes.bool.isRequired,
canSetAliases: React.PropTypes.bool.isRequired,
aliasEvents: React.PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: React.PropTypes.object, // MatrixEvent
roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired,
aliasEvents: PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: PropTypes.object, // MatrixEvent
},
getDefaultProps: function() {
@ -57,8 +58,8 @@ module.exports = React.createClass({
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
state.remoteDomains = Object.keys(state.domainToAliases).filter((alias) => {
return alias !== localDomain;
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0;
});
if (canonicalAliasEvent) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const Tinter = require('../../../Tinter');
@ -42,7 +43,7 @@ module.exports = React.createClass({
displayName: 'ColorSettings',
propTypes: {
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
},
getInitialState: function() {

View file

@ -15,10 +15,12 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual';
const GROUP_ID_REGEX = /\+\S+\:\S+/;
@ -26,13 +28,13 @@ module.exports = React.createClass({
displayName: 'RelatedGroupSettings',
propTypes: {
roomId: React.PropTypes.string.isRequired,
canSetRelatedGroups: React.PropTypes.bool.isRequired,
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
roomId: PropTypes.string.isRequired,
canSetRelatedGroups: PropTypes.bool.isRequired,
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient),
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
@ -43,13 +45,25 @@ module.exports = React.createClass({
getInitialState: function() {
return {
newGroupsList: this.props.relatedGroupsEvent ?
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
newGroupsList: this.getInitialGroupList(),
newGroupId: null,
};
},
getInitialGroupList: function() {
return this.props.relatedGroupsEvent ? (this.props.relatedGroupsEvent.getContent().groups || []) : [];
},
needsSaving: function() {
const cli = this.context.matrixClient;
const room = cli.getRoom(this.props.roomId);
if (!room.currentState.maySendStateEvent('m.room.related_groups', cli.getUserId())) return false;
return !isEqual(this.getInitialGroupList(), this.state.newGroupsList);
},
saveSettings: function() {
if (!this.needsSaving()) return Promise.resolve();
return this.context.matrixClient.sendStateEvent(
this.props.roomId,
'm.room.related_groups',
@ -97,7 +111,7 @@ module.exports = React.createClass({
onGroupDeleted: function(index) {
const newGroupsList = this.state.newGroupsList.slice();
newGroupsList.splice(index, 1),
newGroupsList.splice(index, 1);
this.setState({ newGroupsList });
},

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require("../../../index");
import { _t, _td } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'UrlPreviewSettings',
propTypes: {
room: React.PropTypes.object,
room: PropTypes.object,
},
saveSettings: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
@ -27,6 +28,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils';
import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -35,10 +37,10 @@ module.exports = React.createClass({
displayName: 'AppsDrawer',
propTypes: {
userId: React.PropTypes.string.isRequired,
room: React.PropTypes.object.isRequired,
showApps: React.PropTypes.bool, // Should apps be rendered
hide: React.PropTypes.bool, // If rendered, should apps drawer be visible
userId: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
showApps: PropTypes.bool, // Should apps be rendered
hide: PropTypes.bool, // If rendered, should apps drawer be visible
},
defaultProps: {
@ -139,6 +141,9 @@ module.exports = React.createClass({
'$matrix_room_id': this.props.room.roomId,
'$matrix_display_name': user ? user.displayName : this.props.userId,
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
// TODO: Namespace themes through some standard
'$theme': SettingsStore.getValue("theme"),
};
app.id = appId;

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
@ -29,27 +30,27 @@ module.exports = React.createClass({
propTypes: {
// js-sdk room object
room: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
showApps: React.PropTypes.bool, // Render apps
hideAppsDrawer: React.PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
room: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
showApps: PropTypes.bool, // Render apps
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
// Conference Handler implementation
conferenceHandler: React.PropTypes.object,
conferenceHandler: PropTypes.object,
// set to true to show the file drop target
draggingFile: React.PropTypes.bool,
draggingFile: PropTypes.bool,
// set to true to show the 'active conf call' banner
displayConfCallNotification: React.PropTypes.bool,
displayConfCallNotification: PropTypes.bool,
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: React.PropTypes.number,
maxHeight: PropTypes.number,
// a callback which is called when the content of the aux panel changes
// content in a way that is likely to make it change size.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
},
defaultProps: {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
@ -51,18 +52,18 @@ const EntityTile = React.createClass({
displayName: 'EntityTile',
propTypes: {
name: React.PropTypes.string,
title: React.PropTypes.string,
avatarJsx: React.PropTypes.any, // <BaseAvatar />
className: React.PropTypes.string,
presenceState: React.PropTypes.string,
presenceLastActiveAgo: React.PropTypes.number,
presenceLastTs: React.PropTypes.number,
presenceCurrentlyActive: React.PropTypes.bool,
showInviteButton: React.PropTypes.bool,
shouldComponentUpdate: React.PropTypes.func,
onClick: React.PropTypes.func,
suppressOnHover: React.PropTypes.bool,
name: PropTypes.string,
title: PropTypes.string,
avatarJsx: PropTypes.any, // <BaseAvatar />
className: PropTypes.string,
presenceState: PropTypes.string,
presenceLastActiveAgo: PropTypes.number,
presenceLastTs: PropTypes.number,
presenceCurrentlyActive: PropTypes.bool,
showInviteButton: PropTypes.bool,
shouldComponentUpdate: PropTypes.func,
onClick: PropTypes.func,
suppressOnHover: PropTypes.bool,
},
getDefaultProps: function() {

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +19,7 @@ limitations under the License.
const React = require('react');
import PropTypes from 'prop-types';
const classNames = require("classnames");
import { _t, _td } from '../../../languageHandler';
const Modal = require('../../../Modal');
@ -28,6 +30,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
const ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
import {makeEventPermalink} from "../../../matrix-to";
const ObjectUtils = require('../../../ObjectUtils');
@ -76,65 +79,65 @@ module.exports = withMatrixClient(React.createClass({
propTypes: {
/* MatrixClient instance for sender verification etc */
matrixClient: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
mxEvent: 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,
isRedacted: PropTypes.bool,
/* true if this is a continuation of the previous event (which has the
* effect of not showing another avatar/displayname
*/
continuation: React.PropTypes.bool,
continuation: PropTypes.bool,
/* true if this is the last event in the timeline (which has the effect
* of always showing the timestamp)
*/
last: React.PropTypes.bool,
last: PropTypes.bool,
/* true if this is search context (which has the effect of greying out
* the text
*/
contextual: React.PropTypes.bool,
contextual: PropTypes.bool,
/* a list of words to highlight, ordered by longest first */
highlights: React.PropTypes.array,
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
showUrlPreview: PropTypes.bool,
/* is this the focused event */
isSelectedEvent: React.PropTypes.bool,
isSelectedEvent: PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
onWidgetLoad: PropTypes.func,
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
readReceipts: PropTypes.arrayOf(React.PropTypes.object),
/* opaque readreceipt info for each userId; used by ReadReceiptMarker
* to manage its animations. Should be an empty object when the room
* first loads
*/
readReceiptMap: React.PropTypes.object,
readReceiptMap: PropTypes.object,
/* A function which is used to check if the parent panel is being
* unmounted, to avoid unnecessary work. Should return true if we
* are being unmounted.
*/
checkUnmounting: React.PropTypes.func,
checkUnmounting: PropTypes.func,
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
eventSendStatus: PropTypes.string,
/* the shape of the tile. by default, the layout is intended for the
* normal room timeline. alternative values are: "file_list", "file_grid"
@ -143,10 +146,10 @@ module.exports = withMatrixClient(React.createClass({
* boiilerplatey. So just make the necessary render decisions conditional
* for now.
*/
tileShape: React.PropTypes.string,
tileShape: PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
isTwelveHour: PropTypes.bool,
},
getInitialState: function() {
@ -197,6 +200,8 @@ module.exports = withMatrixClient(React.createClass({
*/
_onDecrypted: function() {
// we need to re-verify the sending device.
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption
// has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent);
this.forceUpdate();
},
@ -215,6 +220,9 @@ module.exports = withMatrixClient(React.createClass({
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
this.setState({
verified: verified,
}, () => {
// Decryption may have caused a change in size
this.props.onWidgetLoad();
});
},
@ -473,9 +481,7 @@ module.exports = withMatrixClient(React.createClass({
mx_EventTile_redacted: isRedacted,
});
const permalink = "https://matrix.to/#/" +
this.props.mxEvent.getRoomId() + "/" +
this.props.mxEvent.getId();
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
const readAvatars = this.getReadAvatars();
@ -513,7 +519,7 @@ module.exports = withMatrixClient(React.createClass({
if (needsSenderProfile) {
let text = null;
if (!this.props.tileShape) {
if (!this.props.tileShape || this.props.tileShape === 'quote') {
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
@ -530,79 +536,104 @@ module.exports = withMatrixClient(React.createClass({
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") {
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</div>
);
} else if (this.props.tileShape === "file_grid") {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a
className="mx_EventTile_senderDetailsLink"
href={permalink}
onClick={this.onPermalinkClicked}
>
switch (this.props.tileShape) {
case 'notif': {
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
<a href={permalink} onClick={this.onPermalinkClicked}>
{ sender }
{ timestamp }
</a>
</div>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</a>
</div>
);
} else {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
);
}
case 'file_grid': {
return (
<div className={classes}>
<div className="mx_EventTile_line" >
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
<a
className="mx_EventTile_senderDetailsLink"
href={permalink}
onClick={this.onPermalinkClicked}
>
<div className="mx_EventTile_senderDetails">
{ sender }
{ timestamp }
</div>
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ editButton }
</div>
</div>
);
);
}
case 'quote': {
return (
<div className={classes}>
{ avatar }
{ sender }
<div className="mx_EventTile_line mx_EventTile_quote">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
tileShape="quote"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onWidgetLoad={this.props.onWidgetLoad}
showUrlPreview={false} />
</div>
</div>
);
}
default: {
return (
<div className={classes}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
{ editButton }
</div>
</div>
);
}
}
},
}));
@ -655,3 +686,5 @@ function E2ePadlockUnencrypted(props) {
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
}
module.exports.getHandlerTile = getHandlerTile;

View file

@ -16,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import { KeyCode } from '../../../Keyboard';
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
onCancelClick: React.PropTypes.func.isRequired,
onCancelClick: PropTypes.func.isRequired,
},
componentWillMount: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -32,10 +33,10 @@ module.exports = React.createClass({
displayName: 'LinkPreviewWidget',
propTypes: {
link: React.PropTypes.string.isRequired, // the URL being previewed
mxEvent: React.PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: React.PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: React.PropTypes.func, // called when the preview's contents has loaded
link: PropTypes.string.isRequired, // the URL being previewed
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: PropTypes.func, // called when the preview's contents has loaded
},
getInitialState: function() {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -65,6 +66,6 @@ export default class MemberDeviceInfo extends React.Component {
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};

View file

@ -27,6 +27,7 @@ limitations under the License.
* 'isTargetMod': boolean
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
@ -41,13 +42,12 @@ import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
member: React.PropTypes.object.isRequired,
matrixClient: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
},
getInitialState: function() {
@ -440,40 +440,57 @@ module.exports = withMatrixClient(React.createClass({
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
const self = this;
if (!room) {
return;
}
const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "",
);
if (!powerLevelEvent) {
return;
}
if (powerLevelEvent.getContent().users) {
const myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId];
if (parseInt(myPower) === parseInt(powerLevel)) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
} else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
} else {
if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
if (!powerLevelEvent.getContent().users) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
return;
}
const myUserId = this.props.matrixClient.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) {
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: (confirmed) => {
if (confirmed) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
return;
}
const myPower = powerLevelEvent.getContent().users[myUserId];
if (parseInt(myPower) === parseInt(powerLevel)) {
Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: (confirmed) => {
if (confirmed) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
return;
}
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
},
onNewDMClick: function() {
@ -713,6 +730,10 @@ module.exports = withMatrixClient(React.createClass({
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
const dmRoomMap = new DMRoomMap(this.props.matrixClient);
// dmRooms will not include dmRooms that we have been invited into but did not join.
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
// XXX: we potentially want DMs we have been invited to, to also show up here :L
// especially as logic below concerns specially if we haven't joined but have been invited
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
const RoomTile = sdk.getComponent("rooms.RoomTile");
@ -722,10 +743,15 @@ module.exports = withMatrixClient(React.createClass({
const room = this.props.matrixClient.getRoom(roomId);
if (room) {
const me = room.getMember(this.props.matrixClient.credentials.userId);
const highlight = (
room.getUnreadNotificationCount('highlight') > 0 ||
me.membership === "invite"
);
// not a DM room if we have are not joined
if (!me.membership || me.membership !== 'join') continue;
// not a DM room if they are not joined
const them = this.props.member;
if (!them.membership || them.membership !== 'join') continue;
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
tiles.push(
<RoomTile key={room.roomId} room={room}
collapsed={false}
@ -831,8 +857,8 @@ module.exports = withMatrixClient(React.createClass({
}
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room

View file

@ -315,6 +315,30 @@ module.exports = React.createClass({
});
},
_getPending3PidInvites: function() {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < requiredKeys.length; ++i) {
if (e.getContent()[requiredKeys[i]] === undefined) return false;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) return false;
return true;
});
}
},
_makeMemberTiles: function(members, membership) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
@ -329,33 +353,13 @@ module.exports = React.createClass({
// Double XXX: Now it's really, really not the right home for this logic:
// we shouldn't even be passing in the 'membership' param to this function.
// Ew, ew, and ew.
// Triple XXX: This violates the size constraint, the output is expected/desired
// to be the same length as the members input array.
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const EntityTile = sdk.getComponent("rooms.EntityTile");
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
// any events without these keys are not valid 3pid invites, so we ignore them
const required_keys = ['key_validity_url', 'public_key', 'display_name'];
for (let i = 0; i < required_keys.length; ++i) {
if (e.getContent()[required_keys[i]] === undefined) return;
}
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />,
);
});
}
memberList.push(...this._getPending3PidInvites().map((e) => {
return <EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />;
}));
}
return memberList;
@ -374,7 +378,7 @@ module.exports = React.createClass({
},
_getChildCountInvited: function() {
return this.state.filteredInvitedMembers.length;
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
},
render: function() {

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
@ -28,7 +29,7 @@ module.exports = React.createClass({
displayName: 'MemberTile',
propTypes: {
member: React.PropTypes.any.isRequired, // RoomMember
member: PropTypes.any.isRequired, // RoomMember
},
getInitialState: function() {

View file

@ -15,13 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpack from './Stickerpack';
@ -40,9 +41,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.render = this.render.bind(this);
//
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this.state = {
autocompleteQuery: '',
@ -54,6 +53,7 @@ export default class MessageComposer extends React.Component {
wordCount: 0,
},
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
};
}
@ -63,12 +63,16 @@ 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);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
}
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
}
onEvent(event) {
@ -77,6 +81,12 @@ export default class MessageComposer extends React.Component {
this.forceUpdate();
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
}
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
@ -304,8 +314,20 @@ export default class MessageComposer extends React.Component {
key="controls_formatting" />
);
const placeholderText = roomIsEncrypted ?
_t('Send an encrypted message') + '…' : _t('Send a message (unencrypted)') + '…';
let placeholderText;
if (this.state.isQuoting) {
if (roomIsEncrypted) {
placeholderText = _t('Send an encrypted reply…');
} else {
placeholderText = _t('Send a reply (unencrypted)…');
}
} else {
if (roomIsEncrypted) {
placeholderText = _t('Send an encrypted message…');
} else {
placeholderText = _t('Send a message (unencrypted)…');
}
}
controls.push(
<MessageComposerInput
@ -377,14 +399,17 @@ export default class MessageComposer extends React.Component {
MessageComposer.propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
callState: PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
uploadFile: PropTypes.func.isRequired,
// string representing the current room app drawer state
showApps: PropTypes.bool,
};

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
@ -50,6 +51,10 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
import QuotePreview from "./QuotePreview";
import RoomViewStore from '../../../stores/RoomViewStore';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@ -86,15 +91,15 @@ export default class MessageComposerInput extends React.Component {
static propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onContentChanged: PropTypes.func,
onInputStateChanged: React.PropTypes.func,
onInputStateChanged: PropTypes.func,
};
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
@ -268,6 +273,7 @@ export default class MessageComposerInput extends React.Component {
let contentState = this.state.editorState.getCurrentContent();
switch (payload.action) {
case 'quote_event':
case 'focus_composer':
editor.focus();
break;
@ -281,12 +287,13 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion({
completion,
selection,
href: `https://matrix.to/#/${payload.user_id}`,
href: makeUserPermalink(payload.user_id),
suffix: selection.getStartOffset() === 0 ? ': ' : ' ',
});
}
break;
case 'quote': {
case 'quote': { // old quoting, whilst rich quoting is in labs
/// XXX: Not doing rich-text quoting from formatted-body because draft-js
/// has regressed such that when links are quoted, errors are thrown. See
/// https://github.com/vector-im/riot-web/issues/4756.
@ -512,7 +519,8 @@ export default class MessageComposerInput extends React.Component {
// composer. For some reason the editor won't scroll automatically if we paste
// blocks of text in or insert newlines.
if (textContent.slice(selection.start).indexOf("\n") === -1) {
this.refs.editor.refs.editor.scrollTop = this.refs.editor.refs.editor.scrollHeight;
let editorRoot = this.refs.editor.refs.editor.parentNode.parentNode;
editorRoot.scrollTop = editorRoot.scrollHeight;
}
});
}
@ -652,7 +660,7 @@ export default class MessageComposerInput extends React.Component {
}
return false;
}
};
onTextPasted(text: string, html?: string) {
const currentSelection = this.state.editorState.getSelection();
@ -715,6 +723,7 @@ export default class MessageComposerInput extends React.Component {
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
if (cmd) {
if (!cmd.error) {
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
this.setState({
editorState: this.createEditorState(),
});
@ -742,9 +751,17 @@ export default class MessageComposerInput extends React.Component {
return true;
}
const quotingEv = RoomViewStore.getQuotingEvent();
if (this.state.isRichtextEnabled) {
// We should only send HTML if any block is styled or contains inline style
let shouldSendHTML = false;
// If we are quoting we need HTML Content
if (quotingEv) {
shouldSendHTML = true;
}
const blocks = contentState.getBlocksAsArray();
if (blocks.some((block) => block.getType() !== 'unstyled')) {
shouldSendHTML = true;
@ -802,7 +819,8 @@ export default class MessageComposerInput extends React.Component {
}).join('\n');
const md = new Markdown(pt);
if (md.isPlainText()) {
// if contains no HTML and we're not quoting (needing HTML)
if (md.isPlainText() && !quotingEv) {
contentText = md.toPlaintext();
} else {
contentHTML = md.toHTML();
@ -825,6 +843,24 @@ export default class MessageComposerInput extends React.Component {
sendTextFn = this.client.sendEmoteMessage;
}
if (quotingEv) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(quotingEv.getRoomId());
const sender = room.currentState.getMember(quotingEv.getSender());
const {body/*, formatted_body*/} = quotingEv.getContent();
const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId());
contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
// we have finished quoting, clear the quotingEvent
dis.dispatch({
action: 'quote_event',
event: null,
});
}
let sendMessagePromise;
if (contentHTML) {
sendMessagePromise = sendHtmlFn.call(
@ -1137,6 +1173,7 @@ export default class MessageComposerInput extends React.Component {
return (
<div className="mx_MessageComposer_input_wrapper">
<div className="mx_MessageComposer_autocomplete_wrapper">
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <QuotePreview /> }
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}
@ -1177,15 +1214,15 @@ export default class MessageComposerInput extends React.Component {
MessageComposerInput.propTypes = {
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
onResize: PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onContentChanged: PropTypes.func,
onFilesPasted: React.PropTypes.func,
onFilesPasted: PropTypes.func,
onInputStateChanged: React.PropTypes.func,
onInputStateChanged: PropTypes.func,
};

Some files were not shown because too many files have changed in this diff Show more