1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-17 17:42:41 +03:00

Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/hide-join-part-2

This commit is contained in:
Michael Telatynski
2017-07-21 19:23:42 +01:00
145 changed files with 9478 additions and 3774 deletions

View File

@@ -0,0 +1,382 @@
/*
Copyright 2017 Vector Creations Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 PropTypes from 'prop-types';
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
name: PropTypes.string,
avatar_url: PropTypes.string,
canonical_alias: PropTypes.string,
}).isRequired,
});
const UserSummaryType = PropTypes.shape({
summaryInfo: PropTypes.shape({
user_id: PropTypes.string.isRequired,
}).isRequired,
});
const CategoryRoomList = React.createClass({
displayName: 'CategoryRoomList',
props: {
rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
category: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const roomNodes = this.props.rooms.map((r) => {
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
});
let catHeader = null;
if (this.props.category && this.props.category.profile) {
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
}
return <div>
{catHeader}
{roomNodes}
</div>;
},
});
const FeaturedRoom = React.createClass({
displayName: 'FeaturedRoom',
props: {
summaryInfo: RoomSummaryType.isRequired,
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_room',
room_alias: this.props.summaryInfo.profile.canonical_alias,
room_id: this.props.summaryInfo.room_id,
});
},
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const oobData = {
roomId: this.props.summaryInfo.room_id,
avatarUrl: this.props.summaryInfo.profile.avatar_url,
name: this.props.summaryInfo.profile.name,
};
let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
}
let roomNameNode = null;
if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
} else {
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
}
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<RoomAvatar oobData={oobData} width={64} height={64} />
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
</AccessibleButton>;
},
});
const RoleUserList = React.createClass({
displayName: 'RoleUserList',
props: {
users: PropTypes.arrayOf(UserSummaryType).isRequired,
role: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const userNodes = this.props.users.map((u) => {
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
});
let roleHeader = null;
if (this.props.role && this.props.role.profile) {
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
}
return <div>
{roleHeader}
{userNodes}
</div>;
},
});
const FeaturedUser = React.createClass({
displayName: 'FeaturedUser',
props: {
summaryInfo: UserSummaryType.isRequired,
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_start_chat_or_reuse',
user_id: this.props.summaryInfo.user_id,
go_home_on_cancel: false,
});
},
render: function() {
// Add avatar once we get profile info inline in the summary response
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
</AccessibleButton>;
},
});
export default React.createClass({
displayName: 'GroupView',
propTypes: {
groupId: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
summary: null,
error: null,
editing: false,
};
},
componentWillMount: function() {
this._loadGroupFromServer(this.props.groupId);
},
componentWillReceiveProps: function(newProps) {
if (this.props.groupId != newProps.groupId) {
this.setState({
summary: null,
error: null,
}, () => {
this._loadGroupFromServer(newProps.groupId);
});
}
},
_loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({
summary: res,
error: null,
});
}, (err) => {
this.setState({
summary: null,
error: err,
});
});
},
_onSettingsClick: function() {
this.setState({editing: true});
},
_getFeaturedRoomsNode() {
const summary = this.state.summary;
if (summary.rooms_section.rooms.length == 0) return null;
const defaultCategoryRooms = [];
const categoryRooms = {};
summary.rooms_section.rooms.forEach((r) => {
if (r.category_id === null) {
defaultCategoryRooms.push(r);
} else {
let list = categoryRooms[r.category_id];
if (list === undefined) {
list = [];
categoryRooms[r.category_id] = list;
}
list.push(r);
}
});
let defaultCategoryNode = null;
if (defaultCategoryRooms.length > 0) {
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
}
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
const cat = summary.rooms_section.categories[catId];
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Rooms:')}
</div>
{defaultCategoryNode}
{categoryRoomNodes}
</div>;
},
_getFeaturedUsersNode() {
const summary = this.state.summary;
if (summary.users_section.users.length == 0) return null;
const noRoleUsers = [];
const roleUsers = {};
summary.users_section.users.forEach((u) => {
if (u.role_id === null) {
noRoleUsers.push(u);
} else {
let list = roleUsers[u.role_id];
if (list === undefined) {
list = [];
roleUsers[u.role_id] = list;
}
list.push(u);
}
});
let noRoleNode = null;
if (noRoleUsers.length > 0) {
noRoleNode = <RoleUserList users={noRoleUsers} />;
}
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
const role = summary.users_section.roles[roleId];
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Users:')}
</div>
{noRoleNode}
{roleUserNodes}
</div>;
},
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.state.summary === null && this.state.error === null) {
return <Loader />;
} else if (this.state.editing) {
return <div />;
} else if (this.state.summary) {
const summary = this.state.summary;
let description = null;
if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description);
}
const roomBody = <div>
<div className="mx_GroupView_groupDesc">{description}</div>
{this._getFeaturedRoomsNode()}
{this._getFeaturedUsersNode()}
</div>;
let nameNode;
if (summary.profile && summary.profile.name) {
nameNode = <div className="mx_RoomHeader_name">
<span>{summary.profile.name}</span>
<span className="mx_GroupView_header_groupid">
({this.props.groupId})
</span>
</div>;
} else {
nameNode = <div className="mx_RoomHeader_name">
<span>{this.props.groupId}</span>
</div>;
}
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
// settings button is display: none until settings is wired up
return (
<div className="mx_GroupView">
<div className="mx_RoomHeader">
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_avatar">
<GroupAvatar
groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl}
width={48} height={48}
/>
</div>
<div className="mx_RoomHeader_info">
{nameNode}
<div className="mx_RoomHeader_topic">
{summary.profile.short_description}
</div>
</div>
<AccessibleButton className="mx_RoomHeader_button" onClick={this._onSettingsClick} title={_t("Settings")} style={{display: 'none'}}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>
</div>
</div>
{roomBody}
</div>
);
} else if (this.state.error) {
if (this.state.error.httpStatus === 404) {
return (
<div className="mx_GroupView_error">
Group {this.props.groupId} not found
</div>
);
} else {
let extraText;
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
extraText = <div>{_t('This Home server does not support groups')}</div>;
}
return (
<div className="mx_GroupView_error">
Failed to load {this.props.groupId}
{extraText}
</div>
);
}
} else {
console.error("Invalid state for GroupView");
return <div />;
}
},
});

View File

@@ -156,13 +156,20 @@ export default React.createClass({
}
*/
var handled = false;
let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
switch (ev.keyCode) {
case KeyCode.UP:
case KeyCode.DOWN:
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
var action = ev.keyCode == KeyCode.UP ?
let action = ev.keyCode == KeyCode.UP ?
'view_prev_room' : 'view_next_room';
dis.dispatch({action: action});
handled = true;
@@ -184,6 +191,14 @@ export default React.createClass({
handled = true;
}
break;
case KeyCode.KEY_K:
if (ctrlCmdOnly) {
dis.dispatch({
action: 'focus_room_filter',
});
handled = true;
}
break;
}
if (handled) {
@@ -210,8 +225,11 @@ export default React.createClass({
const CreateRoom = sdk.getComponent('structures.CreateRoom');
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
let page_element;
@@ -239,7 +257,6 @@ export default React.createClass({
page_element = <UserSettings
onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
@@ -247,6 +264,10 @@ export default React.createClass({
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break;
case PageTypes.MyGroups:
page_element = <MyGroups />;
break;
case PageTypes.CreateRoom:
page_element = <CreateRoom
onRoomCreated={this.props.onRoomCreated}
@@ -263,32 +284,40 @@ export default React.createClass({
break;
case PageTypes.HomePage:
// If team server config is present, pass the teamServerURL. props.teamToken
// must also be set for the team page to be displayed, otherwise the
// welcomePageUrl is used (which might be undefined).
const teamServerUrl = this.props.config.teamServerConfig ?
this.props.config.teamServerConfig.teamServerURL : null;
{
// If team server config is present, pass the teamServerURL. props.teamToken
// must also be set for the team page to be displayed, otherwise the
// welcomePageUrl is used (which might be undefined).
const teamServerUrl = this.props.config.teamServerConfig ?
this.props.config.teamServerConfig.teamServerURL : null;
page_element = <HomePage
collapsedRhs={this.props.collapse_rhs}
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
homePageUrl={this.props.config.welcomePageUrl}
/>;
page_element = <HomePage
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
homePageUrl={this.props.config.welcomePageUrl}
/>;
}
break;
case PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
break;
case PageTypes.GroupView:
page_element = <GroupView
groupId={this.props.currentGroupId}
/>;
break;
}
let topBar;
const isGuest = this.props.matrixClient.isGuest();
var topBar;
if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import Matrix from "matrix-js-sdk";
@@ -41,9 +41,44 @@ import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
/** constants for MatrixChat.state.view */
const VIEWS = {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
LOADING: 0,
// we are showing the login view
LOGIN: 1,
// we are showing the registration view
REGISTER: 2,
// completeing the registration flow
POST_REGISTRATION: 3,
// showing the 'forgot password' view
FORGOT_PASSWORD: 4,
// we have valid matrix credentials (either via an explicit login, via the
// initial re-animation/guest registration, or via a registration), and are
// now setting up a matrixclient to talk to it. This isn't an instant
// process because (a) we need to clear out indexeddb, and (b) we need to
// talk to the team server; while it is going on we show a big spinner.
LOGGING_IN: 5,
// we are logged in with an active matrix client.
LOGGED_IN: 6,
};
module.exports = React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
VIEWS: VIEWS,
},
displayName: 'MatrixChat',
propTypes: {
@@ -59,8 +94,8 @@ module.exports = React.createClass({
// the initial queryParams extracted from the hash-fragment of the URI
startingFragmentQueryParams: React.PropTypes.object,
// called when the session load completes
onLoadCompleted: React.PropTypes.func,
// called when we have completed a token login
onTokenLoginCompleted: React.PropTypes.func,
// Represents the screen to display as a result of parsing the initial
// window.location
@@ -93,14 +128,11 @@ module.exports = React.createClass({
getInitialState: function() {
const s = {
loading: true,
screen: undefined,
screenAfterLogin: this.props.initialScreenAfterLogin,
// the master view we are showing.
view: VIEWS.LOADING,
// Stashed guest credentials if the user logs out
// whilst logged in as a guest user (so they can change
// their mind & log back in)
guestCreds: null,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin,
// What the LoggedInView would be showing if visible
page_type: null,
@@ -113,8 +145,6 @@ module.exports = React.createClass({
// If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null,
loggedIn: false,
loggingIn: false,
collapse_lhs: false,
collapse_rhs: false,
ready: false,
@@ -127,6 +157,7 @@ module.exports = React.createClass({
newVersion: null,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
// Parameters used in the registration dance with the IS
register_client_secret: null,
@@ -143,7 +174,7 @@ module.exports = React.createClass({
realQueryParams: {},
startingFragmentQueryParams: {},
config: {},
onLoadCompleted: () => {},
onTokenLoginCompleted: () => {},
};
},
@@ -193,7 +224,7 @@ module.exports = React.createClass({
// Used by _viewRoom before getting state from sync
this.firstSyncComplete = false;
this.firstSyncPromise = q.defer();
this.firstSyncPromise = Promise.defer();
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
@@ -259,6 +290,9 @@ module.exports = React.createClass({
if (this.onUserClick) {
linkifyMatrix.onUserClick = this.onUserClick;
}
if (this.onGroupClick) {
linkifyMatrix.onGroupClick = this.onGroupClick;
}
window.addEventListener('resize', this.handleResize);
this.handleResize();
@@ -266,39 +300,49 @@ module.exports = React.createClass({
const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
// if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
// the first thing to do is to try the token params in the query-string
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
if(loggedIn) {
this.props.onTokenLoginCompleted();
const firstScreen = this.state.screenAfterLogin ?
this.state.screenAfterLogin.screen : null;
// don't do anything else until the page reloads - just stay in
// the 'loading' state.
return;
}
if (firstScreen === 'login' ||
firstScreen === 'register' ||
firstScreen === 'forgot_password') {
this.props.onLoadCompleted();
this.setState({loading: false});
this._showScreenAfterLogin();
return;
}
// if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
const firstScreen = this.state.screenAfterLogin ?
this.state.screenAfterLogin.screen : null;
// the extra q() ensures that synchronous exceptions hit the same codepath as
// asynchronous ones.
q().then(() => {
return Lifecycle.loadSession({
realQueryParams: this.props.realQueryParams,
fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
if (firstScreen === 'login' ||
firstScreen === 'register' ||
firstScreen === 'forgot_password') {
this.setState({loading: false});
this._showScreenAfterLogin();
return;
}
// the extra Promise.resolve() ensures that synchronous exceptions hit the same codepath as
// asynchronous ones.
return Promise.resolve().then(() => {
return Lifecycle.loadSession({
fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).catch((e) => {
console.error(`Error attempting to load session: ${e}`);
return false;
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
});
}).catch((e) => {
console.error("Unable to load session", e);
}).done(()=>{
// stuff this through the dispatcher so that it happens
// after the on_logged_in action.
dis.dispatch({action: 'load_completed'});
});
}).done();
},
componentWillUnmount: function() {
@@ -317,18 +361,19 @@ module.exports = React.createClass({
}
},
setStateForNewScreen: function(state) {
setStateForNewView: function(state) {
if (state.view === undefined) {
throw new Error("setStateForNewView with no view!");
}
const newState = {
screen: undefined,
viewUserId: null,
loggedIn: false,
ready: false,
};
Object.assign(newState, state);
this.setState(newState);
},
onAction: function(payload) {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@@ -340,26 +385,19 @@ module.exports = React.createClass({
this._startRegistration(payload.params || {});
break;
case 'start_login':
if (MatrixClientPeg.get() &&
MatrixClientPeg.get().isGuest()
) {
this.setState({
guestCreds: MatrixClientPeg.getCredentials(),
});
}
this.setStateForNewScreen({
screen: 'login',
this.setStateForNewView({
view: VIEWS.LOGIN,
});
this.notifyNewScreen('login');
break;
case 'start_post_registration':
this.setState({ // don't clobber loggedIn status
screen: 'post_registration',
this.setState({
view: VIEWS.POST_REGISTRATION,
});
break;
case 'start_password_recovery':
this.setStateForNewScreen({
screen: 'forgot_password',
this.setStateForNewView({
view: VIEWS.FORGOT_PASSWORD,
});
this.notifyNewScreen('forgot_password');
break;
@@ -383,7 +421,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().leave(payload.room_id).done(() => {
modal.close();
if (this.currentRoomId === payload.room_id) {
if (this.state.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
@@ -448,6 +486,18 @@ module.exports = React.createClass({
this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
case 'view_my_groups':
this._setPage(PageTypes.MyGroups);
this.notifyNewScreen('groups');
break;
case 'view_group':
{
const groupId = payload.group_id;
this.setState({currentGroupId: groupId});
this._setPage(PageTypes.GroupView);
this.notifyNewScreen('group/' + groupId);
}
break;
case 'view_home_page':
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
@@ -456,7 +506,7 @@ module.exports = React.createClass({
this._setMxId(payload);
break;
case 'view_start_chat_or_reuse':
this._chatCreateOrReuse(payload.user_id);
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
break;
case 'view_create_chat':
this._createChat();
@@ -500,10 +550,11 @@ module.exports = React.createClass({
break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
// and also that we're not ready (we'll be marked as logged
// in once the login completes, then ready once the sync
// completes).
this.setState({loggingIn: true, ready: false});
// NB. This does not touch 'ready' since if our dispatches
// are delayed, the sync could already have completed
this.setStateForNewView({
view: VIEWS.LOGGING_IN,
});
break;
case 'on_logged_in':
this._onLoggedIn(payload.teamToken);
@@ -512,10 +563,12 @@ module.exports = React.createClass({
this._onLoggedOut();
break;
case 'will_start_client':
this._onWillStartClient();
break;
case 'load_completed':
this._onLoadCompleted();
this.setState({ready: false}, () => {
// if the client is about to start, we are, by definition, not ready.
// Set ready to false now, then it'll be set to true when the sync
// listener we set below fires.
this._onWillStartClient();
});
break;
case 'new_version':
this.onVersion(
@@ -523,6 +576,12 @@ module.exports = React.createClass({
payload.releaseNotes,
);
break;
case 'check_updates':
this.setState({ checkingForUpdate: payload.value });
break;
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
}
},
@@ -537,8 +596,8 @@ module.exports = React.createClass({
},
_startRegistration: function(params) {
this.setStateForNewScreen({
screen: 'register',
this.setStateForNewView({
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
@@ -635,7 +694,7 @@ module.exports = React.createClass({
// Wait for the first sync to complete so that if a room does have an alias,
// it would have been retrieved.
let waitFor = q(null);
let waitFor = Promise.resolve(null);
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
@@ -742,7 +801,9 @@ module.exports = React.createClass({
});
},
_chatCreateOrReuse: function(userId) {
_chatCreateOrReuse: function(userId, goHomeOnCancel) {
if (goHomeOnCancel === undefined) goHomeOnCancel = true;
const ChatCreateOrReuseDialog = sdk.getComponent(
'views.dialogs.ChatCreateOrReuseDialog',
);
@@ -773,7 +834,7 @@ module.exports = React.createClass({
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (!success) {
if (!success && goHomeOnCancel) {
// Dialog cancelled, default to home
dis.dispatch({ action: 'view_home_page' });
}
@@ -846,14 +907,6 @@ module.exports = React.createClass({
});
},
/**
* Called when the sessionloader has finished
*/
_onLoadCompleted: function() {
this.props.onLoadCompleted();
this.setState({loading: false});
},
/**
* Called whenever someone changes the theme
*
@@ -906,9 +959,7 @@ module.exports = React.createClass({
*/
_onLoggedIn: function(teamToken) {
this.setState({
guestCreds: null,
loggedIn: true,
loggingIn: false,
view: VIEWS.LOGGED_IN,
});
if (teamToken) {
@@ -917,10 +968,6 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
this._is_registered = false;
// reset the 'have completed first sync' flag,
// since we've just logged in and will be about to sync
this.firstSyncComplete = false;
this.firstSyncPromise = q.defer();
// Set the display name = user ID localpart
MatrixClientPeg.get().setDisplayName(
@@ -969,8 +1016,8 @@ module.exports = React.createClass({
*/
_onLoggedOut: function() {
this.notifyNewScreen('login');
this.setStateForNewScreen({
loggedIn: false,
this.setStateForNewView({
view: VIEWS.LOGIN,
ready: false,
collapse_lhs: false,
collapse_rhs: false,
@@ -978,6 +1025,7 @@ module.exports = React.createClass({
page_type: PageTypes.RoomDirectory,
});
this._teamToken = null;
this._setPageSubtitle();
},
/**
@@ -986,6 +1034,12 @@ module.exports = React.createClass({
*/
_onWillStartClient() {
const self = this;
// reset the 'have completed first sync' flag,
// since we're about to start the client and therefore about
// to do the first sync
this.firstSyncComplete = false;
this.firstSyncPromise = Promise.defer();
const cli = MatrixClientPeg.get();
// Allow the JS SDK to reap timeline events. This reduces the amount of
@@ -1056,6 +1110,14 @@ module.exports = React.createClass({
}
}
});
const krh = new KeyRequestHandler(cli);
cli.on("crypto.roomKeyRequest", (req) => {
krh.handleKeyRequest(req);
});
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
},
showScreen: function(screen, params) {
@@ -1095,6 +1157,10 @@ module.exports = React.createClass({
dis.dispatch({
action: 'view_room_directory',
});
} else if (screen == 'groups') {
dis.dispatch({
action: 'view_my_groups',
});
} else if (screen == 'post_registration') {
dis.dispatch({
action: 'start_post_registration',
@@ -1133,7 +1199,7 @@ module.exports = React.createClass({
// we can't view a room unless we're logged in
// (a guest account is fine)
if (this.state.loggedIn) {
if (this.state.view === VIEWS.LOGGED_IN) {
dis.dispatch(payload);
}
} else if (screen.indexOf('user/') == 0) {
@@ -1154,6 +1220,15 @@ module.exports = React.createClass({
member: member,
});
}
} else if (screen.indexOf('group/') == 0) {
const groupId = screen.substring(6);
// TODO: Check valid group ID
dis.dispatch({
action: 'view_group',
group_id: groupId,
});
} else {
console.info("Ignoring showScreen for '%s'", screen);
}
@@ -1182,6 +1257,11 @@ module.exports = React.createClass({
});
},
onGroupClick: function(event, groupId) {
event.preventDefault();
dis.dispatch({action: 'view_group', group_id: groupId});
},
onLogoutClick: function(event) {
dis.dispatch({
action: 'logout',
@@ -1231,29 +1311,25 @@ module.exports = React.createClass({
this.showScreen("forgot_password");
},
onReturnToGuestClick: function() {
// reanimate our guest login
if (this.state.guestCreds) {
// TODO: this is probably a bit broken - we don't want to be
// clearing storage when we reanimate the guest creds.
Lifecycle.setLoggedIn(this.state.guestCreds);
this.setState({guestCreds: null});
}
onReturnToAppClick: function() {
// treat it the same as if the user had completed the login
this._onLoggedIn(null);
},
// returns a promise which resolves to the new MatrixClient
onRegistered: function(credentials, teamToken) {
// XXX: These both should be in state or ideally store(s) because we risk not
// rendering the most up-to-date view of state otherwise.
// teamToken may not be truthy
this._teamToken = teamToken;
this._is_registered = true;
Lifecycle.setLoggedIn(credentials);
return Lifecycle.setLoggedIn(credentials);
},
onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show
this.setState({
screen: undefined,
view: VIEWS.LOGGED_IN,
});
this.showScreen("settings");
},
@@ -1264,9 +1340,35 @@ module.exports = React.createClass({
newVersion: latest,
hasNewVersion: current !== latest,
newVersionReleaseNotes: releaseNotes,
checkingForUpdate: null,
});
},
onSendEvent: function(roomId, event) {
const cli = MatrixClientPeg.get();
if (!cli) {
dis.dispatch({action: 'message_send_failed'});
return;
}
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
if (err.name === 'UnknownDeviceError') {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: cli.getRoom(roomId),
});
}
dis.dispatch({action: 'message_send_failed'});
});
},
_setPageSubtitle: function(subtitle='') {
document.title = `Riot ${subtitle}`;
},
updateStatusIndicator: function(state, prevState) {
let notifCount = 0;
@@ -1287,15 +1389,15 @@ module.exports = React.createClass({
PlatformPeg.get().setNotificationCount(notifCount);
}
let title = "Riot ";
let subtitle = '';
if (state === "ERROR") {
title += `[${_t("Offline")}] `;
subtitle += `[${_t("Offline")}] `;
}
if (notifCount > 0) {
title += `[${notifCount}]`;
subtitle += `[${notifCount}]`;
}
document.title = title;
this._setPageSubtitle(subtitle);
},
onUserSettingsClose: function() {
@@ -1321,11 +1423,9 @@ module.exports = React.createClass({
},
render: function() {
// `loading` might be set to false before `loggedIn = true`, causing the default
// (`<Login>`) to be visible for a few MS (say, whilst a request is in-flight to
// the RTS). So in the meantime, use `loggingIn`, which is true between
// actions `on_logging_in` and `on_logged_in`.
if (this.state.loading || this.state.loggingIn) {
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
const Spinner = sdk.getComponent('elements.Spinner');
return (
<div className="mx_MatrixChat_splash">
@@ -1335,7 +1435,7 @@ module.exports = React.createClass({
}
// needs to be before normal PageTypes as you are logged in technically
if (this.state.screen == 'post_registration') {
if (this.state.view === VIEWS.POST_REGISTRATION) {
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return (
<PostRegistration
@@ -1343,38 +1443,42 @@ module.exports = React.createClass({
);
}
// `ready` and `loggedIn` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.loggedIn && this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
*/
const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
{...this.props}
{...this.state}
/>
);
} else if (this.state.loggedIn) {
// we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
return (
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
{ _t('Logout') }
</a>
</div>
);
} else if (this.state.screen == 'register') {
if (this.state.view === VIEWS.LOGGED_IN) {
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
*/
const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
{...this.props}
{...this.state}
/>
);
} else {
// we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
return (
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
{ _t('Logout') }
</a>
</div>
);
}
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.login.Registration');
return (
<Registration
@@ -1394,10 +1498,13 @@ module.exports = React.createClass({
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick}
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
/>
);
} else if (this.state.screen == 'forgot_password') {
}
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return (
<ForgotPassword
@@ -1409,7 +1516,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick}
onLoginClick={this.onLoginClick} />
);
} else {
}
if (this.state.view === VIEWS.LOGIN) {
const Login = sdk.getComponent('structures.login.Login');
return (
<Login
@@ -1423,9 +1532,11 @@ module.exports = React.createClass({
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest}
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
/>
);
}
console.error(`Unknown view ${this.state.view}`);
},
});

View File

@@ -0,0 +1,141 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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, _tJsx } from '../../languageHandler';
import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton';
import dis from '../../dispatcher';
import PropTypes from 'prop-types';
import Modal from '../../Modal';
const GroupTile = React.createClass({
displayName: 'GroupTile',
propTypes: {
groupId: PropTypes.string.isRequired,
},
onClick: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
render: function() {
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
},
});
export default withMatrixClient(React.createClass({
displayName: 'MyGroups',
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
groups: null,
error: null,
};
},
componentWillMount: function() {
this._fetch();
},
_onCreateGroupClick: function() {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
Modal.createDialog(CreateGroupDialog);
},
_fetch: function() {
this.props.matrixClient.getJoinedGroups().done((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
this.setState({groups: null, error: err});
});
},
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let content;
if (this.state.groups) {
const groupNodes = [];
this.state.groups.forEach((g) => {
groupNodes.push(
<div key={g}>
<GroupTile groupId={g} />
</div>,
);
});
content = <div>
<div>{_t('You are a member of these groups:')}</div>
{groupNodes}
</div>;
} else if (this.state.error) {
content = <div className="mx_MyGroups_error">
{_t('Error whilst fetching joined groups')}
</div>;
} else {
content = <Loader />;
}
return <div className="mx_MyGroups">
<SimpleRoomHeader title={ _t("Groups") } />
<div className='mx_MyGroups_joinCreateBox'>
<div className="mx_MyGroups_createBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Create a new group')}
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_t(
'Create a group to represent your community! '+
'Define a set of rooms and your own custom homepage '+
'to mark out your space in the Matrix universe.',
)}
</div>
<div className="mx_MyGroups_joinBox">
<div className="mx_MyGroups_joinCreateHeader">
{_t('Join an existing group')}
</div>
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
</AccessibleButton>
{_tJsx(
'To join an exisitng group you\'ll have to '+
'know its group identifier; this will look '+
'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/,
(sub) => <i>{sub}</i>,
)}
</div>
</div>
<div className="mx_MyGroups_content">
{content}
</div>
</div>;
},
}));

View File

@@ -33,9 +33,6 @@ module.exports = React.createClass({
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
// a TabComplete object
tabComplete: React.PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
@@ -143,12 +140,9 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall ||
this.props.tabComplete.isTabCompleting()
this.props.hasActiveCall
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (this.props.unsentMessageError) {
return STATUS_BAR_EXPANDED_LARGE;
}
@@ -237,8 +231,6 @@ module.exports = React.createClass({
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
// no conn bar trumps unread count since you can't get unread messages
@@ -259,20 +251,6 @@ module.exports = React.createClass({
);
}
if (this.props.tabComplete.isTabCompleting()) {
return (
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
{_t('Auto-complete')}
</div>
</div>
</div>
);
}
if (this.props.unsentMessageError) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">

View File

@@ -22,7 +22,7 @@ limitations under the License.
var React = require("react");
var ReactDOM = require("react-dom");
var q = require("q");
import Promise from 'bluebird';
var classNames = require("classnames");
var Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
@@ -33,7 +33,6 @@ var ContentMessages = require("../../ContentMessages");
var Modal = require("../../Modal");
var sdk = require('../../index');
var CallHandler = require('../../CallHandler');
var TabComplete = require("../../TabComplete");
var Resend = require("../../Resend");
var dis = require("../../dispatcher");
var Tinter = require("../../Tinter");
@@ -47,13 +46,14 @@ import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
var DEBUG = false;
let DEBUG = false;
let debuglog = function() {};
const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
debuglog = console.log.bind(console);
}
module.exports = React.createClass({
@@ -93,6 +93,7 @@ module.exports = React.createClass({
roomId: null,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
// The event to be scrolled to initially
initialEventId: null,
@@ -112,6 +113,7 @@ module.exports = React.createClass({
callState: null,
guestsCanJoin: false,
canPeek: false,
showApps: false,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@@ -141,15 +143,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
this.tabComplete = new TabComplete({
allowLooping: false,
autoEnterTabComplete: true,
onClickCompletes: true,
onStateChange: (isCompleting) => {
this.forceUpdate();
},
});
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@@ -168,8 +161,14 @@ module.exports = React.createClass({
initialEventId: RoomViewStore.getInitialEventId(),
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
};
// finished joining, start waiting for a room and show a spinner. See onRoom.
newState.waitingForRoom = this.state.joining && !newState.joining &&
!RoomViewStore.getJoinError();
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
console.log(
'RVS update:',
@@ -177,12 +176,11 @@ module.exports = React.createClass({
newState.roomAlias,
'loading?', newState.roomLoading,
'joining?', newState.joining,
'initial?', initial,
'waiting?', newState.waitingForRoom,
'shouldPeek?', newState.shouldPeek,
);
// finished joining, start waiting for a room and show a spinner. See onRoom.
newState.waitingForRoom = this.state.joining && !newState.joining &&
!RoomViewStore.getJoinError();
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
@@ -228,13 +226,16 @@ module.exports = React.createClass({
// making it impossible to indicate a newly joined room.
const room = this.state.room;
if (room) {
this._updateAutoComplete(room);
this.tabComplete.loadEntries(room);
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
showApps: this._shouldShowApps(room),
});
this._onRoomLoaded(room);
}
if (!this.state.joining && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (!room) {
} else if (!room && this.state.shouldPeek) {
console.log("Attempting to peek into room %s", this.state.roomId);
this.setState({
peekLoading: true,
@@ -262,13 +263,22 @@ module.exports = React.createClass({
} else if (room) {
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking();
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
});
this._onRoomLoaded(room);
}
},
_shouldShowApps: function(room) {
if (!BROWSER_SUPPORTS_SANDBOX) return false;
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
// any valid widget = show apps
for (let i = 0; i < appsStateEvents.length; i++) {
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
return true;
}
}
return false;
},
componentDidMount: function() {
var call = this._getCallForRoom();
var callState = call ? call.call_state : "ended";
@@ -449,13 +459,13 @@ module.exports = React.createClass({
this._updateConfCallNotification();
this.setState({
callState: callState
callState: callState,
});
break;
case 'forward_event':
case 'appsDrawer':
this.setState({
forwardingEvent: payload.content,
showApps: payload.show,
});
break;
}
@@ -498,9 +508,7 @@ module.exports = React.createClass({
// update the tab complete list as it depends on who most recently spoke,
// and that has probably just changed
if (ev.sender) {
this.tabComplete.onMemberSpoke(ev.sender);
// nb. we don't need to update the new autocomplete here since
// its results are currently ordered purely by search score.
UserProvider.getInstance().onUserSpoke(ev.sender);
}
},
@@ -523,6 +531,7 @@ module.exports = React.createClass({
this._warnAboutEncryption(room);
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
UserProvider.getInstance().setUserListFromRoom(room);
},
_warnAboutEncryption: function(room) {
@@ -698,8 +707,7 @@ module.exports = React.createClass({
this._updateConfCallNotification();
// refresh the tab complete list
this.tabComplete.loadEntries(this.state.room);
this._updateAutoComplete(this.state.room);
UserProvider.getInstance().setUserListFromRoom(this.state.room);
// 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
@@ -767,7 +775,7 @@ module.exports = React.createClass({
onSearchResultsFillRequest: function(backwards) {
if (!backwards) {
return q(false);
return Promise.resolve(false);
}
if (this.state.searchResults.next_batch) {
@@ -777,7 +785,7 @@ module.exports = React.createClass({
return this._handleSearchResult(searchPromise);
} else {
debuglog("no more search results");
return q(false);
return Promise.resolve(false);
}
},
@@ -838,7 +846,7 @@ module.exports = React.createClass({
return;
}
q().then(() => {
Promise.resolve().then(() => {
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
@@ -857,7 +865,7 @@ module.exports = React.createClass({
}
}
}
return q();
return Promise.resolve();
});
},
@@ -1164,8 +1172,13 @@ module.exports = React.createClass({
this.updateTint();
this.setState({
editingRoomSettings: false,
forwardingEvent: null,
});
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.dispatch({action: 'focus_composer'});
},
@@ -1419,14 +1432,6 @@ module.exports = React.createClass({
}
},
_updateAutoComplete: function(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
UserProvider.getInstance().setUserList(members);
},
render: function() {
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
@@ -1463,7 +1468,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite.
var room_alias = this.state.room_alias;
const roomAlias = this.state.roomAlias;
return (
<div className="mx_RoomView">
<RoomHeader ref="header"
@@ -1476,7 +1481,7 @@ module.exports = React.createClass({
onForgetClick={ this.onForgetClick }
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
roomAlias={roomAlias}
spinner={previewBarSpinner}
inviterName={inviterName}
invitedEmail={invitedEmail}
@@ -1554,7 +1559,6 @@ module.exports = React.createClass({
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar
room={this.state.room}
tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
@@ -1576,7 +1580,7 @@ module.exports = React.createClass({
} else if (this.state.uploadingRoomSettings) {
aux = <Loader/>;
} else if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
@@ -1607,11 +1611,13 @@ module.exports = React.createClass({
var auxPanel = (
<AuxPanel ref="auxPanel" room={this.state.room}
userId={MatrixClientPeg.get().credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
onResize={this.onChildResize} >
onResize={this.onChildResize}
showApps={this.state.showApps && !this.state.editingRoomSettings} >
{ aux }
</AuxPanel>
);
@@ -1624,8 +1630,13 @@ module.exports = React.createClass({
if (canSpeak) {
messageComposer =
<MessageComposer
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
room={this.state.room}
onResize={this.onChildResize}
uploadFile={this.uploadFile}
callState={this.state.callState}
opacity={ this.props.opacity }
showApps={ this.state.showApps }
/>;
}
// TODO: Why aren't we storing the term/scope/count in this format

View File

@@ -17,7 +17,7 @@ limitations under the License.
var React = require("react");
var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar');
var q = require("q");
import Promise from 'bluebird';
var KeyCode = require('../../KeyCode');
var DEBUG_SCROLL = false;
@@ -145,7 +145,7 @@ module.exports = React.createClass({
return {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return q(false); },
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};
@@ -386,19 +386,12 @@ module.exports = React.createClass({
debuglog("ScrollPanel: starting "+dir+" fill");
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call. That
// does present the risk that we might not ever actually fire off the
// fill request, so wrap it in a try/catch.
// events) so make sure we set this before firing off the call.
this._pendingFillRequests[dir] = true;
var fillPromise;
try {
fillPromise = this.props.onFillRequest(backwards);
} catch (e) {
this._pendingFillRequests[dir] = false;
throw e;
}
q.finally(fillPromise, () => {
Promise.try(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
this._pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {

View File

@@ -17,7 +17,7 @@ limitations under the License.
var React = require('react');
var ReactDOM = require("react-dom");
var q = require("q");
import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline;
@@ -314,13 +314,13 @@ var TimelinePanel = React.createClass({
if (!this.state[canPaginateKey]) {
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
return q(false);
return Promise.resolve(false);
}
if(!this._timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
this.setState({[canPaginateKey]: false});
return q(false);
return Promise.resolve(false);
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
@@ -353,9 +353,9 @@ var TimelinePanel = React.createClass({
});
},
onMessageListScroll: function() {
onMessageListScroll: function(e) {
if (this.props.onScroll) {
this.props.onScroll();
this.props.onScroll(e);
}
if (this.props.manageReadMarkers) {

View File

@@ -21,7 +21,8 @@ const MatrixClientPeg = require("../../MatrixClientPeg");
const PlatformPeg = require("../../PlatformPeg");
const Modal = require('../../Modal');
const dis = require("../../dispatcher");
const q = require('q');
import sessionStore from '../../stores/SessionStore';
import Promise from 'bluebird';
const packageJson = require('../../../package.json');
const UserSettingsStore = require('../../UserSettingsStore');
const CallMediaHandler = require('../../CallMediaHandler');
@@ -93,8 +94,12 @@ const SETTINGS_LABELS = [
label: 'Hide removed messages',
},
{
id: 'disableMarkdown',
label: 'Disable markdown formatting',
id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting',
},
{
id: 'MessageComposerInput.autoReplaceEmoji',
label: 'Automatically replace plain text Emoji',
},
/*
{
@@ -173,9 +178,6 @@ module.exports = React.createClass({
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
// true if RightPanel is collapsed
collapsedRhs: React.PropTypes.bool,
// Team token for the referral link. If falsy, the referral section will
// not appear
teamToken: React.PropTypes.string,
@@ -205,7 +207,7 @@ module.exports = React.createClass({
this._addThreepid = null;
if (PlatformPeg.get()) {
q().then(() => {
Promise.resolve().then(() => {
return PlatformPeg.get().getAppVersion();
}).done((appVersion) => {
if (this._unmounted) return;
@@ -250,6 +252,12 @@ module.exports = React.createClass({
this.setState({
language: languageHandler.getCurrentLanguage(),
});
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
},
componentDidMount: function() {
@@ -276,12 +284,28 @@ module.exports = React.createClass({
}
},
// `UserSettings` assumes that the client peg will not be null, so give it some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `UserSettings` maintains its own state and if this state
// updates (e.g. during _setStateFromSessionStore) after the client peg has been made
// null (during logout), then it will attempt to re-render and throw errors.
shouldComponentUpdate: function() {
return Boolean(MatrixClientPeg.get());
},
_setStateFromSessionStore: function() {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
_electronSettings: function(ev, settings) {
this.setState({ electron_settings: settings });
},
_refreshMediaDevices: function() {
q().then(() => {
Promise.resolve().then(() => {
return CallMediaHandler.getDevices();
}).then((mediaDevices) => {
// console.log("got mediaDevices", mediaDevices, this._unmounted);
@@ -296,7 +320,7 @@ module.exports = React.createClass({
_refreshFromServer: function() {
const self = this;
q.all([
Promise.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) {
self.setState({
@@ -548,15 +572,16 @@ module.exports = React.createClass({
});
// reject the invites
const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId);
return MatrixClientPeg.get().leave(room.roomId).catch((e) => {
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
});
});
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
q.allSettled(promises).then(() => {
Promise.all(promises).then(() => {
this.setState({
rejectingInvites: false,
});
}).done();
});
},
_onExportE2eKeysClicked: function() {
@@ -626,6 +651,10 @@ module.exports = React.createClass({
},
_renderUserInterfaceSettings: function() {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) =>
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
return (
<div>
<h3>{ _t("User Interface") }</h3>
@@ -633,8 +662,21 @@ module.exports = React.createClass({
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
<table>
<tbody>
<tr>
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
<td>
<input
type="number"
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
onChange={onChange}
/>
</td>
</tr>
</tbody>
</table>
{ this._renderLanguageSetting() }
</div>
</div>
);
@@ -868,6 +910,21 @@ module.exports = React.createClass({
</div>;
},
_renderCheckUpdate: function() {
const platform = PlatformPeg.get();
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
return <div>
<h3>{_t('Updates')}</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
{_t('Check for update')}
</AccessibleButton>
</div>
</div>;
}
return <div />;
},
_renderBulkOptions: function() {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite");
@@ -1168,7 +1225,6 @@ module.exports = React.createClass({
<div className="mx_UserSettings">
<SimpleRoomHeader
title={ _t("Settings") }
collapsedRhs={ this.props.collapsedRhs }
onCancelClick={ this.props.onClose }
/>
@@ -1209,10 +1265,14 @@ module.exports = React.createClass({
<h3>{ _t("Account") }</h3>
<div className="mx_UserSettings_section cadcampoHide">
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
{ _t("Sign out") }
</AccessibleButton>
{ this.state.userHasGeneratedPassword ?
<div className="mx_UserSettings_passwordWarning">
{ _t("To return to your account in future you need to set a password") }
</div> : null
}
{accountJsx}
</div>
@@ -1266,6 +1326,8 @@ module.exports = React.createClass({
</div>
</div>
{this._renderCheckUpdate()}
{this._renderClearCache()}
{this._renderDeactivateAccount()}

View File

@@ -72,9 +72,14 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this._initLoginLogic();
},
componentWillUnmount: function() {
this._unmounted = true;
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
this.setState({
busy: true,
@@ -87,6 +92,9 @@ module.exports = React.createClass({
).then((data) => {
this.props.onLoggedIn(data);
}, (error) => {
if(this._unmounted) {
return;
}
let errorText;
// Some error strings only apply for logging in
@@ -109,8 +117,11 @@ module.exports = React.createClass({
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
});
}).finally(() => {
if(this._unmounted) {
return;
}
this.setState({
busy: false
busy: false,
});
}).done();
},

View File

@@ -17,7 +17,7 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import sdk from '../../../index';
@@ -180,7 +180,7 @@ module.exports = React.createClass({
// will just nop. The point of this being we might not have the email address
// that the user registered with at this stage (depending on whether this
// is the client they initiated registration).
let trackPromise = q(null);
let trackPromise = Promise.resolve(null);
if (this._rtsClient && extra.emailSid) {
// Track referral if this.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
@@ -218,29 +218,29 @@ module.exports = React.createClass({
}
trackPromise.then((teamToken) => {
this.props.onLoggedIn({
return this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token
}, teamToken);
}).then(() => {
return this._setupPushers();
}).then((cli) => {
return this._setupPushers(cli);
});
},
_setupPushers: function() {
_setupPushers: function(matrixClient) {
if (!this.props.brand) {
return q();
return Promise.resolve();
}
return MatrixClientPeg.get().getPushers().then((resp)=>{
return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') {
const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => {
matrixClient.setPusher(emailPusher).done(() => {
console.log("Set email branding to " + this.props.brand);
}, (error) => {
console.error("Couldn't set email branding: " + error);

View File

@@ -0,0 +1,66 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default React.createClass({
displayName: 'GroupAvatar',
propTypes: {
groupId: PropTypes.string,
groupAvatarUrl: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
},
getDefaultProps: function() {
return {
width: 36,
height: 36,
resizeMethod: 'crop',
};
},
getGroupAvatarUrl: function() {
return MatrixClientPeg.get().mxcUrlToHttp(
this.props.groupAvatarUrl,
this.props.width,
this.props.height,
this.props.resizeMethod,
);
},
render: function() {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
return (
<BaseAvatar
name={this.props.groupId[1]}
idName={this.props.groupId}
url={this.getGroupAvatarUrl()}
{...otherProps}
/>
);
},
});

View File

@@ -72,7 +72,7 @@ module.exports = React.createClass({
},
getRoomAvatarUrl: function(props) {
if (!this.props.room) return null;
if (!props.room) return null;
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
@@ -84,7 +84,7 @@ module.exports = React.createClass({
},
getOneToOneAvatar: function(props) {
if (!this.props.room) return null;
if (!props.room) return null;
var mlist = props.room.currentState.members;
var userIds = [];
@@ -126,9 +126,16 @@ module.exports = React.createClass({
},
getFallbackAvatar: function(props) {
if (!this.props.room) return null;
let roomId = null;
if (props.oobData && props.oobData.roomId) {
roomId = this.props.oobData.roomId;
} else if (props.room) {
roomId = props.room.roomId;
} else {
return null;
}
return Avatar.defaultAvatarUrlForString(props.room.roomId);
return Avatar.defaultAvatarUrlForString(roomId);
},
render: function() {

View File

@@ -23,7 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
import Promise from 'bluebird';
import dis from '../../../dispatcher';
const TRUNCATE_QUERY_LIST = 40;
@@ -178,7 +178,7 @@ module.exports = React.createClass({
},
onQueryChanged: function(ev) {
const query = ev.target.value.toLowerCase();
const query = ev.target.value;
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
@@ -271,10 +271,11 @@ module.exports = React.createClass({
query,
searchError: null,
});
const queryLowercase = query.toLowerCase();
const results = [];
MatrixClientPeg.get().getUsers().forEach((user) => {
if (user.userId.toLowerCase().indexOf(query) === -1 &&
user.displayName.toLowerCase().indexOf(query) === -1
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
) {
return;
}
@@ -497,7 +498,7 @@ module.exports = React.createClass({
}
// wait a bit to let the user finish typing
return q.delay(500).then(() => {
return Promise.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {

View File

@@ -0,0 +1,199 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
// We match fairly liberally and leave it up to the server to reject if
// there are invalid characters etc.
const GROUP_REGEX = /^\+(.*?):(.*)$/;
export default React.createClass({
displayName: 'CreateGroupDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
},
_onGroupNameChange: function(e) {
this.setState({
groupName: e.target.value,
});
},
_onGroupIdChange: function(e) {
this.setState({
groupId: e.target.value,
});
},
_onGroupIdBlur: function(e) {
this._checkGroupId();
},
_checkGroupId: function(e) {
const parsedGroupId = this._parseGroupId(this.state.groupId);
let error = null;
if (parsedGroupId === null) {
error = _t(
"Group IDs must be of the form +localpart:%(domain)s",
{domain: MatrixClientPeg.get().getDomain()},
);
} else {
const domain = parsedGroupId[1];
if (domain !== MatrixClientPeg.get().getDomain()) {
error = _t(
"It is currently only possible to create groups on your own home server: "+
"use a group ID ending with %(domain)s",
{domain: MatrixClientPeg.get().getDomain()},
);
}
}
this.setState({
groupIdError: error,
});
return error;
},
_onFormSubmit: function(e) {
e.preventDefault();
if (this._checkGroupId()) return;
const parsedGroupId = this._parseGroupId(this.state.groupId);
const profile = {};
if (this.state.groupName !== '') {
profile.name = this.state.groupName;
}
this.setState({creating: true});
MatrixClientPeg.get().createGroup({
localpart: parsedGroupId[0],
profile: profile,
}).then((result) => {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
});
this.props.onFinished(true);
}).catch((e) => {
this.setState({createError: e});
}).finally(() => {
this.setState({creating: false});
}).done();
},
_onCancel: function() {
this.props.onFinished(false);
},
/**
* Parse a string that may be a group ID
* If the string is a valid group ID, return a list of [localpart, domain],
* otherwise return null.
*
* @param {string} groupId The ID of the group
* @return {string[]} array of localpart, domain
*/
_parseGroupId: function(groupId) {
const matches = GROUP_REGEX.exec(this.state.groupId);
if (!matches || matches.length < 3) {
return null;
}
return [matches[1], matches[2]];
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
if (this.state.creating) {
return <Spinner />;
}
let createErrorNode;
if (this.state.createError) {
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
createErrorNode = <div className="error">
<div>{_t('Room creation failed')}</div>
<div>{this.state.createError.message}</div>
</div>;
}
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Group')}
>
<form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupname">{_t('Group Name')}</label>
</div>
<div>
<input id="groupname" className="mx_CreateGroupDialog_input"
autoFocus={true} size="64"
placeholder={_t('Example')}
onChange={this._onGroupNameChange}
value={this.state.groupName}
/>
</div>
</div>
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupid">{_t('Group ID')}</label>
</div>
<div>
<input id="groupid" className="mx_CreateGroupDialog_input"
size="64"
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
onChange={this._onGroupIdChange}
onBlur={this._onGroupIdBlur}
value={this.state.groupId}
/>
</div>
</div>
<div className="error">
{this.state.groupIdError}
</div>
{createErrorNode}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this._onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
</div>
</form>
</BaseDialog>
);
},
});

View File

@@ -0,0 +1,172 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 Modal from '../../../Modal';
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
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,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if(!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
}).done();
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createDialog(DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
});
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.";
} else {
text = "Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.";
}
text = _t(text, {displayName: displayName});
return (
<div>
<p>{text}</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
{_t('Start verification')}
</button>
<button onClick={this._onShareClicked}>
{_t('Share without verifying')}
</button>
<button onClick={this._onIgnoreClicked}>
{_t('Ignore request')}
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div>
<p>{_t('Loading device info...')}</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
>
{content}
</BaseDialog>
);
},
});

View File

@@ -154,7 +154,7 @@ export default React.createClass({
/>
<input
type="submit"
value={_t("Cancel")}
value={_t("Skip")}
onClick={this.onCancelled}
/>
</div>

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';

View File

@@ -0,0 +1,191 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 url from 'url';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
export default React.createClass({
displayName: 'AppTile',
propTypes: {
id: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
room: React.PropTypes.object.isRequired,
type: React.PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
url: "",
};
},
getInitialState: function() {
return {
loading: false,
widgetUrl: this.props.url,
error: null,
deleting: false,
};
},
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
isScalarUrl: function() {
const scalarUrl = SdkConfig.get().integrations_rest_url;
return scalarUrl && this.props.url.startsWith(scalarUrl);
},
componentWillMount: function() {
if (!this.isScalarUrl()) {
return;
}
// Fetch the token before loading the iframe as we need to mangle the URL
this.setState({
loading: true,
});
this._scalarClient = new ScalarAuthClient();
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param
const u = url.parse(this.props.url);
if (!u.search) {
u.search = "?scalar_token=" + encodeURIComponent(token);
} else {
u.search += "&scalar_token=" + encodeURIComponent(token);
}
this.setState({
error: null,
widgetUrl: u.format(),
loading: false,
});
}, (err) => {
this.setState({
error: err.message,
loading: false,
});
});
},
_onEditClick: function(e) {
console.log("Edit widget ID ", this.props.id);
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
Modal.createDialog(IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
},
_onDeleteClick: function() {
console.log("Delete widget %s", this.props.id);
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).then(() => {
console.log('Deleted widget');
}, (e) => {
console.error('Failed to delete widget', e);
this.setState({deleting: false});
});
},
formatAppTileName: function() {
let appTileName = "No name";
if(this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim();
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
}
return appTileName;
},
render: function() {
let appTileBody;
// Don't render widget if it is in the process of being deleted
if (this.state.deleting) {
return <div></div>;
}
if (this.state.loading) {
appTileBody = (
<div> Loading... </div>
);
} else {
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts";
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
appTileBody = (
<div className="mx_AppTileBody">
<iframe ref="appFrame" src={safeWidgetUrl} allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
}
// editing is done in scalar
const showEditButton = Boolean(this._scalarClient);
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div className="mx_AppTileMenuBar">
{this.formatAppTileName()}
<span className="mx_AppTileMenuBarWidgets">
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>&#946;</span>
{/* Edit widget */}
{showEditButton && <img
src="img/edit.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8" alt="Edit"
onClick={this._onEditClick}
/>}
{/* Delete widget */}
<img src="img/cancel.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
width="8" height="8" alt={_t("Cancel")}
onClick={this._onDeleteClick}
/>
</span>
</div>
{appTileBody}
</div>
);
},
});

View File

@@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import q from 'q';
import Promise from 'bluebird';
/**
* A component which wraps an EditableText, with a spinner while updates take
@@ -148,5 +148,5 @@ EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: function(v) {return q(); },
onSubmit: function(v) {return Promise.resolve(); },
};

View File

@@ -0,0 +1,117 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
import { getDisplayAliasForRoom } from '../../../Rooms';
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+]).*)$/;
export default React.createClass({
statics: {
isPillUrl: (url) => {
return !!REGEX_MATRIXTO.exec(url);
},
isMessagePillUrl: (url) => {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
},
},
props: {
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
url: PropTypes.string,
// Whether the pill is in a message
inMessage: PropTypes.bool,
// The room in which this pill is being rendered
room: PropTypes.instanceOf(Room),
},
render: function() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
let regex = REGEX_MATRIXTO;
if (this.props.inMessage) {
regex = REGEX_LOCAL_MATRIXTO;
}
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = regex.exec(this.props.url) || [];
const resource = matrixToMatch[1]; // The room/user ID
const prefix = matrixToMatch[2]; // The first character of prefix
// Default to the room/user ID
let linkText = resource;
const isUserPill = prefix === '@';
const isRoomPill = prefix === '#' || prefix === '!';
let avatar = null;
let userId;
if (isUserPill) {
// If this user is not a member of this room, default to the empty member
// TODO: This could be improved by doing an async profile lookup
const member = this.props.room.getMember(resource) ||
new RoomMember(null, resource);
if (member) {
userId = member.userId;
linkText = member.name;
avatar = <MemberAvatar member={member} width={16} height={16}/>;
}
} else if (isRoomPill) {
const room = prefix === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getAliases().includes(resource);
}) : MatrixClientPeg.get().getRoom(resource);
if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
avatar = <RoomAvatar room={room} width={16} height={16}/>;
}
}
const classes = classNames({
"mx_UserPill": isUserPill,
"mx_RoomPill": isRoomPill,
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
});
if ((isUserPill || isRoomPill) && avatar) {
return this.props.inMessage ?
<a className={classes} href={this.props.url}>
{avatar}
{linkText}
</a> :
<span className={classes}>
{avatar}
{linkText}
</span>;
} else {
// Deliberately render nothing if the URL isn't recognised
return null;
}
},
});

View File

@@ -46,6 +46,10 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
this._captchaWidgetId = null;
},
componentDidMount: function() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
@@ -75,6 +79,10 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
this._resetRecaptcha();
},
_renderRecaptcha: function(divId) {
if (!global.grecaptcha) {
console.error("grecaptcha not loaded!");
@@ -90,12 +98,18 @@ module.exports = React.createClass({
}
console.log("Rendering to %s", divId);
global.grecaptcha.render(divId, {
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
});
},
_resetRecaptcha: function() {
if (this._captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId);
}
},
_onCaptchaLoaded: function() {
console.log("Loaded recaptcha script.");
try {

View File

@@ -69,10 +69,19 @@ class PasswordLogin extends React.Component {
onSubmitForm(ev) {
ev.preventDefault();
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
this.props.onSubmit(
'', // XXX: Synapse breaks if you send null here:
this.state.phoneCountry,
this.state.phoneNumber,
this.state.password,
);
return;
}
this.props.onSubmit(
this.state.username,
this.state.phoneCountry,
this.state.phoneNumber,
null,
null,
this.state.password,
);
}

View File

@@ -24,7 +24,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
@@ -123,7 +123,7 @@ module.exports = React.createClass({
this.fixupHeight();
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = q(null);
let thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,

View File

@@ -20,7 +20,7 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
@@ -79,7 +79,7 @@ module.exports = React.createClass({
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedThumbnailUrl;
} else if (content.info.thumbnail_url) {
} else if (content.info && content.info.thumbnail_url) {
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
} else {
return null;
@@ -89,7 +89,7 @@ module.exports = React.createClass({
componentDidMount: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
var thumbnailPromise = q(null);
var thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file

View File

@@ -29,6 +29,10 @@ import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
import MatrixClientPeg from '../../../MatrixClientPeg';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
linkifyMatrix(linkify);
@@ -79,6 +83,10 @@ module.exports = React.createClass({
componentDidMount: function() {
this._unmounted = false;
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
this.pillifyLinks(this.refs.content.children);
linkifyElement(this.refs.content, linkifyMatrix.options);
this.calculateUrlPreview();
@@ -90,7 +98,18 @@ module.exports = React.createClass({
setTimeout(() => {
if (this._unmounted) return;
for (let i = 0; i < blocks.length; i++) {
highlight.highlightBlock(blocks[i]);
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
highlight.highlightBlock(blocks[i])
} else {
// Only syntax highlight if there's a class starting with language-
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
return cl.startsWith('language-');
});
if (classes.length != 0) {
highlight.highlightBlock(blocks[i]);
}
}
}
}, 10);
}
@@ -131,9 +150,15 @@ module.exports = React.createClass({
if (this.props.showUrlPreview && !this.state.links.length) {
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// de-dup the links (but preserve ordering)
const seen = new Set();
links = links.filter((link) => {
if (seen.has(link)) return false;
seen.add(link);
return true;
});
this.setState({ links: links });
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
@@ -144,14 +169,38 @@ module.exports = React.createClass({
}
},
pillifyLinks: function(nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href");
// If the link is a (localised) matrix.to link, replace it with a pill
const Pill = sdk.getComponent('elements.Pill');
if (Pill.isMessagePillUrl(href)) {
const pillContainer = document.createElement('span');
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pill = <Pill url={href} inMessage={true} room={room}/>;
ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node);
}
} else if (node.children && node.children.length) {
this.pillifyLinks(node.children);
}
}
},
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href"))
{
if (this.isLinkPreviewable(node)) {
links.push(node);
links.push(node.getAttribute("href"));
}
}
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
@@ -213,10 +262,9 @@ module.exports = React.createClass({
onEmoteSenderClick: function(event) {
const mxEvent = this.props.mxEvent;
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
dis.dispatch({
action: 'insert_displayname',
displayname: name.replace(' (IRC)', ''),
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var q = require("q");
import Promise from 'bluebird';
var React = require('react');
var ObjectUtils = require("../../../ObjectUtils");
var MatrixClientPeg = require('../../../MatrixClientPeg');
@@ -104,7 +104,7 @@ module.exports = React.createClass({
}
if (oldCanonicalAlias !== this.state.canonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
promises = [q.all(promises).then(
promises = [Promise.all(promises).then(
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.canonical_alias", {
alias: this.state.canonicalAlias

View File

@@ -13,7 +13,7 @@ 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.
*/
var q = require("q");
import Promise from 'bluebird';
var React = require('react');
var sdk = require('../../../index');
@@ -72,7 +72,7 @@ module.exports = React.createClass({
saveSettings: function() { // : Promise
if (!this.state.hasChanged) {
return q(); // They didn't explicitly give a color to save.
return Promise.resolve(); // They didn't explicitly give a color to save.
}
var originalState = this.getInitialState();
if (originalState.primary_color !== this.state.primary_color ||
@@ -92,7 +92,7 @@ module.exports = React.createClass({
}
});
}
return q(); // no color diff
return Promise.resolve(); // no color diff
},
_getColorIndex: function(scheme) {

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var q = require("q");
import Promise from 'bluebird';
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");

View File

@@ -0,0 +1,197 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 MatrixClientPeg from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'AppsDrawer',
propTypes: {
room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
apps: this._getApps(),
};
},
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
componentDidMount: function() {
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
if (this.state.apps && this.state.apps.length < 1) {
this.onClickAddWidget();
}
// TODO -- Handle Scalar errors
// },
// (err) => {
// this.setState({
// scalar_error: err,
// });
});
}
},
componentWillUnmount: function() {
ScalarMessaging.stopListening();
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
},
/**
* Encodes a URI according to a set of template variables. Variables will be
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
encodeUri: function(pathTemplate, variables) {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
}
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key]),
);
}
return pathTemplate;
},
_initAppConfig: function(appId, app) {
const user = MatrixClientPeg.get().getUser(this.props.userId);
const params = {
'$matrix_user_id': this.props.userId,
'$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) : '',
};
if(app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
});
}
app.id = appId;
app.name = app.name || app.type;
app.url = this.encodeUri(app.url, params);
return app;
},
onRoomStateEvents: function(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
return;
}
this._updateApps();
},
_getApps: function() {
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
if (!appsStateEvents) {
return [];
}
return appsStateEvents.filter((ev) => {
return ev.getContent().type && ev.getContent().url;
}).map((ev) => {
return this._initAppConfig(ev.getStateKey(), ev.getContent());
});
},
_updateApps: function() {
const apps = this._getApps();
if (apps.length < 1) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
this.setState({
apps: apps,
});
},
onClickAddWidget: function(e) {
if (e) {
e.preventDefault();
}
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
null;
Modal.createDialog(IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
},
render: function() {
const apps = this.state.apps.map(
(app, index, arr) => {
return <AppTile
key={app.id}
id={app.id}
url={app.url}
name={app.name}
type={app.type}
fullWidth={arr.length<2 ? true : false}
room={this.props.room}
userId={this.props.userId}
/>;
});
const addWidget = this.state.apps && this.state.apps.length < 2 &&
(<div onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
className="mx_AddWidget_button"
title={_t('Add a widget')}>
[+] {_t('Add a widget')}
</div>);
return (
<div className="mx_AppsDrawer">
<div id="apps" className="mx_AppsContainer">
{apps}
</div>
{addWidget}
</div>
);
},
});

View File

@@ -5,7 +5,8 @@ import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Q from 'q';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import {getCompletions} from '../../../autocomplete/Autocompleter';
@@ -39,26 +40,62 @@ export default class Autocomplete extends React.Component {
};
}
async componentWillReceiveProps(props, state) {
if (props.query === this.props.query) {
return null;
}
return await this.complete(props.query, props.selection);
}
async complete(query, selection) {
let forceComplete = this.state.forceComplete;
const completionPromise = getCompletions(query, selection, forceComplete);
this.completionPromise = completionPromise;
const completions = await this.completionPromise;
// There's a newer completion request, so ignore results.
if (completionPromise !== this.completionPromise) {
componentWillReceiveProps(newProps, state) {
// Query hasn't changed so don't try to complete it
if (newProps.query === this.props.query) {
return;
}
const completionList = flatMap(completions, provider => provider.completions);
this.complete(newProps.query, newProps.selection);
}
complete(query, selection) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
}
if (query === "") {
this.setState({
// Clear displayed completions
completions: [],
completionList: [],
// Reset selected completion
selectionOffset: COMPOSER_SELECTED,
// Hide the autocomplete box
hide: true,
});
return Promise.resolve(null);
}
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) {
autocompleteDelay = 0;
}
const deferred = Promise.defer();
this.debounceCompletionsRequest = setTimeout(() => {
this.processQuery(query, selection).then(() => {
deferred.resolve();
});
}, autocompleteDelay);
return deferred.promise;
}
processQuery(query, selection) {
return getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
return;
}
this.processCompletions(completions);
});
}
processCompletions(completions) {
const completionList = flatMap(completions, (provider) => provider.completions);
// Reset selection when completion list becomes empty.
let selectionOffset = COMPOSER_SELECTED;
@@ -69,33 +106,26 @@ export default class Autocomplete extends React.Component {
const currentSelection = this.state.selectionOffset === 0 ? null :
this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex(
completion => completion.completion === currentSelection);
(completion) => completion.completion === currentSelection);
if (selectionOffset === -1) {
selectionOffset = COMPOSER_SELECTED;
} else {
selectionOffset++; // selectionOffset is 1-indexed!
}
} else {
// If no completions were returned, we should turn off force completion.
forceComplete = false;
}
let hide = this.state.hide;
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
newMatches = completions.map(completion => !!completion.command.command);
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
if (!isEqual(oldMatches, newMatches)) {
hide = false;
}
// If `completion.command.command` is truthy, then a provider has matched with the query
const anyMatches = completions.some((completion) => !!completion.command.command);
hide = !anyMatches;
this.setState({
completions,
completionList,
selectionOffset,
hide,
forceComplete,
// Force complete is turned off each time since we can't edit the query in that case
forceComplete: false,
});
}
@@ -146,12 +176,13 @@ export default class Autocomplete extends React.Component {
}
forceComplete() {
const done = Q.defer();
const done = Promise.defer();
this.setState({
forceComplete: true,
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
done.resolve();
done.resolve(this.countCompletions());
});
});
return done.promise;
@@ -169,7 +200,7 @@ export default class Autocomplete extends React.Component {
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset});
this.setState({selectionOffset, hide: false});
}
componentDidUpdate() {
@@ -185,21 +216,24 @@ export default class Autocomplete extends React.Component {
}
}
setState(state, func) {
super.setState(state, func);
}
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 1;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
const className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
const componentPosition = position;
position++;
let onMouseOver = () => this.setSelection(componentPosition);
let onClick = () => {
const onMouseOver = () => this.setSelection(componentPosition);
const onClick = () => {
this.setSelection(componentPosition);
this.onCompletionClicked();
};
@@ -220,7 +254,7 @@ export default class Autocomplete extends React.Component {
{completionResult.provider.renderCompletions(completions)}
</div>
) : null;
}).filter(completion => !!completion);
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={(e) => this.container = e}>

View File

@@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import { _t, _tJsx} from '../../../languageHandler';
import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({
@@ -28,6 +30,8 @@ module.exports = React.createClass({
propTypes: {
// js-sdk room object
room: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
showApps: React.PropTypes.bool,
// Conference Handler implementation
conferenceHandler: React.PropTypes.object,
@@ -70,10 +74,10 @@ module.exports = React.createClass({
},
render: function() {
var CallView = sdk.getComponent("voip.CallView");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileDropTarget = null;
let fileDropTarget = null;
if (this.props.draggingFile) {
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
@@ -87,14 +91,13 @@ module.exports = React.createClass({
);
}
var conferenceCallNotification = null;
let conferenceCallNotification = null;
if (this.props.displayConfCallNotification) {
let supportedText = '';
let joinNode;
if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = _t(" (unsupported)");
}
else {
} else {
joinNode = (<span>
{_tJsx(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
@@ -105,7 +108,6 @@ module.exports = React.createClass({
]
)}
</span>);
}
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
// but there are translations for this in the languages we do have so I'm leaving it for now.
@@ -118,7 +120,7 @@ module.exports = React.createClass({
);
}
var callView = (
const callView = (
<CallView ref="callView" room={this.props.room}
ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize}
@@ -126,8 +128,17 @@ module.exports = React.createClass({
/>
);
let appsDrawer = null;
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
appsDrawer = <AppsDrawer ref="appsDrawer"
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}/>;
}
return (
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
{ appsDrawer }
{ fileDropTarget }
{ callView }
{ conferenceCallNotification }

View File

@@ -24,7 +24,7 @@ var Modal = require('../../../Modal');
var sdk = require('../../../index');
var TextForEvent = require('../../../TextForEvent');
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
var ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
@@ -59,7 +59,7 @@ var MAX_READ_AVATARS = 5;
// | '--------------------------------------' |
// '----------------------------------------------------------'
module.exports = WithMatrixClient(React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'EventTile',
propTypes: {
@@ -193,13 +193,12 @@ module.exports = WithMatrixClient(React.createClass({
}
},
_verifyEvent: function(mxEvent) {
var verified = null;
if (mxEvent.isEncrypted()) {
verified = this.props.matrixClient.isEventSenderVerified(mxEvent);
_verifyEvent: async function(mxEvent) {
if (!mxEvent.isEncrypted()) {
return;
}
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
this.setState({
verified: verified
});
@@ -336,6 +335,7 @@ module.exports = WithMatrixClient(React.createClass({
suppressAnimation={this._suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts}
showTwelveHour={this.props.isTwelveHour}
/>
);
}
@@ -357,10 +357,10 @@ module.exports = WithMatrixClient(React.createClass({
},
onSenderProfileClick: function(event) {
var mxEvent = this.props.mxEvent;
const mxEvent = this.props.mxEvent;
dis.dispatch({
action: 'insert_displayname',
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},

View File

@@ -17,7 +17,6 @@
import React from 'react';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
@@ -26,11 +25,6 @@ module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
currentRoomId: React.PropTypes.string.isRequired,
/* the MatrixEvent to be forwarded */
mxEvent: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func.isRequired,
},
@@ -44,7 +38,6 @@ module.exports = React.createClass({
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this._onKeyDown);
},
@@ -54,30 +47,9 @@ module.exports = React.createClass({
sideOpacity: 1.0,
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
document.removeEventListener('keydown', this._onKeyDown);
},
onAction: function(payload) {
if (payload.action === 'view_room') {
const event = this.props.mxEvent;
const Client = MatrixClientPeg.get();
Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: Client.getRoom(payload.room_id),
});
}
dis.dispatch({action: 'message_send_failed'});
});
if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick();
}
},
_onKeyDown: function(ev) {
switch (ev.keyCode) {
case KeyCode.ESCAPE:

View File

@@ -36,12 +36,12 @@ import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = WithMatrixClient(React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
propTypes: {
@@ -136,8 +136,12 @@ module.exports = WithMatrixClient(React.createClass({
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
var devices = this.props.matrixClient.getStoredDevicesForUser(userId);
this.setState({devices: devices});
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
this.setState({devices: devices});
});
}
},
@@ -204,14 +208,15 @@ module.exports = WithMatrixClient(React.createClass({
var client = this.props.matrixClient;
var self = this;
client.downloadKeys([member.userId], true).finally(function() {
client.downloadKeys([member.userId], true).then(() => {
return client.getStoredDevicesForUser(member.userId);
}).finally(function() {
self._cancelDeviceList = null;
}).done(function() {
}).done(function(devices) {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
var devices = client.getStoredDevicesForUser(member.userId);
self._disambiguateDevices(devices);
self.setState({devicesLoading: false, devices: devices});
}, function(err) {

View File

@@ -17,7 +17,7 @@ var React = require('react');
import { _t } from '../../../languageHandler';
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var q = require('q');
import Promise from 'bluebird';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var Entities = require("../../../Entities");

View File

@@ -13,16 +13,14 @@ 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.
*/
var React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
var CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
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 classNames from 'classnames';
import UserSettingsStore from '../../../UserSettingsStore';
@@ -32,19 +30,17 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onShowAppsClick = this.onShowAppsClick.bind(this);
this.onHideAppsClick = this.onHideAppsClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this.onPageUnload = this.onPageUnload.bind(this);
this.state = {
autocompleteQuery: '',
@@ -57,7 +53,6 @@ export default class MessageComposer extends React.Component {
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
};
}
componentDidMount() {
@@ -66,21 +61,12 @@ export default class MessageComposer extends React.Component {
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
window.addEventListener('beforeunload', this.onPageUnload);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
}
window.removeEventListener('beforeunload', this.onPageUnload);
}
onPageUnload(event) {
if (this.messageComposerInput) {
this.messageComposerInput.sentHistory.saveLastTextEntry();
}
}
onEvent(event) {
@@ -127,7 +113,7 @@ export default class MessageComposer extends React.Component {
if(shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) {
for(var i=0; i<files.length; i++) {
for(let i=0; i<files.length; i++) {
this.props.uploadFile(files[i]);
}
}
@@ -139,7 +125,7 @@ export default class MessageComposer extends React.Component {
}
onHangupClick() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
const call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (!call) {
return;
@@ -152,20 +138,68 @@ export default class MessageComposer extends React.Component {
});
}
// _startCallApp(isAudioConf) {
// dis.dispatch({
// action: 'appsDrawer',
// show: true,
// });
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
// let appsStateEvent = {};
// if (appsStateEvents) {
// appsStateEvent = appsStateEvents.getContent();
// }
// if (!appsStateEvent.videoConf) {
// appsStateEvent.videoConf = {
// type: 'jitsi',
// // FIXME -- This should not be localhost
// url: 'http://localhost:8000/jitsi.html',
// data: {
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
// isAudioConf: isAudioConf,
// },
// };
// MatrixClientPeg.get().sendStateEvent(
// this.props.room.roomId,
// 'im.vector.modular.widgets',
// appsStateEvent,
// '',
// ).then(() => console.log('Sent state'), (e) => console.error(e));
// }
// }
onCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId,
});
// this._startCallApp(false);
}
onVoiceCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({
action: 'place_call',
type: 'voice',
type: "voice",
room_id: this.props.room.roomId,
});
// this._startCallApp(true);
}
onShowAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: true,
});
}
onHideAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {
@@ -179,21 +213,6 @@ export default class MessageComposer extends React.Component {
this.setState({inputState});
}
onUpArrow() {
return this.refs.autocomplete.onUpArrow();
}
onDownArrow() {
return this.refs.autocomplete.onDownArrow();
}
_tryComplete(): boolean {
if (this.refs.autocomplete) {
return this.refs.autocomplete.onCompletionClicked();
}
return false;
}
_onAutocompleteConfirm(range, completion) {
if (this.messageComposerInput) {
this.messageComposerInput.setDisplayedCompletion(range, completion);
@@ -216,19 +235,18 @@ export default class MessageComposer extends React.Component {
}
render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
var controls = [];
const controls = [];
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
</div>
</div>,
);
let e2eImg, e2eTitle, e2eClass;
@@ -247,16 +265,15 @@ export default class MessageComposer extends React.Component {
controls.push(
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
alt={e2eTitle} title={e2eTitle}
/>
/>,
);
var callButton, videoCallButton, hangupButton;
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
</div>;
}
else {
} else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
@@ -267,14 +284,29 @@ export default class MessageComposer extends React.Component {
</div>;
}
var canSendMessages = this.props.room.currentState.maySendMessage(
// Apps
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
if (this.props.showApps) {
hideAppsButton =
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
</div>;
} else {
showAppsButton =
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
</div>;
}
}
const canSendMessages = this.props.room.currentState.maySendMessage(
MatrixClientPeg.get().credentials.userId);
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
var uploadButton = (
const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title={ _t('Upload file') }>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
@@ -290,8 +322,7 @@ export default class MessageComposer extends React.Component {
title={_t("Show Text Formatting Toolbar")}
src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked}
style={{visibility: this.state.showFormatting ||
!UserSettingsStore.isFeatureEnabled('rich_text_editor') ? 'hidden' : 'visible'}}
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
key="controls_formatting" />
);
@@ -300,58 +331,40 @@ export default class MessageComposer extends React.Component {
controls.push(
<MessageComposerInput
ref={c => this.messageComposerInput = c}
ref={(c) => this.messageComposerInput = c}
key="controls_input"
onResize={this.props.onResize}
room={this.props.room}
placeholder={placeholderText}
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onFilesPasted={this.uploadFiles}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
formattingButton,
uploadButton,
hangupButton,
callButton,
videoCallButton
videoCallButton,
showAppsButton,
hideAppsButton,
);
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') }
</div>
</div>,
);
}
let autoComplete;
if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) {
autoComplete = <div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref="autocomplete"
onConfirm={this._onAutocompleteConfirm}
query={this.state.autocompleteQuery}
selection={this.state.selection} />
</div>;
}
const {style, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
name => {
(name) => {
const active = style.includes(name) || blockType === name;
const suffix = active ? '-o-n' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
const className = classNames("mx_MessageComposer_format_button", {
mx_MessageComposer_format_button_disabled: disabled,
mx_filterFlipColor: true,
});
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={ _t(name) }
onMouseDown={disabled ? null : onFormatButtonClicked}
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
@@ -365,30 +378,26 @@ export default class MessageComposer extends React.Component {
{controls}
</div>
</div>
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons}
<div style={{flex: 1}}></div>
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title={ _t("Hide Text Formatting Toolbar") }
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>: null
}
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons}
<div style={{flex: 1}}></div>
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title={ _t("Hide Text Formatting Toolbar") }
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>
</div>
);
}
}
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
@@ -403,5 +412,8 @@ MessageComposer.propTypes = {
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number
opacity: React.PropTypes.number,
// string representing the current room app drawer state
showApps: React.PropTypes.bool,
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,470 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
var React = require("react");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
var Markdown = require("../../../Markdown");
var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
export function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({
action: 'message_send_failed',
});
}
/*
* The textInput part of the MessageComposer
*/
export default React.createClass({
displayName: 'MessageComposerInput',
statics: {
// the height we limit the composer to
MAX_HEIGHT: 100,
},
propTypes: {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired,
// callback to handle files pasted into the composer
onFilesPasted: React.PropTypes.func,
},
componentWillMount: function() {
this.oldScrollHeight = 0;
this.markdownEnabled = !UserSettingsStore.getSyncedSetting('disableMarkdown', false);
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent
data: [],
// The position in data currently displayed
position: -1,
// The room the history is for.
roomId: null,
// The original text before they hit UP
originalText: null,
// The textarea element to set text to.
element: null,
init: function(element, roomId) {
this.roomId = roomId;
this.element = element;
this.position = -1;
var storedData = window.sessionStorage.getItem(
"history_" + roomId
);
if (storedData) {
this.data = JSON.parse(storedData);
}
if (this.roomId) {
this.setLastTextEntry();
}
},
push: function(text) {
// store a message in the sent history
this.data.unshift(text);
window.sessionStorage.setItem(
"history_" + this.roomId,
JSON.stringify(this.data)
);
// reset history position
this.position = -1;
this.originalText = null;
},
// move in the history. Returns true if we managed to move.
next: function(offset) {
if (this.position === -1) {
// user is going into the history, save the current line.
this.originalText = this.element.value;
}
else {
// user may have modified this line in the history; remember it.
this.data[this.position] = this.element.value;
}
if (offset > 0 && this.position === (this.data.length - 1)) {
// we've run out of history
return false;
}
// retrieve the next item (bounded).
var newPosition = this.position + offset;
newPosition = Math.max(-1, newPosition);
newPosition = Math.min(newPosition, this.data.length - 1);
this.position = newPosition;
if (this.position !== -1) {
// show the message
this.element.value = this.data[this.position];
}
else if (this.originalText !== undefined) {
// restore the original text the user was typing.
this.element.value = this.originalText;
}
self.resizeInput();
return true;
},
saveLastTextEntry: function() {
// save the currently entered text in order to restore it later.
// NB: This isn't 'originalText' because we want to restore
// sent history items too!
var text = this.element.value;
window.sessionStorage.setItem("input_" + this.roomId, text);
},
setLastTextEntry: function() {
var text = window.sessionStorage.getItem("input_" + this.roomId);
if (text) {
this.element.value = text;
self.resizeInput();
}
}
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.sentHistory.init(
this.refs.textarea,
this.props.room.roomId
);
this.resizeInput();
if (this.props.tabComplete) {
this.props.tabComplete.setTextArea(this.refs.textarea);
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this.sentHistory.saveLastTextEntry();
},
onAction: function(payload) {
var textarea = this.refs.textarea;
switch (payload.action) {
case 'focus_composer':
textarea.focus();
break;
case 'insert_displayname':
if (textarea.value.length) {
var left = textarea.value.substring(0, textarea.selectionStart);
var right = textarea.value.substring(textarea.selectionEnd);
if (right.length) {
left += payload.displayname;
}
else {
left = left.replace(/( ?)$/, " " + payload.displayname);
}
textarea.value = left + right;
textarea.focus();
textarea.setSelectionRange(left.length, left.length);
}
else {
textarea.value = payload.displayname + ": ";
textarea.focus();
}
break;
}
},
onKeyDown: function(ev) {
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
var input = this.refs.textarea.value;
if (input.length === 0) {
ev.preventDefault();
return;
}
this.sentHistory.push(input);
this.onEnter(ev);
}
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
var oldSelectionStart = this.refs.textarea.selectionStart;
// Remember the keyCode because React will recycle the synthetic event
var keyCode = ev.keyCode;
// set a callback so we can see if the cursor position changes as
// a result of this event. If it doesn't, we cycle history.
setTimeout(() => {
if (this.refs.textarea.selectionStart == oldSelectionStart) {
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
this.resizeInput();
}
}, 0);
}
if (this.props.tabComplete) {
this.props.tabComplete.onKeyDown(ev);
}
var self = this;
setTimeout(function() {
if (self.refs.textarea && self.refs.textarea.value != '') {
self.onTypingActivity();
} else {
self.onFinishedTyping();
}
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
},
resizeInput: function() {
// scrollHeight is at least equal to clientHeight, so we have to
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
var newHeight = Math.min(this.refs.textarea.scrollHeight,
this.constructor.MAX_HEIGHT);
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
this.oldScrollHeight = this.refs.textarea.scrollHeight;
if (this.props.onResize) {
// kick gemini-scrollbar to re-layout
this.props.onResize();
}
},
onKeyUp: function(ev) {
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
ev.keyCode === KeyCode.DELETE ||
ev.keyCode === KeyCode.BACKSPACE)
{
this.resizeInput();
}
},
onEnter: function(ev) {
var contentText = this.refs.textarea.value;
// bodge for now to set markdown state on/off. We probably want a separate
// area for "local" commands which don't hit out to the server.
if (contentText.indexOf("/markdown") === 0) {
ev.preventDefault();
this.refs.textarea.value = '';
if (contentText.indexOf("/markdown on") === 0) {
this.markdownEnabled = true;
}
else if (contentText.indexOf("/markdown off") === 0) {
this.markdownEnabled = false;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Unknown command"),
description: _t("Usage") + ": /markdown on|off",
});
}
return;
}
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
if (cmd) {
ev.preventDefault();
if (!cmd.error) {
this.refs.textarea.value = '';
}
if (cmd.promise) {
cmd.promise.done(function() {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Server error"),
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
});
});
}
else if (cmd.error) {
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Command error"),
description: cmd.error,
});
}
return;
}
var isEmote = /^\/me( |$)/i.test(contentText);
var sendMessagePromise;
if (isEmote) {
contentText = contentText.substring(4);
}
else if (contentText[0] === '/') {
contentText = contentText.substring(1);
}
let send_markdown = false;
let mdown;
if (this.markdownEnabled) {
mdown = new Markdown(contentText);
send_markdown = !mdown.isPlainText();
}
if (send_markdown) {
const htmlText = mdown.toHTML();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
}
else {
if (mdown) contentText = mdown.toPlaintext();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
}
sendMessagePromise.done(function(res) {
dis.dispatch({
action: 'message_sent'
});
}, (e) => onSendMessageFailed(e, this.props.room));
this.refs.textarea.value = '';
this.resizeInput();
ev.preventDefault();
},
onTypingActivity: function() {
this.isTyping = true;
if (!this.userTypingTimer) {
this.sendTyping(true);
}
this.startUserTypingTimer();
this.startServerTypingTimer();
},
onFinishedTyping: function() {
this.isTyping = false;
this.sendTyping(false);
this.stopUserTypingTimer();
this.stopServerTypingTimer();
},
startUserTypingTimer: function() {
this.stopUserTypingTimer();
var self = this;
this.userTypingTimer = setTimeout(function() {
self.isTyping = false;
self.sendTyping(self.isTyping);
self.userTypingTimer = null;
}, TYPING_USER_TIMEOUT);
},
stopUserTypingTimer: function() {
if (this.userTypingTimer) {
clearTimeout(this.userTypingTimer);
this.userTypingTimer = null;
}
},
startServerTypingTimer: function() {
if (!this.serverTypingTimer) {
var self = this;
this.serverTypingTimer = setTimeout(function() {
if (self.isTyping) {
self.sendTyping(self.isTyping);
self.startServerTypingTimer();
}
}, TYPING_SERVER_TIMEOUT / 2);
}
},
stopServerTypingTimer: function() {
if (this.serverTypingTimer) {
clearTimeout(this.servrTypingTimer);
this.serverTypingTimer = null;
}
},
sendTyping: function(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT
).done();
},
refreshTyping: function() {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = null;
}
},
onInputClick: function(ev) {
this.refs.textarea.focus();
},
_onPaste: function(ev) {
const items = ev.clipboardData.items;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
}
}
if (files.length && this.props.onFilesPasted) {
this.props.onFilesPasted(files);
return true;
}
return false;
},
render: function() {
return (
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea dir="auto" autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
onPaste={this._onPaste}
/>
</div>
);
}
});

View File

@@ -66,6 +66,9 @@ module.exports = React.createClass({
// Timestamp when the receipt was read
timestamp: React.PropTypes.number,
// True to show twelve hour format, false otherwise
showTwelveHour: React.PropTypes.bool,
},
getDefaultProps: function() {
@@ -172,7 +175,7 @@ module.exports = React.createClass({
if (this.props.timestamp) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp))}
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)}
);
}

View File

@@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require('classnames');
var sdk = require('../../../index');
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
var dis = require("../../../dispatcher");
var rate_limited_func = require('../../../ratelimitedfunc');
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import dis from "../../../dispatcher";
import RateLimitedFunc from '../../../ratelimitedfunc';
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import {CancelButton} from './SimpleRoomHeader';
@@ -58,7 +58,7 @@ module.exports = React.createClass({
},
componentDidMount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
// When a room name occurs, RoomState.events is fired *before*
@@ -79,14 +79,14 @@ module.exports = React.createClass({
if (this.props.room) {
this.props.room.removeListener("Room.name", this._onRoomNameChange);
}
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
}
},
_onRoomStateEvents: function(event, state) {
if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
@@ -94,7 +94,8 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
_rateLimitedUpdate: new rate_limited_func(function() {
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500),
@@ -109,15 +110,14 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
}
changeAvatar.onFileSelected(ev).catch(function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const errMsg = (typeof err === "string") ? err : (err.error || "");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set avatar: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: _t("Error"),
@@ -133,10 +133,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new name for the room
*
* Returns undefined if we didn't let the user edit the room name
* @return {?string} newName or undefined if we didn't let the user edit the room name
*/
getEditedName: function() {
var newName;
let newName;
if (this.refs.nameEditor) {
newName = this.refs.nameEditor.getRoomName();
}
@@ -146,10 +146,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new topic for the room
*
* Returns undefined if we didn't let the user edit the room topic
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
*/
getEditedTopic: function() {
var newTopic;
let newTopic;
if (this.refs.topicEditor) {
newTopic = this.refs.topicEditor.getTopic();
}
@@ -157,38 +157,31 @@ module.exports = React.createClass({
},
render: function() {
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
var header;
var name = null;
var searchStatus = null;
var topic_el = null;
var cancel_button = null;
var spinner = null;
var save_button = null;
var settings_button = null;
let name = null;
let searchStatus = null;
let topicElement = null;
let cancelButton = null;
let spinner = null;
let saveButton = null;
let settingsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
let canSetRoomTopic;
if (this.props.editing) {
// calculate permissions. XXX: this should be done on mount or something
var user_id = MatrixClientPeg.get().credentials.userId;
const userId = MatrixClientPeg.get().credentials.userId;
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
'm.room.avatar', user_id
);
var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
'm.room.topic', user_id
);
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
save_button = (
saveButton = (
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
{_t("Save")}
</AccessibleButton>
@@ -196,39 +189,41 @@ module.exports = React.createClass({
}
if (this.props.onCancelClick) {
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
}
if (this.props.saving) {
var Spinner = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
}
if (can_set_room_name) {
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
if (canSetRoomName) {
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
}
else {
var searchStatus;
} else {
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
if (this.props.searchInfo &&
this.props.searchInfo.searchCount !== undefined &&
this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
</div>;
}
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
var settingsHint = false;
var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
let settingsHint = false;
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
if (members) {
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!name || !name.getContent().name) {
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!nameEvent || !nameEvent.getContent().name) {
settingsHint = true;
}
}
}
var roomName = _t("Join Room");
let roomName = _t("Join Room");
if (this.props.oobData && this.props.oobData.name) {
roomName = this.props.oobData.name;
} else if (this.props.room) {
@@ -243,24 +238,25 @@ module.exports = React.createClass({
</div>;
}
if (can_set_room_topic) {
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
if (canSetRoomTopic) {
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
} else {
var topic;
let topic;
if (this.props.room) {
var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (ev) {
topic = ev.getContent().topic;
}
}
if (topic) {
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
}
}
var roomAvatar = null;
if (can_set_room_avatar) {
let roomAvatar = null;
if (canSetRoomAvatar) {
roomAvatar = (
<div className="mx_RoomHeader_avatarPicker">
<div onClick={ this.onAvatarPickerClick }>
@@ -276,8 +272,7 @@ module.exports = React.createClass({
</div>
</div>
);
}
else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
roomAvatar = (
<div onClick={this.props.onSettingsClick}>
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
@@ -285,9 +280,8 @@ module.exports = React.createClass({
);
}
var settings_button;
if (this.props.onSettingsClick) {
settings_button =
settingsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>;
@@ -301,61 +295,58 @@ module.exports = React.createClass({
// </div>;
// }
var forget_button;
let forgetButton;
if (this.props.onForgetClick) {
forget_button =
forgetButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</AccessibleButton>;
}
let search_button;
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
search_button =
searchButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
var rightPanel_buttons;
let rightPanelButtons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
rightPanelButtons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>;
}
var right_row;
let rightRow;
if (!this.props.editing) {
right_row =
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
{ search_button }
{ rightPanel_buttons }
{ settingsButton }
{ forgetButton }
{ searchButton }
{ rightPanelButtons }
</div>;
}
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topic_el }
</div>
</div>
{spinner}
{save_button}
{cancel_button}
{right_row}
</div>;
return (
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
{ header }
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topicElement }
</div>
</div>
{spinner}
{saveButton}
{cancelButton}
{rightRow}
</div>
</div>
);
},

View File

@@ -18,7 +18,7 @@ limitations under the License.
'use strict';
var React = require("react");
var ReactDOM = require("react-dom");
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
@@ -33,11 +33,28 @@ var Receipt = require('../../../utils/Receipt');
const HIDE_CONFERENCE_CHANS = true;
const VERBS = {
'm.favourite': 'favourite',
'im.vector.fake.direct': 'tag direct chat',
'im.vector.fake.recent': 'restore',
'm.lowpriority': 'demote',
function phraseForSection(section) {
// These would probably be better as individual strings,
// but for some reason we have translations for these strings
// as-is, so keeping it like this for now.
let verb;
switch (section) {
case 'm.favourite':
verb = _t('to favourite');
break;
case 'im.vector.fake.direct':
verb = _t('to tag direct chat');
break;
case 'im.vector.fake.recent':
verb = _t('to restore');
break;
case 'm.lowpriority':
verb = _t('to demote');
break;
default:
return _t('Drop here to tag %(section)s', {section: section});
}
return _t('Drop here %(toAction)s', {toAction: verb});
};
module.exports = React.createClass({
@@ -478,17 +495,25 @@ module.exports = React.createClass({
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
Press
<StartChatButton size="16" callout={true}/>
to start a chat with someone
{_tJsx(
"Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/],
[
(sub) => <StartChatButton size="16" callout={true}/>
]
)}
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
You're not in any rooms yet! Press
<CreateRoomButton size="16" callout={true}/>
to make a room or
<RoomDirectoryButton size="16" callout={true}/>
to browse the directory
{_tJsx(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
[
(sub) => <CreateRoomButton size="16" callout={true}/>,
(sub) => <RoomDirectoryButton size="16" callout={true}/>
]
)}
</div>;
}
@@ -497,7 +522,7 @@ module.exports = React.createClass({
return null;
}
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
const labelText = phraseForSection(section);
return <RoomDropTarget label={labelText} />;
},

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import { _t, _tJsx } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) {
const BannedUser = React.createClass({
propTypes: {
canUnban: React.PropTypes.bool,
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
reason: React.PropTypes.string,
},
@@ -67,13 +68,17 @@ const BannedUser = React.createClass({
},
render: function() {
let unbanButton;
if (this.props.canUnban) {
unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
{ _t('Unban') }
</AccessibleButton>;
}
return (
<li>
<AccessibleButton className="mx_RoomSettings_unbanButton"
onClick={this._onUnbanClick}
>
{ _t('Unban') }
</AccessibleButton>
{ unbanButton }
<strong>{this.props.member.name}</strong> {this.props.member.userId}
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
</li>
@@ -178,8 +183,14 @@ module.exports = React.createClass({
});
},
/**
* Returns a promise which resolves once all of the save operations have completed or failed.
*
* The result is a list of promise state snapshots, each with the form
* `{ state: "fulfilled", value: v }` or `{ state: "rejected", reason: r }`.
*/
save: function() {
var stateWasSetDefer = q.defer();
var stateWasSetDefer = Promise.defer();
// the caller may have JUST called setState on stuff, so we need to re-render before saving
// else we won't use the latest values of things.
// We can be a bit cheeky here and set a loading flag, and listen for the callback on that
@@ -189,8 +200,18 @@ module.exports = React.createClass({
this.setState({ _loading: false});
});
function mapPromiseToSnapshot(p) {
return p.then((r) => {
return { state: "fulfilled", value: r };
}, (e) => {
return { state: "rejected", reason: e };
});
}
return stateWasSetDefer.promise.then(() => {
return q.allSettled(this._calcSavePromises());
return Promise.all(
this._calcSavePromises().map(mapPromiseToSnapshot),
);
});
},
@@ -277,7 +298,7 @@ module.exports = React.createClass({
// color scheme
var p;
p = this.saveColor();
if (!q.isFulfilled(p)) {
if (!p.isFulfilled()) {
promises.push(p);
}
@@ -289,7 +310,7 @@ module.exports = React.createClass({
// encryption
p = this.saveEnableEncryption();
if (!q.isFulfilled(p)) {
if (!p.isFulfilled()) {
promises.push(p);
}
@@ -300,25 +321,25 @@ module.exports = React.createClass({
},
saveAliases: function() {
if (!this.refs.alias_settings) { return [q()]; }
if (!this.refs.alias_settings) { return [Promise.resolve()]; }
return this.refs.alias_settings.saveSettings();
},
saveColor: function() {
if (!this.refs.color_settings) { return q(); }
if (!this.refs.color_settings) { return Promise.resolve(); }
return this.refs.color_settings.saveSettings();
},
saveUrlPreviewSettings: function() {
if (!this.refs.url_preview_settings) { return q(); }
if (!this.refs.url_preview_settings) { return Promise.resolve(); }
return this.refs.url_preview_settings.saveSettings();
},
saveEnableEncryption: function() {
if (!this.refs.encrypt) { return q(); }
if (!this.refs.encrypt) { return Promise.resolve(); }
var encrypt = this.refs.encrypt.checked;
if (!encrypt) { return q(); }
if (!encrypt) { return Promise.resolve(); }
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
@@ -667,6 +688,7 @@ module.exports = React.createClass({
const banned = this.props.room.getMembersWithMembership("ban");
let bannedUsersSection;
if (banned.length) {
const canBanUsers = current_user_level >= ban_level;
bannedUsersSection =
<div>
<h3>{ _t('Banned users') }</h3>
@@ -674,7 +696,7 @@ module.exports = React.createClass({
{banned.map(function(member) {
const banEvent = member.events.member.getContent();
return (
<BannedUser key={member.userId} member={member} reason={banEvent.reason} />
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
);
})}
</ul>

View File

@@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
@@ -45,17 +42,10 @@ export default React.createClass({
title: React.PropTypes.string,
onCancelClick: React.PropTypes.func,
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
},
onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
render: function() {
let cancelButton;
let icon;
@@ -70,25 +60,12 @@ export default React.createClass({
/>;
}
let showRhsButton;
/* // don't bother cluttering things up with this for now.
const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.props.collapsedRhs) {
showRhsButton =
<div className="mx_RoomHeader_button" style={{ float: 'right' }} onClick={this.onShowRhsClick} title=">">
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
</div>
}
*/
return (
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ showRhsButton }
{ cancelButton }
</div>
</div>

View File

@@ -1,48 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
'use strict';
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
module.exports = React.createClass({
displayName: 'TabCompleteBar',
propTypes: {
tabComplete: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="mx_TabCompleteBar">
{this.props.tabComplete.peek(6).map((entry, i) => {
return (
<div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()}
<span className="mx_TabCompleteBar_text">
{entry.getText()}
</span>
</div>
);
})}
</div>
);
}
});

View File

@@ -19,10 +19,10 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import AddThreepid from '../../../AddThreepid';
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import Modal from '../../../Modal';
export default WithMatrixClient(React.createClass({
export default withMatrixClient(React.createClass({
displayName: 'AddPhoneNumber',
propTypes: {

View File

@@ -21,7 +21,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require("../../../index");
import q from 'q';
import Promise from 'bluebird';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
@@ -161,7 +161,7 @@ module.exports = React.createClass({
},
_optionallySetEmail: function() {
const deferred = q.defer();
const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
Modal.createDialog(SetEmailDialog, {

View File

@@ -13,11 +13,11 @@ 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.
*/
var React = require("react");
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
import React from 'react';
import dis from '../../../dispatcher';
import CallHandler from '../../../CallHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@@ -73,10 +73,10 @@ module.exports = React.createClass({
},
showCall: function() {
var call;
let call;
if (this.props.room) {
var roomId = this.props.room.roomId;
const roomId = this.props.room.roomId;
call = CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ?
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
@@ -86,9 +86,7 @@ module.exports = React.createClass({
if (this.call) {
this.setState({ call: call });
}
}
else {
} else {
call = CallHandler.getAnyActiveCall();
this.setState({ call: call });
}
@@ -109,8 +107,7 @@ module.exports = React.createClass({
call.confUserId ? "none" : "block"
);
this.getVideoView().getRemoteVideoElement().style.display = "block";
}
else {
} else {
this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
@@ -126,11 +123,11 @@ module.exports = React.createClass({
},
render: function() {
var VideoView = sdk.getComponent('voip.VideoView');
const VideoView = sdk.getComponent('voip.VideoView');
var voice;
let voice;
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
var callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
voice = (
<div className="mx_CallView_voice" onClick={ this.props.onClick }>
{_t("Active call (%(roomName)s)", {roomName: callRoom.name})}
@@ -147,6 +144,6 @@ module.exports = React.createClass({
{ voice }
</div>
);
}
},
});

View File

@@ -13,10 +13,9 @@ 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.
*/
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@@ -29,34 +28,32 @@ module.exports = React.createClass({
onAnswerClick: function() {
dis.dispatch({
action: 'answer',
room_id: this.props.incomingCall.roomId
room_id: this.props.incomingCall.roomId,
});
},
onRejectClick: function() {
dis.dispatch({
action: 'hangup',
room_id: this.props.incomingCall.roomId
room_id: this.props.incomingCall.roomId,
});
},
render: function() {
var room = null;
let room = null;
if (this.props.incomingCall) {
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
}
var caller = room ? room.name : _t("unknown caller");
const caller = room ? room.name : _t("unknown caller");
let incomingCallText = null;
if (this.props.incomingCall) {
if (this.props.incomingCall.type === "voice") {
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
}
else if (this.props.incomingCall.type === "video") {
} else if (this.props.incomingCall.type === "video") {
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
}
else {
} else {
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
}
}
@@ -81,6 +78,6 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View File

@@ -16,7 +16,7 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
module.exports = React.createClass({
displayName: 'VideoFeed',

View File

@@ -16,11 +16,11 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
import React from 'react';
import ReactDOM from 'react-dom';
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import sdk from '../../../index';
import dis from '../../../dispatcher';
module.exports = React.createClass({
displayName: 'VideoView',
@@ -53,9 +53,10 @@ module.exports = React.createClass({
// this needs to be somewhere at the top of the DOM which
// always exists to avoid audio interruptions.
// Might as well just use DOM.
var remoteAudioElement = document.getElementById("remoteAudio");
const remoteAudioElement = document.getElementById("remoteAudio");
if (!remoteAudioElement) {
console.error("Failed to find remoteAudio element - cannot play audio! You need to add an <audio/> to the DOM.");
console.error("Failed to find remoteAudio element - cannot play audio!"
+ "You need to add an <audio/> to the DOM.");
}
return remoteAudioElement;
},
@@ -70,22 +71,21 @@ module.exports = React.createClass({
onAction: function(payload) {
switch (payload.action) {
case 'video_fullscreen':
case 'video_fullscreen': {
if (!this.container) {
return;
}
var element = this.container;
const element = this.container;
if (payload.fullscreen) {
var requestMethod = (
const requestMethod = (
element.requestFullScreen ||
element.webkitRequestFullScreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen
);
requestMethod.call(element);
}
else {
var exitMethod = (
} else {
const exitMethod = (
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
@@ -96,17 +96,18 @@ module.exports = React.createClass({
}
}
break;
}
}
},
render: function() {
var VideoFeed = sdk.getComponent('voip.VideoFeed');
const VideoFeed = sdk.getComponent('voip.VideoFeed');
// if we're fullscreen, we don't want to set a maxHeight on the video element.
var fullscreenElement = (document.fullscreenElement ||
const fullscreenElement = (document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement);
var maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
return (
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
@@ -119,5 +120,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});