Merge branch 'develop' into travis/remove-presence
This commit is contained in:
commit
fe2cbc584d
246 changed files with 16884 additions and 4954 deletions
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -29,11 +30,21 @@ module.exports = {
|
|||
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||
|
||||
propTypes: {
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
menuColour: React.PropTypes.string,
|
||||
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
||||
top: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
},
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
|
@ -51,14 +62,19 @@ module.exports = {
|
|||
createMenu: function(Element, props) {
|
||||
const self = this;
|
||||
|
||||
const closeMenu = function() {
|
||||
const closeMenu = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, arguments);
|
||||
props.onFinished.apply(null, args);
|
||||
}
|
||||
};
|
||||
|
||||
// Close the menu on window resize
|
||||
const windowResize = function() {
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
||||
|
@ -129,13 +145,26 @@ module.exports = {
|
|||
menuStyle["backgroundColor"] = props.menuColour;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingLeft))) {
|
||||
menuStyle["paddingLeft"] = props.menuPaddingLeft;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingBottom))) {
|
||||
menuStyle["paddingBottom"] = props.menuPaddingBottom;
|
||||
}
|
||||
if (!isNaN(Number(props.menuPaddingRight))) {
|
||||
menuStyle["paddingRight"] = props.menuPaddingRight;
|
||||
}
|
||||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
const menu = (
|
||||
<div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle}>
|
||||
{ chevron }
|
||||
<Element {...props} onFinished={closeMenu} />
|
||||
<Element {...props} onFinished={closeMenu} onResize={windowResize} />
|
||||
</div>
|
||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||
<style>{ chevronCSS }</style>
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
@ -30,8 +31,8 @@ module.exports = React.createClass({
|
|||
displayName: 'CreateRoom',
|
||||
|
||||
propTypes: {
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
onRoomCreated: PropTypes.func,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
phases: {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
|
@ -28,7 +29,7 @@ const FilePanel = React.createClass({
|
|||
displayName: 'FilePanel',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -67,6 +68,9 @@ const FilePanel = React.createClass({
|
|||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true,
|
||||
"not_types": [
|
||||
"m.sticker",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2017, 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -29,8 +29,9 @@ import classnames from 'classnames';
|
|||
|
||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import FlairStore from '../../stores/FlairStore';
|
||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({
|
|||
|
||||
let permalink = null;
|
||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||
permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias);
|
||||
}
|
||||
|
||||
let roomNameNode = null;
|
||||
|
@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({
|
|||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||
|
||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||
const httpUrl = MatrixClientPeg.get()
|
||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||
|
@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({
|
|||
});
|
||||
|
||||
const GroupContext = {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
|
||||
};
|
||||
|
||||
CategoryRoomList.contextTypes = GroupContext;
|
||||
|
@ -398,6 +399,9 @@ FeaturedRoom.contextTypes = GroupContext;
|
|||
RoleUserList.contextTypes = GroupContext;
|
||||
FeaturedUser.contextTypes = GroupContext;
|
||||
|
||||
const GROUP_JOINPOLICY_OPEN = "open";
|
||||
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupView',
|
||||
|
||||
|
@ -408,7 +412,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
childContextTypes: {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||
groupStore: PropTypes.instanceOf(GroupStore),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
|
@ -428,6 +432,7 @@ export default React.createClass({
|
|||
editing: false,
|
||||
saving: false,
|
||||
uploadingAvatar: false,
|
||||
avatarChanged: false,
|
||||
membershipBusy: false,
|
||||
publicityBusy: false,
|
||||
inviterProfile: null,
|
||||
|
@ -461,6 +466,10 @@ export default React.createClass({
|
|||
_onGroupMyMembership: function(group) {
|
||||
if (group.groupId !== this.props.groupId) return;
|
||||
|
||||
if (group.myMembership === 'leave') {
|
||||
// Leave settings - the user might have clicked the "Leave" button
|
||||
this._closeSettings();
|
||||
}
|
||||
this.setState({membershipBusy: false});
|
||||
},
|
||||
|
||||
|
@ -543,6 +552,12 @@ export default React.createClass({
|
|||
this.setState({
|
||||
editing: true,
|
||||
profileForm: Object.assign({}, this.state.summary.profile),
|
||||
joinableForm: {
|
||||
policyType:
|
||||
this.state.summary.profile.is_openly_joinable ?
|
||||
GROUP_JOINPOLICY_OPEN :
|
||||
GROUP_JOINPOLICY_INVITE,
|
||||
},
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
|
@ -551,6 +566,10 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this._closeSettings();
|
||||
},
|
||||
|
||||
_closeSettings() {
|
||||
this.setState({
|
||||
editing: false,
|
||||
profileForm: null,
|
||||
|
@ -589,6 +608,10 @@ export default React.createClass({
|
|||
this.setState({
|
||||
uploadingAvatar: false,
|
||||
profileForm: newProfileForm,
|
||||
|
||||
// Indicate that FlairStore needs to be poked to show this change
|
||||
// in TagTile (TagPanel), Flair and GroupTile (MyGroups).
|
||||
avatarChanged: true,
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.setState({uploadingAvatar: false});
|
||||
|
@ -601,11 +624,15 @@ export default React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
_onJoinableChange: function(ev) {
|
||||
this.setState({
|
||||
joinableForm: { policyType: ev.target.value },
|
||||
});
|
||||
},
|
||||
|
||||
_onSaveClick: function() {
|
||||
this.setState({saving: true});
|
||||
const savePromise = this.state.isUserPrivileged ?
|
||||
this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) :
|
||||
Promise.resolve();
|
||||
const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
|
||||
savePromise.then((result) => {
|
||||
this.setState({
|
||||
saving: false,
|
||||
|
@ -614,6 +641,11 @@ export default React.createClass({
|
|||
});
|
||||
dis.dispatch({action: 'panel_disable'});
|
||||
this._initGroupStore(this.props.groupId);
|
||||
|
||||
if (this.state.avatarChanged) {
|
||||
// XXX: Evil - poking a store should be done from an async action
|
||||
FlairStore.refreshGroupProfile(this._matrixClient, this.props.groupId);
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.setState({
|
||||
saving: false,
|
||||
|
@ -624,11 +656,27 @@ export default React.createClass({
|
|||
title: _t('Error'),
|
||||
description: _t('Failed to update community'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
avatarChanged: false,
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
_onAcceptInviteClick: function() {
|
||||
_saveGroup: async function() {
|
||||
await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
|
||||
await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
|
||||
type: this.state.joinableForm.policyType,
|
||||
});
|
||||
},
|
||||
|
||||
_onAcceptInviteClick: async function() {
|
||||
this.setState({membershipBusy: true});
|
||||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
|
||||
this._groupStore.acceptGroupInvite().then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
}).catch((e) => {
|
||||
|
@ -641,9 +689,14 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onRejectInviteClick: function() {
|
||||
_onRejectInviteClick: async function() {
|
||||
this.setState({membershipBusy: true});
|
||||
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
|
||||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
|
||||
this._groupStore.leaveGroup().then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
}).catch((e) => {
|
||||
this.setState({membershipBusy: false});
|
||||
|
@ -655,6 +708,25 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onJoinClick: async function() {
|
||||
this.setState({membershipBusy: true});
|
||||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
|
||||
this._groupStore.joinGroup().then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
}).catch((e) => {
|
||||
this.setState({membershipBusy: false});
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to join community"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onLeaveClick: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||
|
@ -662,18 +734,23 @@ export default React.createClass({
|
|||
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||
button: _t("Leave"),
|
||||
danger: true,
|
||||
onFinished: (confirmed) => {
|
||||
onFinished: async (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
|
||||
this.setState({membershipBusy: true});
|
||||
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
|
||||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
|
||||
this._groupStore.leaveGroup().then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
}).catch((e) => {
|
||||
this.setState({membershipBusy: false});
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
|
||||
Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to leave room"),
|
||||
description: _t("Unable to leave community"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -691,8 +768,22 @@ export default React.createClass({
|
|||
});
|
||||
|
||||
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
|
||||
const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ?
|
||||
<div className="mx_GroupView_changeDelayWarning">
|
||||
{ _t(
|
||||
'Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> ' +
|
||||
'might not be seen by other users for up to 30 minutes.',
|
||||
{},
|
||||
{
|
||||
'bold1': (sub) => <b> { sub } </b>,
|
||||
'bold2': (sub) => <b> { sub } </b>,
|
||||
},
|
||||
) }
|
||||
</div> : <div />;
|
||||
return <div className={groupSettingsSectionClasses}>
|
||||
{ header }
|
||||
{ changeDelayWarning }
|
||||
{ this._getJoinableNode() }
|
||||
{ this._getLongDescriptionNode() }
|
||||
{ this._getRoomsNode() }
|
||||
</div>;
|
||||
|
@ -831,9 +922,8 @@ export default React.createClass({
|
|||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
const group = this._matrixClient.getGroup(this.props.groupId);
|
||||
if (!group) return null;
|
||||
|
||||
if (group.myMembership === 'invite') {
|
||||
if (group && group.myMembership === 'invite') {
|
||||
if (this.state.membershipBusy || this.state.inviterProfileBusy) {
|
||||
return <div className="mx_GroupView_membershipSection">
|
||||
<Spinner />
|
||||
|
@ -874,33 +964,107 @@ export default React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
} else if (group.myMembership === 'join' && this.state.editing) {
|
||||
const leaveButtonTooltip = this.state.isUserPrivileged ?
|
||||
}
|
||||
|
||||
let membershipContainerExtraClasses;
|
||||
let membershipButtonExtraClasses;
|
||||
let membershipButtonTooltip;
|
||||
let membershipButtonText;
|
||||
let membershipButtonOnClick;
|
||||
|
||||
// User is not in the group
|
||||
if ((!group || group.myMembership === 'leave') &&
|
||||
this.state.summary &&
|
||||
this.state.summary.profile &&
|
||||
Boolean(this.state.summary.profile.is_openly_joinable)
|
||||
) {
|
||||
membershipButtonText = _t("Join this community");
|
||||
membershipButtonOnClick = this._onJoinClick;
|
||||
|
||||
membershipButtonExtraClasses = 'mx_GroupView_joinButton';
|
||||
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_leave';
|
||||
} else if (
|
||||
group &&
|
||||
group.myMembership === 'join' &&
|
||||
this.state.editing
|
||||
) {
|
||||
membershipButtonText = _t("Leave this community");
|
||||
membershipButtonOnClick = this._onLeaveClick;
|
||||
membershipButtonTooltip = this.state.isUserPrivileged ?
|
||||
_t("You are an administrator of this community") :
|
||||
_t("You are a member of this community");
|
||||
const leaveButtonClasses = classnames({
|
||||
"mx_RoomHeader_textButton": true,
|
||||
"mx_GroupView_textButton": true,
|
||||
"mx_GroupView_leaveButton": true,
|
||||
"mx_RoomHeader_textButton_danger": this.state.isUserPrivileged,
|
||||
});
|
||||
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
|
||||
<div className="mx_GroupView_membershipSubSection">
|
||||
{ /* Empty div for flex alignment */ }
|
||||
<div />
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
<AccessibleButton
|
||||
className={leaveButtonClasses}
|
||||
onClick={this._onLeaveClick}
|
||||
title={leaveButtonTooltip}
|
||||
>
|
||||
{ _t("Leave") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
membershipButtonExtraClasses = {
|
||||
'mx_GroupView_leaveButton': true,
|
||||
'mx_RoomHeader_textButton_danger': this.state.isUserPrivileged,
|
||||
};
|
||||
membershipContainerExtraClasses = 'mx_GroupView_membershipSection_joined';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
const membershipButtonClasses = classnames([
|
||||
'mx_RoomHeader_textButton',
|
||||
'mx_GroupView_textButton',
|
||||
],
|
||||
membershipButtonExtraClasses,
|
||||
);
|
||||
|
||||
const membershipContainerClasses = classnames(
|
||||
'mx_GroupView_membershipSection',
|
||||
membershipContainerExtraClasses,
|
||||
);
|
||||
|
||||
return <div className={membershipContainerClasses}>
|
||||
<div className="mx_GroupView_membershipSubSection">
|
||||
{ /* The <div /> is for flex alignment */ }
|
||||
{ this.state.membershipBusy ? <Spinner /> : <div /> }
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
<AccessibleButton
|
||||
className={membershipButtonClasses}
|
||||
onClick={membershipButtonOnClick}
|
||||
title={membershipButtonTooltip}
|
||||
>
|
||||
{ membershipButtonText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
_getJoinableNode: function() {
|
||||
return this.state.editing ? <div>
|
||||
<h3>
|
||||
{ _t('Who can join this community?') }
|
||||
{ this.state.groupJoinableLoading ?
|
||||
<InlineSpinner /> : <div />
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_INVITE}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
||||
onClick={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Only people who have been invited') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_OPEN}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
||||
onClick={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Everyone') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div> : null;
|
||||
},
|
||||
|
||||
_getLongDescriptionNode: function() {
|
||||
|
@ -946,6 +1110,7 @@ export default React.createClass({
|
|||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||
return <Spinner />;
|
||||
|
@ -1096,9 +1261,9 @@ export default React.createClass({
|
|||
{ rightButtons }
|
||||
</div>
|
||||
</div>
|
||||
<GeminiScrollbar className="mx_GroupView_body">
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ bodyNodes }
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
|
|||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
|
@ -26,18 +27,18 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the auth process has finished,
|
||||
// successfully or unsuccessfully.
|
||||
|
@ -51,22 +52,22 @@ export default React.createClass({
|
|||
// the auth session.
|
||||
// * clientSecret {string} The client secret used in auth
|
||||
// sessions with the ID server.
|
||||
onAuthFinished: React.PropTypes.func.isRequired,
|
||||
onAuthFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Inputs provided by the user to the auth process
|
||||
// and used by various stages. As passed to js-sdk
|
||||
// interactive-auth
|
||||
inputs: React.PropTypes.object,
|
||||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: React.PropTypes.func,
|
||||
sessionId: React.PropTypes.string,
|
||||
clientSecret: React.PropTypes.string,
|
||||
emailSid: React.PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
||||
// If true, poll to see if the auth flow has been completed
|
||||
// out-of-band
|
||||
poll: React.PropTypes.bool,
|
||||
poll: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
|
@ -31,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -44,23 +47,23 @@ const LoggedInView = React.createClass({
|
|||
displayName: 'LoggedInView',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: React.PropTypes.string.isRequired,
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
onUserSettingsClose: React.PropTypes.func,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
onUserSettingsClose: PropTypes.func,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
teamToken: React.PropTypes.string,
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: React.PropTypes.object,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
|
@ -208,8 +211,51 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onDragEnd: function(result) {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the TagPanel receives an
|
||||
// optimistic update from TagOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomTileEndDrag: function(result) {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
|
@ -328,23 +374,23 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
|
||||
{ topBar }
|
||||
<div className={bodyClasses}>
|
||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default DragDropContext(HTML5Backend)(LoggedInView);
|
||||
export default LoggedInView;
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import Promise from 'bluebird';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
|
@ -92,38 +93,38 @@ export default React.createClass({
|
|||
displayName: 'MatrixChat',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object,
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
enableGuest: React.PropTypes.bool,
|
||||
config: PropTypes.object,
|
||||
ConferenceHandler: PropTypes.any,
|
||||
onNewScreen: PropTypes.func,
|
||||
registrationUrl: PropTypes.string,
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
// the queryParams extracted from the [real] query-string of the URI
|
||||
realQueryParams: React.PropTypes.object,
|
||||
realQueryParams: PropTypes.object,
|
||||
|
||||
// the initial queryParams extracted from the hash-fragment of the URI
|
||||
startingFragmentQueryParams: React.PropTypes.object,
|
||||
startingFragmentQueryParams: PropTypes.object,
|
||||
|
||||
// called when we have completed a token login
|
||||
onTokenLoginCompleted: React.PropTypes.func,
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
|
||||
// Represents the screen to display as a result of parsing the initial
|
||||
// window.location
|
||||
initialScreenAfterLogin: React.PropTypes.shape({
|
||||
screen: React.PropTypes.string.isRequired,
|
||||
params: React.PropTypes.object,
|
||||
initialScreenAfterLogin: PropTypes.shape({
|
||||
screen: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
|
||||
// displayname, if any, to set on the device when logging
|
||||
// in/registering.
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// A function that makes a registration URL
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
appConfig: React.PropTypes.object,
|
||||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
AuxPanel: {
|
||||
|
@ -170,6 +171,10 @@ export default React.createClass({
|
|||
register_hs_url: null,
|
||||
register_is_url: null,
|
||||
register_id_sid: null,
|
||||
|
||||
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||
// and disable it when there are no dialogs
|
||||
hideToSRUsers: false,
|
||||
};
|
||||
return s;
|
||||
},
|
||||
|
@ -286,6 +291,8 @@ export default React.createClass({
|
|||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
|
||||
this._pageChanging = false;
|
||||
|
||||
// check we have the right tint applied for this theme.
|
||||
// N.B. we don't call the whole of setTheme() here as we may be
|
||||
// racing with the theme CSS download finishing from index.js
|
||||
|
@ -363,13 +370,58 @@ export default React.createClass({
|
|||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
componentWillUpdate: function(props, state) {
|
||||
if (this.shouldTrackPageChange(this.state, state)) {
|
||||
this.startPageChangeTimer();
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
if (this.shouldTrackPageChange(prevState, this.state)) {
|
||||
const durationMs = this.stopPageChangeTimer();
|
||||
Analytics.trackPageChange(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
this.focusComposer = false;
|
||||
}
|
||||
},
|
||||
|
||||
startPageChangeTimer() {
|
||||
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
||||
// are used.
|
||||
if (this._pageChanging) {
|
||||
console.warn('MatrixChat.startPageChangeTimer: timer already started');
|
||||
return;
|
||||
}
|
||||
this._pageChanging = true;
|
||||
performance.mark('riot_MatrixChat_page_change_start');
|
||||
},
|
||||
|
||||
stopPageChangeTimer() {
|
||||
if (!this._pageChanging) {
|
||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||
return;
|
||||
}
|
||||
this._pageChanging = false;
|
||||
performance.mark('riot_MatrixChat_page_change_stop');
|
||||
performance.measure(
|
||||
'riot_MatrixChat_page_change_delta',
|
||||
'riot_MatrixChat_page_change_start',
|
||||
'riot_MatrixChat_page_change_stop',
|
||||
);
|
||||
performance.clearMarks('riot_MatrixChat_page_change_start');
|
||||
performance.clearMarks('riot_MatrixChat_page_change_stop');
|
||||
const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop();
|
||||
return measurement.duration;
|
||||
},
|
||||
|
||||
shouldTrackPageChange(prevState, state) {
|
||||
return prevState.currentRoomId !== state.currentRoomId ||
|
||||
prevState.view !== state.view ||
|
||||
prevState.page_type !== state.page_type;
|
||||
},
|
||||
|
||||
setStateForNewView: function(state) {
|
||||
if (state.view === undefined) {
|
||||
throw new Error("setStateForNewView with no view!");
|
||||
|
@ -607,6 +659,16 @@ export default React.createClass({
|
|||
case 'send_event':
|
||||
this.onSendEvent(payload.room_id, payload.event);
|
||||
break;
|
||||
case 'aria_hide_main_app':
|
||||
this.setState({
|
||||
hideToSRUsers: true,
|
||||
});
|
||||
break;
|
||||
case 'aria_unhide_main_app':
|
||||
this.setState({
|
||||
hideToSRUsers: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -617,18 +679,26 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_startRegistration: function(params) {
|
||||
this.setStateForNewView({
|
||||
const newState = {
|
||||
view: VIEWS.REGISTER,
|
||||
// these params may be undefined, but if they are,
|
||||
// unset them from our state: we don't want to
|
||||
// resume a previous registration session if the
|
||||
// user just clicked 'register'
|
||||
register_client_secret: params.client_secret,
|
||||
register_session_id: params.session_id,
|
||||
register_hs_url: params.hs_url,
|
||||
register_is_url: params.is_url,
|
||||
register_id_sid: params.sid,
|
||||
});
|
||||
};
|
||||
|
||||
// Only honour params if they are all present, otherwise we reset
|
||||
// HS and IS URLs when switching to registration.
|
||||
if (params.client_secret &&
|
||||
params.session_id &&
|
||||
params.hs_url &&
|
||||
params.is_url &&
|
||||
params.sid
|
||||
) {
|
||||
newState.register_client_secret = params.client_secret;
|
||||
newState.register_session_id = params.session_id;
|
||||
newState.register_hs_url = params.hs_url;
|
||||
newState.register_is_url = params.is_url;
|
||||
newState.register_id_sid = params.sid;
|
||||
}
|
||||
|
||||
this.setStateForNewView(newState);
|
||||
this.notifyNewScreen('register');
|
||||
},
|
||||
|
||||
|
@ -846,16 +916,36 @@ export default React.createClass({
|
|||
}).close;
|
||||
},
|
||||
|
||||
_leaveRoomWarnings: function(roomId) {
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
// Show a warning if there are additional complications.
|
||||
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
||||
const warnings = [];
|
||||
if (joinRules) {
|
||||
const rule = joinRules.getContent().join_rule;
|
||||
if (rule !== "public") {
|
||||
warnings.push((
|
||||
<span className="warning" key="non_public_warning">
|
||||
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||
</span>
|
||||
));
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
},
|
||||
|
||||
_leaveRoom: function(roomId) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const warnings = this._leaveRoomWarnings(roomId);
|
||||
|
||||
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||
title: _t("Leave room"),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
onFinished: (shouldLeave) => {
|
||||
|
@ -1065,10 +1155,10 @@ export default React.createClass({
|
|||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self._loggedInView) {
|
||||
if (!self._loggedInView || !self._loggedInView.child) {
|
||||
return true;
|
||||
}
|
||||
return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId);
|
||||
return self._loggedInView.child.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
|
@ -1142,18 +1232,6 @@ export default React.createClass({
|
|||
cli.on("crypto.warning", (type) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
switch (type) {
|
||||
case 'CRYPTO_WARNING_ACCOUNT_MIGRATED':
|
||||
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
|
||||
title: _t('Cryptography data migrated'),
|
||||
description: _t(
|
||||
"A one-off migration of cryptography data has been performed. "+
|
||||
"End-to-end encryption will not work if you go back to an older "+
|
||||
"version of Riot. If you need to use end-to-end cryptography on "+
|
||||
"an older version, log out of Riot first. To retain message history, "+
|
||||
"export and re-import your keys.",
|
||||
),
|
||||
});
|
||||
break;
|
||||
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
|
||||
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
|
||||
title: _t('Old cryptography data detected'),
|
||||
|
@ -1310,7 +1388,6 @@ export default React.createClass({
|
|||
if (this.props.onNewScreen) {
|
||||
this.props.onNewScreen(screen);
|
||||
}
|
||||
Analytics.trackPageChange();
|
||||
},
|
||||
|
||||
onAliasClick: function(event, alias) {
|
||||
|
@ -1480,6 +1557,17 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl) {
|
||||
newState.register_hs_url = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl) {
|
||||
newState.register_is_url = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
if (this.props.startingFragmentQueryParams.referrer) {
|
||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||
|
@ -1568,6 +1656,7 @@ export default React.createClass({
|
|||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1602,6 +1691,7 @@ export default React.createClass({
|
|||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
|
@ -32,63 +32,63 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: React.PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
// true to show a spinner at the top of the timeline to indicate
|
||||
// back-pagination in progress
|
||||
backPaginating: React.PropTypes.bool,
|
||||
backPaginating: PropTypes.bool,
|
||||
|
||||
// true to show a spinner at the end of the timeline to indicate
|
||||
// forward-pagination in progress
|
||||
forwardPaginating: React.PropTypes.bool,
|
||||
forwardPaginating: PropTypes.bool,
|
||||
|
||||
// the list of MatrixEvents to display
|
||||
events: React.PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// event after which we should show a read marker
|
||||
readMarkerEventId: React.PropTypes.string,
|
||||
readMarkerEventId: PropTypes.string,
|
||||
|
||||
// whether the read marker should be visible
|
||||
readMarkerVisible: React.PropTypes.bool,
|
||||
readMarkerVisible: PropTypes.bool,
|
||||
|
||||
// the userid of our user. This is used to suppress the read marker
|
||||
// for pending messages.
|
||||
ourUserId: React.PropTypes.string,
|
||||
ourUserId: PropTypes.string,
|
||||
|
||||
// true to suppress the date at the start of the timeline
|
||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
suppressFirstDateSeparator: PropTypes.bool,
|
||||
|
||||
// whether to show read receipts
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: PropTypes.bool,
|
||||
|
||||
// true if updates to the event list should cause the scroll panel to
|
||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||
// for more details.
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest: React.PropTypes.func,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
// className for the panel
|
||||
className: React.PropTypes.string.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
|
||||
// shape parameter to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: React.PropTypes.bool,
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// show timestamps always
|
||||
alwaysShowTimestamps: React.PropTypes.bool,
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -325,7 +325,7 @@ module.exports = React.createClass({
|
|||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
|
@ -447,10 +447,18 @@ module.exports = React.createClass({
|
|||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()) {
|
||||
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
|
@ -479,7 +487,7 @@ module.exports = React.createClass({
|
|||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
@ -522,17 +530,7 @@ module.exports = React.createClass({
|
|||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare weekdays
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -26,7 +26,7 @@ export default withMatrixClient(React.createClass({
|
|||
displayName: 'MyGroups',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -62,6 +62,8 @@ export default withMatrixClient(React.createClass({
|
|||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
||||
let content;
|
||||
let contentHeader;
|
||||
|
@ -72,9 +74,26 @@ export default withMatrixClient(React.createClass({
|
|||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</GeminiScrollbar> :
|
||||
<GeminiScrollbarWrapper>
|
||||
<div className="mx_MyGroups_microcopy">
|
||||
<p>
|
||||
{ _t(
|
||||
"Did you know: you can use communities to filter your Riot.im experience!",
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
||||
"filter panel at any time to see only the rooms and people associated " +
|
||||
"with that community.",
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
|
@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -41,59 +42,59 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// the room this statusbar is representing.
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
numUnreadMessages: PropTypes.number,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline.
|
||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||
atEndOfLiveTimeline: PropTypes.bool,
|
||||
|
||||
// This is true when the user is alone in the room, but has also sent a message.
|
||||
// Used to suggest to the user to invite someone
|
||||
sentMessageAndIsAlone: React.PropTypes.bool,
|
||||
sentMessageAndIsAlone: PropTypes.bool,
|
||||
|
||||
// true if there is an active call in this room (means we show
|
||||
// the 'Active Call' text in the status bar if there is nothing
|
||||
// more interesting)
|
||||
hasActiveCall: React.PropTypes.bool,
|
||||
hasActiveCall: PropTypes.bool,
|
||||
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: React.PropTypes.number,
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: React.PropTypes.func,
|
||||
onResendAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'cancel all' button in the
|
||||
// 'unsent messages' bar
|
||||
onCancelAllClick: React.PropTypes.func,
|
||||
onCancelAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'invite others' button in the
|
||||
// 'you are alone' bar
|
||||
onInviteClick: React.PropTypes.func,
|
||||
onInviteClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'stop warning me' button in the
|
||||
// 'you are alone' bar
|
||||
onStopWarningClick: React.PropTypes.func,
|
||||
onStopWarningClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'scroll to bottom' button
|
||||
onScrollToBottomClick: React.PropTypes.func,
|
||||
onScrollToBottomClick: PropTypes.func,
|
||||
|
||||
// callback for when we do something that changes the size of the
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// component.
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
onVisible: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -147,6 +148,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
});
|
||||
},
|
||||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
},
|
||||
|
@ -156,7 +164,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
},
|
||||
|
||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||
|
@ -169,8 +177,10 @@ module.exports = React.createClass({
|
|||
|
||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||
_checkSize: function() {
|
||||
if (this.props.onVisible && this._getSize()) {
|
||||
this.props.onVisible();
|
||||
if (this._getSize()) {
|
||||
if (this.props.onVisible) this.props.onVisible();
|
||||
} else {
|
||||
if (this.props.onHidden) this.props.onHidden();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -286,10 +296,11 @@ module.exports = React.createClass({
|
|||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown devices being present");
|
||||
content = _t(
|
||||
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
||||
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
|
||||
{},
|
||||
{
|
||||
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
|
||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
|
@ -302,11 +313,11 @@ module.exports = React.createClass({
|
|||
) {
|
||||
title = unsentMessages[0].error.data.error;
|
||||
} else {
|
||||
title = _t("Some of your messages have not been sent.");
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||
"You can also select individual messages to resend or cancel.",
|
||||
{},
|
||||
{ count: unsentMessages.length },
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
|
|
|
@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent";
|
|||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
const classNames = require("classnames");
|
||||
import { _t } from '../../languageHandler';
|
||||
|
@ -58,18 +59,18 @@ if (DEBUG) {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
ConferenceHandler: PropTypes.any,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
// An object representing a third party invite to join this room
|
||||
// Fields:
|
||||
// * inviteSignUrl (string) The URL used to join this room from an email invite
|
||||
// (given as part of the link in the invite email)
|
||||
// * invitedEmail (string) The email address that was invited to this room
|
||||
thirdPartyInvite: React.PropTypes.object,
|
||||
thirdPartyInvite: PropTypes.object,
|
||||
|
||||
// Any data about the room that would normally come from the Home Server
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
|
@ -80,10 +81,10 @@ module.exports = React.createClass({
|
|||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||
// * inviterName (string) The display name of the person who
|
||||
// * invited us tovthe room
|
||||
oobData: React.PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -263,12 +264,19 @@ module.exports = React.createClass({
|
|||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop peeking if anything went wrong
|
||||
this.setState({
|
||||
isPeeking: false,
|
||||
|
@ -285,7 +293,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
});
|
||||
}
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
|
@ -459,6 +467,15 @@ module.exports = React.createClass({
|
|||
case 'message_sent':
|
||||
this._checkIfAlone(this.state.room);
|
||||
break;
|
||||
case 'post_sticker_message':
|
||||
this.injectSticker(
|
||||
payload.data.content.url,
|
||||
payload.data.content.info,
|
||||
payload.data.description || payload.data.name);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
this.uploadFile(payload.file);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case 'upload_failed':
|
||||
case 'upload_started':
|
||||
|
@ -619,8 +636,8 @@ module.exports = React.createClass({
|
|||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
},
|
||||
|
||||
|
@ -669,23 +686,7 @@ module.exports = React.createClass({
|
|||
// a member state changed in this room
|
||||
// refresh the conf call notification state
|
||||
this._updateConfCallNotification();
|
||||
|
||||
// if we are now a member of the room, where we were not before, that
|
||||
// means we have finished joining a room we were previously peeking
|
||||
// into.
|
||||
const me = MatrixClientPeg.get().credentials.userId;
|
||||
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
|
||||
// Having just joined a room, check to see if it looks like a DM room, and if so,
|
||||
// mark it as one. This is to work around the fact that some clients don't support
|
||||
// is_direct. We should remove this once they do.
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
|
||||
// XXX: There's not a whole lot we can really do if this fails: at best
|
||||
// perhaps we could try a couple more times, but since it's a temporary
|
||||
// compatability workaround, let's not bother.
|
||||
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
|
||||
}
|
||||
}
|
||||
this._updateDMState();
|
||||
}, 500),
|
||||
|
||||
_checkIfAlone: function(room) {
|
||||
|
@ -726,6 +727,44 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_updateDMState() {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me || me.membership !== "join") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have accepted an invite with is_direct set
|
||||
if (me.events.member.getPrevContent().membership === "invite" &&
|
||||
me.events.member.getPrevContent().is_direct
|
||||
) {
|
||||
// This is a DM with the sender of the invite event (which we assume
|
||||
// preceded the join event)
|
||||
Rooms.setDMRoom(
|
||||
this.state.room.roomId,
|
||||
me.events.member.getUnsigned().prev_sender,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const invitedMembers = this.state.room.getMembersWithMembership("invite");
|
||||
const joinedMembers = this.state.room.getMembersWithMembership("join");
|
||||
|
||||
// There must be one invited member and one joined member
|
||||
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have sent an invite with is_direct sent
|
||||
const other = invitedMembers[0];
|
||||
if (other &&
|
||||
other.membership === "invite" &&
|
||||
other.events.member.getContent().is_direct
|
||||
) {
|
||||
Rooms.setDMRoom(this.state.room.roomId, other.userId);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
onSearchResultsResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
|
@ -818,18 +857,6 @@ module.exports = React.createClass({
|
|||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
});
|
||||
|
||||
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
|
||||
if (this.state.room) {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me && me.membership == 'invite') {
|
||||
if (me.events.member.getContent().is_direct) {
|
||||
// The 'direct' hint is there, so declare that this is a DM room for
|
||||
// whoever invited us.
|
||||
return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
|
@ -889,7 +916,7 @@ module.exports = React.createClass({
|
|||
|
||||
ContentMessages.sendContentToRoom(
|
||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
||||
).done(undefined, (error) => {
|
||||
).catch((error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
return;
|
||||
|
@ -898,11 +925,27 @@ module.exports = React.createClass({
|
|||
console.error("Failed to upload file " + file + " " + error);
|
||||
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
||||
title: _t('Failed to upload file'),
|
||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||
description: ((error && error.message)
|
||||
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
injectSticker: function(url, info, text) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
.done(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onSearch: function(term, scope) {
|
||||
this.setState({
|
||||
searchTerm: term,
|
||||
|
@ -1347,10 +1390,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
if (this.unmounted) return;
|
||||
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
||||
// TODO: Find a less annoying way of hiding the status bar
|
||||
/*if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
});*/
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
|
@ -1583,7 +1628,8 @@ module.exports = React.createClass({
|
|||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
onResize={this.onChildResize}
|
||||
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
||||
showApps={this.state.showApps}
|
||||
hideAppsDrawer={this.state.editingRoomSettings} >
|
||||
{ aux }
|
||||
</AuxPanel>
|
||||
);
|
||||
|
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
|||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index.js';
|
||||
|
||||
const DEBUG_SCROLL = false;
|
||||
// var DEBUG_SCROLL = true;
|
||||
|
@ -86,7 +87,7 @@ module.exports = React.createClass({
|
|||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
|
@ -95,7 +96,7 @@ module.exports = React.createClass({
|
|||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom: React.PropTypes.bool,
|
||||
startAtBottom: PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
|
@ -110,7 +111,7 @@ module.exports = React.createClass({
|
|||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest: React.PropTypes.func,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
|
@ -121,24 +122,24 @@ module.exports = React.createClass({
|
|||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: React.PropTypes.func,
|
||||
onUnfillRequest: PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onResize: a callback which is called whenever the Gemini scroll
|
||||
* panel is resized
|
||||
*/
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: React.PropTypes.object,
|
||||
style: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -223,7 +224,7 @@ module.exports = React.createClass({
|
|||
onResize: function() {
|
||||
this.props.onResize();
|
||||
this.checkScroll();
|
||||
this.refs.geminiPanel.forceUpdate();
|
||||
if (this._gemScroll) this._gemScroll.forceUpdate();
|
||||
},
|
||||
|
||||
// after an update to the contents of the panel, check that the scroll is
|
||||
|
@ -664,14 +665,25 @@ module.exports = React.createClass({
|
|||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
return this.refs.geminiPanel.scrollbar.getViewElement();
|
||||
if (!this._gemScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
|
||||
}
|
||||
|
||||
return this._gemScroll.scrollbar.getViewElement();
|
||||
},
|
||||
|
||||
_collectGeminiScroll: function(gemScroll) {
|
||||
this._gemScroll = gemScroll;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
// TODO: the classnames on the div and ol could do with being updated to
|
||||
// reflect the fact that we don't necessarily contain a list of messages.
|
||||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
return (<GeminiScrollbarWrapper autoshow={true} wrappedRef={this._collectGeminiScroll}
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
|
@ -679,7 +691,7 @@ module.exports = React.createClass({
|
|||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,15 +17,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import FilterStore from '../../stores/FilterStore';
|
||||
import FlairStore from '../../stores/FlairStore';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
@ -36,17 +36,7 @@ const TagPanel = React.createClass({
|
|||
|
||||
getInitialState() {
|
||||
return {
|
||||
// A list of group profiles for tags that are group IDs. The intention in future
|
||||
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
|
||||
// For now, it suffices to maintain a list of ordered group profiles.
|
||||
orderedGroupTagProfiles: [
|
||||
// {
|
||||
// groupId: '+awesome:foo.bar',{
|
||||
// name: 'My Awesome Community',
|
||||
// avatarUrl: 'mxc://...',
|
||||
// shortDescription: 'Some description...',
|
||||
// },
|
||||
],
|
||||
orderedTags: [],
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
|
@ -54,28 +44,15 @@ const TagPanel = React.createClass({
|
|||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
|
||||
this._filterStoreToken = FilterStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedTags: FilterStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderedTags = TagOrderStore.getOrderedTags() || [];
|
||||
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
|
||||
// XXX: One profile lookup failing will bring the whole lot down
|
||||
Promise.all(orderedGroupTags.map(
|
||||
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
||||
)).then((orderedGroupTagProfiles) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({orderedGroupTagProfiles});
|
||||
this.setState({
|
||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||
selectedTags: TagOrderStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
|
@ -85,6 +62,7 @@ const TagPanel = React.createClass({
|
|||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.removeListener("sync", this._onClientSync);
|
||||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
|
@ -95,7 +73,17 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
},
|
||||
|
||||
onClick() {
|
||||
_onClientSync(syncState, prevState) {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
// Load joined groups
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
|
@ -104,30 +92,65 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
onTagTileEndDrag() {
|
||||
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
|
||||
onClearFilterClick(ev) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
key={groupProfile.groupId + '_' + index}
|
||||
groupProfile={groupProfile}
|
||||
selected={this.state.selectedTags.includes(groupProfile.groupId)}
|
||||
onEndDrag={this.onTagTileEndDrag}
|
||||
key={tag}
|
||||
tag={tag}
|
||||
index={index}
|
||||
selected={this.state.selectedTags.includes(tag)}
|
||||
/>;
|
||||
});
|
||||
return <div className="mx_TagPanel" onClick={this.onClick}>
|
||||
<div className="mx_TagPanel_tagTileContainer">
|
||||
{ tags }
|
||||
</div>
|
||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
||||
|
||||
const clearButton = this.state.selectedTags.length > 0 ?
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/> :
|
||||
<div />;
|
||||
|
||||
return <div className="mx_TagPanel">
|
||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
{ clearButton }
|
||||
</AccessibleButton>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
className="mx_TagPanel_scroller"
|
||||
autoshow={true}
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ tags }
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbarWrapper>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_groupsButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
|
@ -58,49 +59,49 @@ var TimelinePanel = React.createClass({
|
|||
// representing. This may or may not have a room, depending on what it's
|
||||
// a timeline representing. If it has a room, we maintain RRs etc for
|
||||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
timelineSet: PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
manageReadMarkers: React.PropTypes.bool,
|
||||
manageReadReceipts: PropTypes.bool,
|
||||
manageReadMarkers: PropTypes.bool,
|
||||
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: React.PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
// typically this will be either 'eventId' or undefined.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// id of an event to jump to. If not given, will go to the end of the
|
||||
// live timeline.
|
||||
eventId: React.PropTypes.string,
|
||||
eventId: PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
// bottom of the viewport. If not given, will try to put the event
|
||||
// half way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
eventPixelOffset: PropTypes.number,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
onReadMarkerUpdated: PropTypes.func,
|
||||
|
||||
// maximum number of events to show in a timeline
|
||||
timelineCap: React.PropTypes.number,
|
||||
timelineCap: PropTypes.number,
|
||||
|
||||
// classname to use for the messagepanel
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// shape property to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: React.PropTypes.string,
|
||||
empty: PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// set off a pagination request.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
|
@ -621,6 +624,7 @@ var TimelinePanel = React.createClass({
|
|||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||
dis.dispatch({
|
||||
action: 'on_room_read',
|
||||
roomId: this.props.timelineSet.room.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1090,6 +1094,17 @@ var TimelinePanel = React.createClass({
|
|||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
_shouldPaginate: function() {
|
||||
// don't try to paginate while events in the timeline are
|
||||
// still being decrypted. We don't render events while they're
|
||||
// being decrypted, so they don't take up space in the timeline.
|
||||
// This means we can pull quite a lot of events into the timeline
|
||||
// and end up trying to render a lot of events.
|
||||
return !this.state.events.some((e) => {
|
||||
return e.isBeingDecrypted();
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -1107,9 +1122,9 @@ var TimelinePanel = React.createClass({
|
|||
// exist.
|
||||
if (this.state.timelineLoading) {
|
||||
return (
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<Loader />
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanelSpinner">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ContentMessages = require('../../ContentMessages');
|
||||
const dis = require('../../dispatcher');
|
||||
const filesize = require('filesize');
|
||||
|
@ -22,7 +23,7 @@ import { _t } from '../../languageHandler';
|
|||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
|
|
@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
|||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const PlatformPeg = require("../../PlatformPeg");
|
||||
|
@ -29,7 +30,6 @@ import Promise from 'bluebird';
|
|||
const packageJson = require('../../../package.json');
|
||||
const UserSettingsStore = require('../../UserSettingsStore');
|
||||
const CallMediaHandler = require('../../CallMediaHandler');
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
const Email = require('../../email');
|
||||
const AddThreepid = require('../../AddThreepid');
|
||||
const SdkConfig = require('../../SdkConfig');
|
||||
|
@ -78,6 +78,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "Pill.shouldHidePillAvatar" },
|
||||
{ id: "TextualBody.disableBigEmoji" },
|
||||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
{ id: "TagPanel.disableTagPanel" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -125,8 +126,8 @@ const THEMES = [
|
|||
|
||||
const IgnoredUser = React.createClass({
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
onUnignored: React.PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
onUnignored: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_onUnignoreClick: function() {
|
||||
|
@ -155,16 +156,16 @@ module.exports = React.createClass({
|
|||
displayName: 'UserSettings',
|
||||
|
||||
propTypes: {
|
||||
onClose: React.PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
// The brand string given when creating email pushers
|
||||
brand: React.PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
|
||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||
referralBaseUrl: React.PropTypes.string,
|
||||
referralBaseUrl: PropTypes.string,
|
||||
|
||||
// Team token for the referral link. If falsy, the referral section will
|
||||
// not appear
|
||||
teamToken: React.PropTypes.string,
|
||||
teamToken: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -375,7 +376,7 @@ module.exports = React.createClass({
|
|||
{ _t("For security, logging out will delete any end-to-end " +
|
||||
"encryption keys from this browser. If you want to be able " +
|
||||
"to decrypt your conversation history from future Riot sessions, " +
|
||||
"please export your room keys for safe-keeping.") }.
|
||||
"please export your room keys for safe-keeping.") }
|
||||
</div>,
|
||||
button: _t("Sign out"),
|
||||
extraButtons: [
|
||||
|
@ -793,11 +794,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Bug Report") }</h3>
|
||||
<h3>{ _t("Debug Logs Submission") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("Found a bug?") }</p>
|
||||
<p>{
|
||||
_t( "If you've submitted a bug via GitHub, debug logs can help " +
|
||||
"us track down the problem. Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited and the usernames of " +
|
||||
"other users. They do not contian messages.",
|
||||
)
|
||||
}</p>
|
||||
<button className="mx_UserSettings_button danger"
|
||||
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||
onClick={this._onBugReportClicked}>{ _t('Submit debug logs') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -811,6 +819,12 @@ module.exports = React.createClass({
|
|||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||
<br />
|
||||
{ _t('Privacy is important to us, so we don\'t collect any personal'
|
||||
+ ' or identifiable data for our analytics.') }
|
||||
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||
{ _t('Learn more about how we use analytics.') }
|
||||
</div>
|
||||
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -1103,6 +1117,7 @@ module.exports = React.createClass({
|
|||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const Notifications = sdk.getComponent("settings.Notifications");
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const avatarUrl = (
|
||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||
|
@ -1198,8 +1213,9 @@ module.exports = React.createClass({
|
|||
onCancelClick={this.props.onClose}
|
||||
/>
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body"
|
||||
autoshow={true}>
|
||||
<GeminiScrollbarWrapper
|
||||
className="mx_UserSettings_body"
|
||||
autoshow={true}>
|
||||
|
||||
<h3>{ _t("Profile") }</h3>
|
||||
|
||||
|
@ -1312,7 +1328,7 @@ module.exports = React.createClass({
|
|||
|
||||
{ this._renderDeactivateAccount() }
|
||||
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,10 +18,12 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
||||
|
@ -29,13 +31,13 @@ module.exports = React.createClass({
|
|||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
onLoginClick: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func,
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
onLoginClick: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -184,7 +186,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!config.disable_custom_urls) {
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
|
@ -36,27 +37,28 @@ module.exports = React.createClass({
|
|||
displayName: 'Login',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
|
||||
enableGuest: React.PropTypes.bool,
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different home server without confusing users.
|
||||
fallbackHsUrl: React.PropTypes.string,
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -217,6 +219,8 @@ module.exports = React.createClass({
|
|||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
|
@ -427,10 +431,10 @@ module.exports = React.createClass({
|
|||
// FIXME: remove status.im theme tweaks
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') }</h2>;
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -25,7 +26,7 @@ module.exports = React.createClass({
|
|||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk';
|
|||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
|
@ -35,31 +36,32 @@ module.exports = React.createClass({
|
|||
displayName: 'Registration',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
clientSecret: React.PropTypes.string,
|
||||
sessionId: React.PropTypes.string,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
idSid: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
brand: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
referrer: React.PropTypes.string,
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
clientSecret: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
teamServerConfig: PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string.isRequired,
|
||||
supportEmail: PropTypes.string.isRequired,
|
||||
// URL of the riot-team-server to get team configurations and track referrals
|
||||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
teamServerURL: PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
teamSelected: PropTypes.object,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -130,6 +132,7 @@ module.exports = React.createClass({
|
|||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
this._replaceClient();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -23,16 +26,20 @@ module.exports = React.createClass({
|
|||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: React.PropTypes.string, // ID for generating hash colours
|
||||
title: React.PropTypes.string, // onHover title text
|
||||
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -48,6 +55,16 @@ module.exports = React.createClass({
|
|||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
|
@ -66,6 +83,23 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
|
@ -25,15 +26,15 @@ module.exports = React.createClass({
|
|||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
member: PropTypes.object.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: React.PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
viewUserOnClick: React.PropTypes.bool,
|
||||
title: React.PropTypes.string,
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from "../../../index";
|
||||
|
@ -25,11 +26,11 @@ module.exports = React.createClass({
|
|||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -47,12 +48,34 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
) return;
|
||||
|
||||
this.setState({
|
||||
urls: this.getImageUrls(this.props),
|
||||
});
|
||||
},
|
||||
|
||||
getImageUrls: function(props) {
|
||||
return [
|
||||
ContentRepo.getHttpUriForMxc(
|
||||
|
@ -86,10 +109,15 @@ module.exports = React.createClass({
|
|||
|
||||
const mlist = props.room.currentState.members;
|
||||
const userIds = [];
|
||||
const leftUserIds = [];
|
||||
// for .. in optimisation to return early if there are >2 keys
|
||||
for (const uid in mlist) {
|
||||
if (mlist.hasOwnProperty(uid)) {
|
||||
userIds.push(uid);
|
||||
if (["join", "invite"].includes(mlist[uid].membership)) {
|
||||
userIds.push(uid);
|
||||
} else {
|
||||
leftUserIds.push(uid);
|
||||
}
|
||||
}
|
||||
if (userIds.length > 2) {
|
||||
return null;
|
||||
|
@ -111,6 +139,14 @@ module.exports = React.createClass({
|
|||
false,
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
// The other 1-1 user left, leaving just the current user, so show the left user's avatar
|
||||
if (leftUserIds.length === 1) {
|
||||
return mlist[leftUserIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
|
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomButton',
|
||||
propTypes: {
|
||||
onCreateRoom: React.PropTypes.func,
|
||||
onCreateRoom: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const Presets = {
|
||||
|
@ -28,8 +29,8 @@ const Presets = {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
preset: React.PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
preset: PropTypes.string,
|
||||
},
|
||||
|
||||
Presets: Presets,
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -22,9 +23,9 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// Specifying a homeserver will make magical things happen when you,
|
||||
// e.g. start typing in the room alias box.
|
||||
homeserver: React.PropTypes.string,
|
||||
alias: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
homeserver: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
|
@ -507,7 +506,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
|
@ -580,14 +580,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{ this.props.title }
|
||||
</div>
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished} title={this.props.title}>
|
||||
<div className="mx_ChatInviteDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>
|
||||
|
@ -597,12 +591,10 @@ module.exports = React.createClass({
|
|||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,10 +15,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -31,33 +36,48 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// onFinished callback to call when Escape is pressed
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// callback to call when Enter is pressed
|
||||
onEnterPressed: React.PropTypes.func,
|
||||
// called when a key is pressed
|
||||
onKeyDown: PropTypes.func,
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// Title for the dialog.
|
||||
// (could probably actually be something more complicated than a string if desired)
|
||||
title: React.PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: React.PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: React.PropTypes.string,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished();
|
||||
} else if (e.keyCode === KeyCode.ENTER) {
|
||||
if (this.props.onEnterPressed) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onEnterPressed(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -69,17 +89,28 @@ export default React.createClass({
|
|||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={this.props.className}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completelly follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -58,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
);
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
|
@ -127,7 +129,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
||||
</AccessibleButton>;
|
||||
content = <div className="mx_Dialog_content">
|
||||
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ _t('You already have existing direct chats with this user:') }
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{ this.state.tiles }
|
||||
|
@ -137,6 +139,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
} else {
|
||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
title = _t('Start chatting');
|
||||
|
||||
|
@ -144,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
if (this.state.busyProfile) {
|
||||
profile = <Spinner />;
|
||||
} else if (this.state.profileError) {
|
||||
profile = <div className="error">
|
||||
profile = <div className="error" role="alert">
|
||||
Unable to load profile information for { this.props.userId }
|
||||
</div>;
|
||||
} else {
|
||||
|
@ -160,17 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
content = <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>
|
||||
{ _t('Click on the button below to start chatting!') }
|
||||
</p>
|
||||
{ profile }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
|
||||
{ _t('Start Chatting') }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -179,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
|
@ -187,9 +188,9 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onNewDMClick: React.PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
onNewDMClick: PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
|
@ -33,20 +33,20 @@ export default React.createClass({
|
|||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: React.PropTypes.object,
|
||||
member: PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason: React.PropTypes.bool,
|
||||
danger: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
askReason: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
|
@ -76,13 +76,11 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
});
|
||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||
|
||||
let reasonBox;
|
||||
if (this.props.askReason) {
|
||||
|
@ -116,10 +114,10 @@ export default React.createClass({
|
|||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div id="mx_Dialog_content" className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
{ avatar }
|
||||
</div>
|
||||
|
@ -127,17 +125,11 @@ export default React.createClass({
|
|||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{ reasonBox }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{ this.props.action }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.action}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={confirmButtonClass}
|
||||
focus={!this.props.askReason}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -55,11 +55,15 @@ export default React.createClass({
|
|||
|
||||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot not be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
// Reset createError to get rid of now stale error message
|
||||
createError: null,
|
||||
});
|
||||
return error;
|
||||
},
|
||||
|
@ -108,7 +112,7 @@ export default React.createClass({
|
|||
// 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">
|
||||
createErrorNode = <div className="error" role="alert">
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
|
@ -116,7 +120,6 @@ export default React.createClass({
|
|||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
|
@ -159,10 +162,10 @@ export default React.createClass({
|
|||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this._onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
|||
export default React.createClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -41,40 +42,37 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={_t('Create Room')}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t('Cancel') }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ _t('Create Room') }
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={_t('Create Room')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let passwordBoxClass = '';
|
||||
|
||||
|
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
{ _t("Deactivate Account") }
|
||||
</div>
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||
|
||||
|
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeactivateAccountDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
|
|||
}
|
||||
|
||||
DeviceVerifyDialog.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -26,20 +26,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -51,22 +52,18 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
title={this.props.title || _t('Error')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,22 +28,22 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
title: React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -72,11 +73,12 @@ export default React.createClass({
|
|||
let content;
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
<div>
|
||||
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
autoFocus="true"
|
||||
>
|
||||
{ _t("Dismiss") }
|
||||
</AccessibleButton>
|
||||
|
@ -84,7 +86,7 @@ export default React.createClass({
|
|||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
|
@ -99,6 +101,7 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
|
|||
*/
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
deviceId: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -125,11 +126,11 @@ export default React.createClass({
|
|||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
<button onClick={this._onVerifyClicked} autoFocus="true">
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
|
@ -153,7 +154,7 @@ export default React.createClass({
|
|||
content = this._renderContent();
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ _t('Loading device info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
@ -164,6 +165,7 @@ export default React.createClass({
|
|||
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
|
|
|
@ -16,20 +16,20 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
danger: React.PropTypes.bool,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
danger: PropTypes.bool,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -53,30 +53,27 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
) : null;
|
||||
const buttonClasses = classnames({
|
||||
mx_Dialog_primary: true,
|
||||
danger: this.props.danger,
|
||||
});
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let primaryButtonClass = "";
|
||||
if (this.props.danger) {
|
||||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
{ this.props.extraButtons }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -25,8 +26,14 @@ export default React.createClass({
|
|||
displayName: 'SessionRestoreErrorDialog',
|
||||
|
||||
propTypes: {
|
||||
error: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.bugreportLink) {
|
||||
this.refs.bugreportLink.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_sendBugReport: function() {
|
||||
|
@ -40,6 +47,7 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let bugreport;
|
||||
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
|
@ -48,16 +56,20 @@ export default React.createClass({
|
|||
{ _t(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
||||
{ 'a': (sub) => <a ref="bugreportLink" onClick={this._sendBugReport}
|
||||
key="bugreport" href='#'>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const shouldFocusContinueButton =!(bugreport==true);
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Unable to restore session')}>
|
||||
<div className="mx_Dialog_content">
|
||||
title={_t('Unable to restore session')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||
"you continue, you will need to log in again, and encrypted chat " +
|
||||
"history will be unreadable.") }</p>
|
||||
|
@ -68,11 +80,9 @@ export default React.createClass({
|
|||
|
||||
{ bugreport }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{ _t("Continue anyway") }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Continue anyway")}
|
||||
onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
|
||||
onCancel={this.props.onFinished} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
|
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
|
|||
export default React.createClass({
|
||||
displayName: 'SetEmailDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -40,9 +41,6 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
},
|
||||
|
||||
onEmailAddressChanged: function(value) {
|
||||
this.setState({
|
||||
emailAddress: value,
|
||||
|
@ -130,6 +128,7 @@ export default React.createClass({
|
|||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={false}
|
||||
|
@ -139,9 +138,10 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_SetEmailDialog"
|
||||
onFinished={this.onCancelled}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
<p id='mx_Dialog_content'>
|
||||
{ _t('This will allow you to reset your password and receive notifications.') }
|
||||
</p>
|
||||
{ emailInput }
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
|
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
|||
export default React.createClass({
|
||||
displayName: 'SetMxIdDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
// Called when the user requests to register with a different homeserver
|
||||
onDifferentServerClicked: React.PropTypes.func.isRequired,
|
||||
onDifferentServerClicked: PropTypes.func.isRequired,
|
||||
// Called if the user wants to switch to login instead
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -234,14 +235,14 @@ export default React.createClass({
|
|||
"error": Boolean(this.state.usernameError),
|
||||
"success": usernameAvailable,
|
||||
});
|
||||
usernameIndicator = <div className={usernameIndicatorClasses}>
|
||||
usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
|
||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let authErrorIndicator = null;
|
||||
if (this.state.authError) {
|
||||
authErrorIndicator = <div className="error">
|
||||
authErrorIndicator = <div className="error" role="alert">
|
||||
{ this.state.authError }
|
||||
</div>;
|
||||
}
|
||||
|
@ -253,8 +254,9 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_SetMxIdDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('To get started, please pick a username!')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref="input_value" value={this.state.username}
|
||||
autoFocus={true}
|
||||
|
|
|
@ -15,21 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'TextInputDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
value: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -58,27 +58,24 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
|
|||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function markAllDevicesKnown(devices) {
|
||||
Object.keys(devices).forEach((userId) => {
|
||||
Object.keys(devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
import { markAllDevicesKnown } from '../../../cryptodevices';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -141,7 +134,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_onSendAnywayClicked: function() {
|
||||
markAllDevicesKnown(this.props.devices);
|
||||
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
|
||||
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
|
@ -153,6 +146,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
|
@ -187,24 +181,18 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
});
|
||||
let sendButton;
|
||||
if (haveUnknownDevices) {
|
||||
sendButton = <button onClick={this._onSendAnywayClicked}>
|
||||
{ this.props.sendAnywayLabel }
|
||||
</button>;
|
||||
} else {
|
||||
sendButton = <button onClick={this._onSendClicked}>
|
||||
{ this.props.sendLabel }
|
||||
</button>;
|
||||
}
|
||||
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
|
||||
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Room contains unknown devices')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<h4>
|
||||
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||
</h4>
|
||||
|
@ -212,15 +200,10 @@ export default React.createClass({
|
|||
{ _t("Unknown devices") }:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{sendButton}
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={this._onDismissClicked}
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
<DialogButtons primaryButton={sendButtonLabel}
|
||||
onPrimaryButtonClick={sendButtonOnClick}
|
||||
onCancel={this._onDismissClicked} />
|
||||
</BaseDialog>
|
||||
);
|
||||
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
|
@ -27,8 +30,34 @@ import React from 'react';
|
|||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, ...restProps} = props;
|
||||
restProps.onClick = onClick;
|
||||
// We need to consume enter onKeyDown and space onKeyUp
|
||||
// otherwise we are risking also activating other keyboard focusable elements
|
||||
// that might receive focus as a result of the AccessibleButtonClick action
|
||||
// It's because we are using html buttons at a few places e.g. inside dialogs
|
||||
// And divs which we report as role button to assistive technologies.
|
||||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||
// inconsistencies here
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.onKeyUp = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||
if (e.keyCode === KeyCode.SPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return onClick(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
|
@ -44,9 +73,9 @@ export default function AccessibleButton(props) {
|
|||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
element: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
|
@ -26,17 +27,17 @@ export default React.createClass({
|
|||
displayName: 'AddressSelector',
|
||||
|
||||
propTypes: {
|
||||
onSelected: React.PropTypes.func.isRequired,
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
// Whether to show the address on the address tiles
|
||||
showAddress: React.PropTypes.bool,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
showAddress: PropTypes.bool,
|
||||
truncateAt: PropTypes.number.isRequired,
|
||||
selected: PropTypes.number,
|
||||
|
||||
// Element to put as a header on top of the list
|
||||
header: React.PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
@ -28,9 +29,9 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
address: UserAddressType.isRequired,
|
||||
canDismiss: React.PropTypes.bool,
|
||||
onDismissed: React.PropTypes.func,
|
||||
justified: React.PropTypes.bool,
|
||||
canDismiss: PropTypes.bool,
|
||||
onDismissed: PropTypes.func,
|
||||
justified: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
|
@ -35,32 +36,25 @@ import WidgetUtils from '../../../WidgetUtils';
|
|||
import dis from '../../../dispatcher';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
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,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: React.PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: React.PropTypes.string,
|
||||
waitForIframeLoad: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
};
|
||||
},
|
||||
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||
|
@ -72,8 +66,8 @@ export default React.createClass({
|
|||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
|
@ -82,8 +76,20 @@ export default React.createClass({
|
|||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
|
||||
this.props.whitelistCapabilities : [],
|
||||
requestedCapabilities: [],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the widget support a given capability
|
||||
* @param {[type]} capability Capability to check for
|
||||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return this.state.allowedCapabilities.some((c) => {return c === capability;});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
|
@ -111,11 +117,7 @@ export default React.createClass({
|
|||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this._getNewState(this.props);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
|
@ -139,7 +141,7 @@ export default React.createClass({
|
|||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
|
@ -151,14 +153,36 @@ export default React.createClass({
|
|||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
WidgetMessaging.startListening();
|
||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
this.setScalarToken();
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
|
||||
// postMessaging API
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Widget postMessage listeners
|
||||
try {
|
||||
if (this.widgetMessaging) {
|
||||
this.widgetMessaging.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||
}
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
|
@ -210,13 +234,7 @@ export default React.createClass({
|
|||
initialising: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetMessaging.stopListening();
|
||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.url !== this.props.url) {
|
||||
|
@ -231,8 +249,10 @@ export default React.createClass({
|
|||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Legacy Jitsi widget messaging
|
||||
// TODO -- This should be replaced with the new widget postMessaging API
|
||||
_onMessage(event) {
|
||||
if (this.props.type !== 'jitsi') {
|
||||
return;
|
||||
|
@ -250,63 +270,140 @@ export default React.createClass({
|
|||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_canUserModify() {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
},
|
||||
}
|
||||
|
||||
_onEditClick(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, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
this.widgetMessaging.getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
* otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
_onDeleteClick() {
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
if (!this.widgetMessaging) {
|
||||
this._onInitialLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on initial load of the widget iframe
|
||||
*/
|
||||
_onInitialLoad() {
|
||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
let requestedWhitelistCapabilies = [];
|
||||
|
||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||
return this.indexOf(e)>=0;
|
||||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
|
||||
requestedWhitelistCapabilies);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
this.setState({
|
||||
requestedCapabilities,
|
||||
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
|
||||
});
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||
});
|
||||
this.setState({loading: false});
|
||||
},
|
||||
}
|
||||
|
||||
_onWidgetAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
|
@ -320,7 +417,7 @@ export default React.createClass({
|
|||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
|
@ -329,20 +426,20 @@ export default React.createClass({
|
|||
return _td('Delete widget');
|
||||
}
|
||||
return _td('Revoke widget access');
|
||||
},
|
||||
}
|
||||
|
||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||
_grantWidgetPermission() {
|
||||
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
},
|
||||
}
|
||||
|
||||
_revokeWidgetPermission() {
|
||||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.removeItem(this.state.widgetPermissionId);
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
},
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
|
@ -350,7 +447,7 @@ export default React.createClass({
|
|||
appTileName = this.props.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
}
|
||||
|
||||
onClickMenuBar(ev) {
|
||||
ev.preventDefault();
|
||||
|
@ -365,16 +462,42 @@ export default React.createClass({
|
|||
action: 'appsDrawer',
|
||||
show: !this.props.show,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
},
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||
title = this.state.widgetPageTitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<b>{ name }</b>
|
||||
<span>{ title ? titleSpacer : '' }{ title }</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
_onMinimiseClick(e) {
|
||||
if (this.props.onMinimiseClick) {
|
||||
this.props.onMinimiseClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
@ -392,9 +515,13 @@ export default React.createClass({
|
|||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<div>
|
||||
<MessageSpinner msg='Loading...' />
|
||||
</div>
|
||||
);
|
||||
|
@ -409,9 +536,15 @@ export default React.createClass({
|
|||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
"allow" attribute, which is unknown to react 15.
|
||||
*/ }
|
||||
<iframe
|
||||
is
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
|
@ -445,29 +578,42 @@ export default React.createClass({
|
|||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
// Picture snapshot - only show button when apps are maximised.
|
||||
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle">
|
||||
<TintableSvgButton
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ this.props.showMinimise && <TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||
<span> - { this.state.widgetPageTitle }</span>
|
||||
) }
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Snapshot widget */ }
|
||||
{ showPictureSnapshotButton && <TintableSvgButton
|
||||
src={showPictureSnapshotIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Picture')}
|
||||
onClick={this._onSnapshotClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
className={"mx_AppTileMenuBarWidget " +
|
||||
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
|
@ -475,18 +621,71 @@ export default React.createClass({
|
|||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
<TintableSvgButton
|
||||
{ this.props.showDelete && <TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
/> }
|
||||
</span>
|
||||
</div>
|
||||
</div> }
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AppTile.displayName ='AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: PropTypes.string,
|
||||
waitForIframeLoad: PropTypes.bool,
|
||||
showMenubar: PropTypes.bool,
|
||||
// Should the AppTile render itself
|
||||
show: PropTypes.bool,
|
||||
// Optional onEditClickHandler (overrides default behaviour)
|
||||
onEditClick: PropTypes.func,
|
||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||
onDeleteClick: PropTypes.func,
|
||||
// Optional onMinimiseClickHandler
|
||||
onMinimiseClick: PropTypes.func,
|
||||
// Optionally hide the tile title
|
||||
showTitle: PropTypes.bool,
|
||||
// Optionally hide the tile minimise icon
|
||||
showMinimise: PropTypes.bool,
|
||||
// Optionally handle minimise button pointer events (default false)
|
||||
handleMinimisePointerEvents: PropTypes.bool,
|
||||
// Optionally hide the delete icon
|
||||
showDelete: PropTypes.bool,
|
||||
// Widget apabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Optional function to be called on widget capability request
|
||||
// Called with an array of the requested capabilities
|
||||
onCapabilityRequest: PropTypes.func,
|
||||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
showMinimise: true,
|
||||
showDelete: true,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
};
|
||||
|
|
|
@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import dis from '../../../dispatcher';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
const tagTileSource = {
|
||||
canDrag: function(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
beginDrag: function(props) {
|
||||
// Return the data describing the dragged item
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor, component) {
|
||||
const dropResult = monitor.getDropResult();
|
||||
if (!monitor.didDrop() || !dropResult) {
|
||||
return;
|
||||
}
|
||||
props.onEndDrag();
|
||||
},
|
||||
};
|
||||
|
||||
const tagTileTarget = {
|
||||
canDrop(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
hover(props, monitor, component) {
|
||||
if (!monitor.canDrop()) return;
|
||||
const draggedY = monitor.getClientOffset().y;
|
||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||
const targetY = (top + bottom) / 2;
|
||||
dis.dispatch({
|
||||
action: 'order_tag',
|
||||
tag: monitor.getItem().tag,
|
||||
targetTag: props.groupProfile.groupId,
|
||||
// Note: we indicate that the tag should be after the target when
|
||||
// it's being dragged over the top half of the target.
|
||||
after: draggedY < targetY,
|
||||
});
|
||||
},
|
||||
|
||||
drop(props) {
|
||||
// Return the data to be returned by getDropResult
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default
|
||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
}))((props) => {
|
||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||
return connectDropTarget(connectDragSource(
|
||||
<div>
|
||||
<TagTile {...otherProps} />
|
||||
</div>,
|
||||
));
|
||||
}));
|
||||
export default function DNDTagTile(props) {
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile {...props} />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,8 +25,8 @@ export default React.createClass({
|
|||
displayName: 'DeviceVerifyButtons',
|
||||
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
62
src/components/views/elements/DialogButtons.js
Normal file
62
src/components/views/elements/DialogButtons.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2017 Aidan Gauland
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Basic container for buttons in modal dialogs.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: "DialogButtons",
|
||||
|
||||
propTypes: {
|
||||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: PropTypes.node.isRequired,
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||
|
||||
// onClick handler for the cancel button.
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
|
||||
focus: PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let primaryButtonClassName = "mx_Dialog_primary";
|
||||
if (this.props.primaryButtonClass) {
|
||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
}
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={primaryButtonClassName}
|
||||
onClick={this.props.onPrimaryButtonClick}
|
||||
autoFocus={this.props.focus}
|
||||
>
|
||||
{ this.props.primaryButton }
|
||||
</button>
|
||||
{ this.props.children }
|
||||
<button onClick={this.props.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class DirectorySearchBox extends React.Component {
|
||||
|
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
|
|||
}
|
||||
|
||||
DirectorySearchBox.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
onClear: React.PropTypes.func,
|
||||
onJoinClick: React.PropTypes.func,
|
||||
placeholder: React.PropTypes.string,
|
||||
showJoinButton: React.PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
onJoinClick: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
showJoinButton: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
|
|||
}
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
React.PropTypes.node,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(React.PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
highlighted: React.PropTypes.bool,
|
||||
dropdownKey: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
onMouseEnter: React.PropTypes.func.isRequired,
|
||||
highlighted: PropTypes.bool,
|
||||
dropdownKey: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -322,20 +323,20 @@ Dropdown.propTypes = {
|
|||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
// Called when the selected option changes
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
searchEnabled: React.PropTypes.bool,
|
||||
onSearchChange: PropTypes.func,
|
||||
searchEnabled: PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
getShortOption: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
// negative for consistency with HTML
|
||||
disabled: React.PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
|
@ -26,18 +27,18 @@ module.exports = React.createClass({
|
|||
displayName: 'EditableText',
|
||||
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
labelClassName: React.PropTypes.string,
|
||||
placeholderClassName: React.PropTypes.string,
|
||||
onValueChanged: PropTypes.func,
|
||||
initialValue: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
placeholderClassName: PropTypes.string,
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel: React.PropTypes.bool,
|
||||
blurToCancel: PropTypes.bool,
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
},
|
||||
|
||||
Phases: {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
|
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
|
|||
|
||||
EditableTextContainer.propTypes = {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue: React.PropTypes.func,
|
||||
getInitialValue: PropTypes.func,
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue: React.PropTypes.string,
|
||||
initialValue: PropTypes.string,
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder: React.PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: React.PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
|
@ -32,8 +33,8 @@ export default function EmojiText(props) {
|
|||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
element: React.PropTypes.string,
|
||||
children: React.PropTypes.string.isRequired,
|
||||
element: PropTypes.string,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiText.defaultProps = {
|
||||
|
|
|
@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
|
|||
};
|
||||
|
||||
FlairAvatar.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
export default class Flair extends React.Component {
|
||||
|
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
|
|||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
|
||||
this.setState({
|
||||
profiles: profiles.filter((profile) => {
|
||||
return profile ? profile.avatarUrl : false;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,5 +138,5 @@ Flair.propTypes = {
|
|||
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||
Flair.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
|
35
src/components/views/elements/GeminiScrollbarWrapper.js
Normal file
35
src/components/views/elements/GeminiScrollbarWrapper.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function GeminiScrollbarWrapper(props) {
|
||||
const {wrappedRef, ...wrappedProps} = props;
|
||||
|
||||
// Enable forceGemini so that gemini is always enabled. This is
|
||||
// to avoid future issues where a feature is implemented without
|
||||
// doing QA on every OS/browser combination.
|
||||
//
|
||||
// By default GeminiScrollbar allows native scrollbars to be used
|
||||
// on macOS. Use forceGemini to enable Gemini's non-native
|
||||
// scrollbars on all OSs.
|
||||
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
|
||||
{ props.children }
|
||||
</GeminiScrollbar>;
|
||||
}
|
||||
export default GeminiScrollbarWrapper;
|
||||
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
|
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
|
|||
}
|
||||
|
||||
LanguageDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -64,7 +64,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||
null,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
|
@ -103,5 +103,5 @@ export default class ManageIntegsButton extends React.Component {
|
|||
}
|
||||
|
||||
ManageIntegsButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -23,19 +24,19 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// An array of member events to summarise
|
||||
events: React.PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: React.PropTypes.array.isRequired,
|
||||
children: PropTypes.array.isRequired,
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength: React.PropTypes.number,
|
||||
summaryLength: PropTypes.number,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: React.PropTypes.number,
|
||||
avatarsMaxLength: PropTypes.number,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: React.PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
onToggle: PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: React.PropTypes.bool,
|
||||
startExpanded: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -17,7 +17,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
|
@ -61,6 +61,17 @@ const Pill = React.createClass({
|
|||
shouldShowPillAvatar: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// ID/alias of the room/user
|
||||
|
@ -135,6 +146,7 @@ const Pill = React.createClass({
|
|||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this.componentWillReceiveProps(this.props);
|
||||
},
|
||||
|
||||
|
@ -195,6 +207,7 @@ const Pill = React.createClass({
|
|||
const member = this.state.member;
|
||||
if (member) {
|
||||
userId = member.userId;
|
||||
member.rawDisplayName = member.rawDisplayName || '';
|
||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
@ -24,23 +25,26 @@ module.exports = React.createClass({
|
|||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: React.PropTypes.number.isRequired,
|
||||
maxValue: PropTypes.number.isRequired,
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: React.PropTypes.number.isRequired,
|
||||
usersDefault: PropTypes.number.isRequired,
|
||||
|
||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||
// to reflect the current value, rather than left freeform.
|
||||
// MemberInfo uses controlled; RoomSettings uses non-controlled.
|
||||
//
|
||||
// ignored if disabled is truthy. false by default.
|
||||
controlled: React.PropTypes.bool,
|
||||
controlled: PropTypes.bool,
|
||||
|
||||
// should the user be able to change the value? false by default.
|
||||
disabled: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
|
||||
// Optional key to pass as the second argument to `onChange`
|
||||
powerLevelKey: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -83,17 +87,17 @@ module.exports = React.createClass({
|
|||
onSelectChange: function(event) {
|
||||
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||
this.props.onChange(event.target.value);
|
||||
this.props.onChange(event.target.value, this.props.powerLevelKey);
|
||||
}
|
||||
},
|
||||
|
||||
onCustomBlur: function(event) {
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
this.props.onChange(parseInt(this.refs.custom.value), this.props.powerLevelKey);
|
||||
},
|
||||
|
||||
onCustomKeyDown: function(event) {
|
||||
if (event.key == "Enter") {
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
this.props.onChange(parseInt(this.refs.custom.value), this.props.powerLevelKey);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -17,12 +17,13 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ProgressBar',
|
||||
propTypes: {
|
||||
value: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
188
src/components/views/elements/Quote.js
Normal file
188
src/components/views/elements/Quote.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
|
||||
|
||||
export default class Quote extends React.Component {
|
||||
static isMessageUrl(url) {
|
||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.object,
|
||||
addRichQuote: PropTypes.func,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
// The matrix.to url of the event
|
||||
url: PropTypes.string,
|
||||
// The original node that was rendered
|
||||
node: PropTypes.instanceOf(Element),
|
||||
// The parent event
|
||||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// The event related to this quote and their nested rich quotes
|
||||
events: [],
|
||||
// Whether the top (oldest) event should be shown or spoilered
|
||||
show: true,
|
||||
// Whether an error was encountered fetching nested older event, show node if it does
|
||||
err: false,
|
||||
};
|
||||
|
||||
this.onQuoteClick = this.onQuoteClick.bind(this);
|
||||
this.addRichQuote = this.addRichQuote.bind(this);
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
addRichQuote: this.addRichQuote,
|
||||
};
|
||||
}
|
||||
|
||||
parseUrl(url) {
|
||||
if (!url) return;
|
||||
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
|
||||
|
||||
const [, roomIdentifier, eventId] = matrixToMatch;
|
||||
return {roomIdentifier, eventId};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
|
||||
if (!roomIdentifier || !eventId) return;
|
||||
|
||||
const room = this.getRoom(roomIdentifier);
|
||||
if (!room) return;
|
||||
|
||||
// Only try and load the event if we know about the room
|
||||
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
|
||||
this.setState({ events: [] });
|
||||
if (room) this.getEvent(room, eventId, true);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.componentWillReceiveProps(this.props);
|
||||
}
|
||||
|
||||
getRoom(id) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (id[0] === '!') return cli.getRoom(id);
|
||||
|
||||
return cli.getRooms().find((r) => {
|
||||
return r.getAliases().includes(id);
|
||||
});
|
||||
}
|
||||
|
||||
async getEvent(room, eventId, show) {
|
||||
const event = room.findEventById(eventId);
|
||||
if (event) {
|
||||
this.addEvent(event, show);
|
||||
return;
|
||||
}
|
||||
|
||||
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
||||
this.addEvent(room.findEventById(eventId), show);
|
||||
}
|
||||
|
||||
addEvent(event, show) {
|
||||
const events = [event].concat(this.state.events);
|
||||
this.setState({events, show});
|
||||
}
|
||||
|
||||
// addRichQuote(roomId, eventId) {
|
||||
addRichQuote(href) {
|
||||
const {roomIdentifier, eventId} = this.parseUrl(href);
|
||||
if (!roomIdentifier || !eventId) {
|
||||
this.setState({ err: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const room = this.getRoom(roomIdentifier);
|
||||
if (!room) {
|
||||
this.setState({ err: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.getEvent(room, eventId, false);
|
||||
}
|
||||
|
||||
onQuoteClick() {
|
||||
this.setState({ show: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const events = this.state.events.slice();
|
||||
if (events.length) {
|
||||
const evTiles = [];
|
||||
|
||||
if (!this.state.show) {
|
||||
const oldestEv = events.shift();
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
|
||||
|
||||
evTiles.push(<blockquote className="mx_Quote" key="load">
|
||||
{
|
||||
_t('<a>In reply to</a> <pill>', {}, {
|
||||
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
|
||||
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
||||
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
|
||||
})
|
||||
}
|
||||
</blockquote>);
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
events.forEach((ev) => {
|
||||
let dateSep = null;
|
||||
|
||||
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
|
||||
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
|
||||
}
|
||||
|
||||
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
|
||||
{ dateSep }
|
||||
<EventTile mxEvent={ev} tileShape="quote" />
|
||||
</blockquote>);
|
||||
});
|
||||
|
||||
return <div>{ evTiles }</div>;
|
||||
}
|
||||
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
// in case we get an undefined/falsey node, replace it with null to make React happy
|
||||
return this.props.node || null;
|
||||
}
|
||||
}
|
|
@ -15,23 +15,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SettingsFlag',
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
level: React.PropTypes.string.isRequired,
|
||||
roomId: React.PropTypes.string, // for per-room settings
|
||||
label: React.PropTypes.string, // untranslated
|
||||
onChange: React.PropTypes.func,
|
||||
isExplicit: React.PropTypes.bool,
|
||||
manualSave: React.PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
level: PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string, // for per-room settings
|
||||
label: PropTypes.string, // untranslated
|
||||
onChange: PropTypes.func,
|
||||
isExplicit: PropTypes.bool,
|
||||
manualSave: PropTypes.bool,
|
||||
|
||||
// If group is supplied, then this will create a radio button instead.
|
||||
group: React.PropTypes.string,
|
||||
value: React.PropTypes.any, // the value for the radio button
|
||||
group: PropTypes.string,
|
||||
value: PropTypes.any, // the value for the radio button
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -20,36 +20,106 @@ import classNames from 'classnames';
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
// - Rooms that are part of the group
|
||||
// - Direct messages with members of the group
|
||||
// with the intention that this could be expanded to arbitrary tags in future.
|
||||
export default React.createClass({
|
||||
displayName: 'TagTile',
|
||||
|
||||
propTypes: {
|
||||
groupProfile: PropTypes.object,
|
||||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// Whether the mouse is over the tile
|
||||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
this._onFlairStoreUpdated();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.removeListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
_onFlairStoreUpdated() {
|
||||
if (this.unmounted) return;
|
||||
FlairStore.getGroupProfileCached(
|
||||
this.context.matrixClient,
|
||||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: this.props.groupProfile.groupId,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||
tag: this.props.tag,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
@ -62,8 +132,8 @@ export default React.createClass({
|
|||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
const profile = this.props.groupProfile || {};
|
||||
const name = profile.name || profile.groupId;
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 35;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
|
@ -78,10 +148,21 @@ export default React.createClass({
|
|||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
|
|
|
@ -18,16 +18,17 @@ limitations under the License.
|
|||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
const Tinter = require("../../../Tinter");
|
||||
|
||||
var TintableSvg = React.createClass({
|
||||
displayName: 'TintableSvg',
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
width: React.PropTypes.string.isRequired,
|
||||
height: React.PropTypes.string.isRequired,
|
||||
className: React.PropTypes.string,
|
||||
src: PropTypes.string.isRequired,
|
||||
width: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
|
|
@ -17,14 +17,15 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSelector',
|
||||
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
|
||||
onChange: PropTypes.func,
|
||||
selected_users: PropTypes.arrayOf(React.PropTypes.string),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler';
|
|||
import { GroupMemberType } from '../../../groups';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GroupMemberInfo',
|
||||
|
@ -132,7 +131,9 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
if (this.state.removingUser) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
return <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
|
@ -178,9 +179,10 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbar autoshow={true}>
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
|
@ -197,7 +199,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,7 +18,6 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
|
@ -134,6 +133,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.state.fetching || this.state.fetchingInvitedMembers) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_MemberList">
|
||||
|
@ -162,10 +162,10 @@ export default React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -22,7 +22,6 @@ import Modal from '../../../Modal';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GroupRoomInfo',
|
||||
|
@ -157,6 +156,7 @@ module.exports = React.createClass({
|
|||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberInfo">
|
||||
|
@ -216,7 +216,7 @@ module.exports = React.createClass({
|
|||
const avatar = <BaseAvatar name={groupRoomName} width={36} height={36} url={avatarUrl} />;
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbar autoshow={true}>
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
|
@ -233,7 +233,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -17,7 +17,6 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||
|
@ -120,16 +119,17 @@ export default React.createClass({
|
|||
</form>
|
||||
);
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return (
|
||||
<div className="mx_GroupRoomList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -66,7 +66,7 @@ const GroupRoomTile = React.createClass({
|
|||
});
|
||||
|
||||
GroupRoomTile.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -17,10 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
|
@ -33,7 +35,7 @@ const GroupTile = React.createClass({
|
|||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -57,7 +59,7 @@ const GroupTile = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
onMouseDown: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -77,10 +79,52 @@ const GroupTile = React.createClass({
|
|||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div>
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div>
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ?
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import sdk from '../../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -55,14 +54,15 @@ export default React.createClass({
|
|||
text = _t('Loading...');
|
||||
} else if (groups.length > 0) {
|
||||
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
|
||||
const groupPublicityToggles = groups.map((groupId, index) => {
|
||||
return <GroupPublicityToggle key={index} groupId={groupId} />;
|
||||
});
|
||||
text = _t('Display your community flair in rooms configured to show it.');
|
||||
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
|
||||
<GeminiScrollbar>
|
||||
<GeminiScrollbarWrapper>
|
||||
{ groupPublicityToggles }
|
||||
</GeminiScrollbar>
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>;
|
||||
} else {
|
||||
text = _t("You're not currently a member of any communities.");
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
@ -29,10 +30,10 @@ module.exports = React.createClass({
|
|||
displayName: 'CaptchaForm',
|
||||
|
||||
propTypes: {
|
||||
sitePublicKey: React.PropTypes.string,
|
||||
sitePublicKey: PropTypes.string,
|
||||
|
||||
// called with the captcha response
|
||||
onCaptchaResponse: React.PropTypes.func,
|
||||
onCaptchaResponse: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CasLogin',
|
||||
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func, // fn()
|
||||
onSubmit: PropTypes.func, // fn()
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
@ -131,11 +132,11 @@ export default class CountryDropdown extends React.Component {
|
|||
}
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
isSmall: React.PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
isSmall: PropTypes.bool,
|
||||
// if isSmall, show +44 in the selected value
|
||||
showPrefix: React.PropTypes.bool,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
showPrefix: PropTypes.bool,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -69,12 +70,12 @@ export const PasswordAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: React.PropTypes.bool,
|
||||
busy: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -127,12 +128,22 @@ export const PasswordAuthEntry = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<p>{ _t("Password:") }</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<label htmlFor="passwordField">{ _t("Password:") }</label>
|
||||
<input
|
||||
name="passwordField"
|
||||
ref="passwordField"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
|
@ -142,9 +153,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
{ submitButtonOrSpinner }
|
||||
</div>
|
||||
</form>
|
||||
<div className="error">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -158,10 +167,10 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
stageParams: React.PropTypes.object.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
busy: React.PropTypes.bool,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
|
@ -179,14 +188,22 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
|
||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||
const sitePublicKey = this.props.stageParams.public_key;
|
||||
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||
onCaptchaResponse={this._onCaptchaResponse}
|
||||
/>
|
||||
<div className="error">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
{ errorSection }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -200,15 +217,15 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
authSessionId: React.PropTypes.string.isRequired,
|
||||
clientSecret: React.PropTypes.string.isRequired,
|
||||
inputs: React.PropTypes.object.isRequired,
|
||||
stageState: React.PropTypes.object.isRequired,
|
||||
fail: React.PropTypes.func.isRequired,
|
||||
setEmailSid: React.PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
clientSecret: PropTypes.string.isRequired,
|
||||
inputs: PropTypes.object.isRequired,
|
||||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -275,15 +292,15 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
inputs: React.PropTypes.shape({
|
||||
phoneCountry: React.PropTypes.string,
|
||||
phoneNumber: React.PropTypes.string,
|
||||
inputs: PropTypes.shape({
|
||||
phoneCountry: PropTypes.string,
|
||||
phoneNumber: PropTypes.string,
|
||||
}),
|
||||
fail: React.PropTypes.func,
|
||||
clientSecret: React.PropTypes.func,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
matrixClient: React.PropTypes.object,
|
||||
submitAuthDict: React.PropTypes.func,
|
||||
fail: PropTypes.func,
|
||||
clientSecret: PropTypes.func,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object,
|
||||
submitAuthDict: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -371,6 +388,14 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||
mx_UserSettings_button: true, // XXX button classes
|
||||
});
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
|
@ -384,6 +409,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||
value={this.state.token}
|
||||
onChange={this._onTokenChange}
|
||||
aria-label={ _t("Code")}
|
||||
/>
|
||||
<br />
|
||||
<input type="submit" value={_t("Submit")}
|
||||
|
@ -391,9 +417,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
disabled={!enableSubmit}
|
||||
/>
|
||||
</form>
|
||||
<div className="error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
{errorSection}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -405,11 +429,11 @@ export const FallbackAuthEntry = React.createClass({
|
|||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
authSessionId: React.PropTypes.string.isRequired,
|
||||
loginType: React.PropTypes.string.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
authSessionId: PropTypes.string.isRequired,
|
||||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -426,6 +450,12 @@ export const FallbackAuthEntry = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.fallbackButton) {
|
||||
this.refs.fallbackButton.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onShowFallbackClick: function() {
|
||||
const url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
|
@ -444,12 +474,18 @@ export const FallbackAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<div className="error">
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -256,17 +257,17 @@ PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
|
|||
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||
initialUsername: React.PropTypes.string,
|
||||
initialPhoneCountry: React.PropTypes.string,
|
||||
initialPhoneNumber: React.PropTypes.string,
|
||||
initialPassword: React.PropTypes.string,
|
||||
onUsernameChanged: React.PropTypes.func,
|
||||
onPhoneCountryChanged: React.PropTypes.func,
|
||||
onPhoneNumberChanged: React.PropTypes.func,
|
||||
onPasswordChanged: React.PropTypes.func,
|
||||
loginIncorrect: React.PropTypes.bool,
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = PasswordLogin;
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { field_input_incorrect } from '../../../UiEffects';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
|
@ -40,25 +41,25 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail: React.PropTypes.string,
|
||||
defaultPhoneCountry: React.PropTypes.string,
|
||||
defaultPhoneNumber: React.PropTypes.string,
|
||||
defaultUsername: React.PropTypes.string,
|
||||
defaultPassword: React.PropTypes.string,
|
||||
teamsConfig: React.PropTypes.shape({
|
||||
defaultEmail: PropTypes.string,
|
||||
defaultPhoneCountry: PropTypes.string,
|
||||
defaultPhoneNumber: PropTypes.string,
|
||||
defaultUsername: PropTypes.string,
|
||||
defaultPassword: PropTypes.string,
|
||||
teamsConfig: PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string,
|
||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
supportEmail: PropTypes.string,
|
||||
teams: PropTypes.arrayOf(React.PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": React.PropTypes.string,
|
||||
"name": PropTypes.string,
|
||||
// The domain of team email addresses
|
||||
"domain": React.PropTypes.string,
|
||||
"domain": PropTypes.string,
|
||||
})).required,
|
||||
}),
|
||||
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
onError: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
minPasswordLength: PropTypes.number,
|
||||
onError: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -28,24 +29,24 @@ module.exports = React.createClass({
|
|||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
onServerConfigChange: React.PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: React.PropTypes.string, // e.g. https://matrix.org
|
||||
defaultIsUrl: React.PropTypes.string, // e.g. https://vector.im
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
|
||||
withToggleButton: React.PropTypes.bool,
|
||||
delayTimeMs: React.PropTypes.number, // time to wait before invoking onChanged
|
||||
withToggleButton: PropTypes.bool,
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import filesize from 'filesize';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
|
@ -81,7 +82,7 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
|||
// downloaded. This limit does not seem to apply when the url is used as
|
||||
// the source attribute of an image tag.
|
||||
//
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
|
||||
// for our purposes they inherit the origin of the page that created them.
|
||||
// This means that any scripts that run when the URL is viewed will be able
|
||||
// to access local storage.
|
||||
|
@ -191,7 +192,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
contextTypes: {
|
||||
appConfig: React.PropTypes.object,
|
||||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -294,7 +295,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<span className="mx_MFileBody" ref="body">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a href="javascript:void(0)" onClick={decrypt}>
|
||||
{ _t("Decrypt %(text)s", { text: text }) }
|
||||
</a>
|
||||
|
@ -326,7 +327,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div className="mx_MFileBody_download">
|
||||
<div style={{display: "none"}}>
|
||||
{ /*
|
||||
* Add dummy copy of the "a" tag
|
||||
|
@ -346,7 +347,7 @@ module.exports = React.createClass({
|
|||
if (this.props.tileShape === "file_grid") {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
|
||||
{ fileName }
|
||||
</a>
|
||||
|
@ -359,7 +360,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
|
|
|
@ -17,8 +17,10 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ImageUtils from '../../../ImageUtils';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
|
@ -28,28 +30,60 @@ import Promise from 'bluebird';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
export default class extends React.Component {
|
||||
displayName: 'MImageBody'
|
||||
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||
},
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.fixupHeight = this.fixupHeight.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
imgError: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
}
|
||||
|
||||
onClick: function onClick(ev) {
|
||||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected && this.state.imgError) {
|
||||
// Load the image again
|
||||
this.setState({
|
||||
imgError: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
if (ev.button == 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -69,43 +103,49 @@ module.exports = React.createClass({
|
|||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_isGif: function() {
|
||||
_isGif() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onImageEnter: function(e) {
|
||||
onImageEnter(e) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getContentUrl();
|
||||
},
|
||||
}
|
||||
|
||||
onImageLeave: function(e) {
|
||||
onImageLeave(e) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getThumbUrl();
|
||||
},
|
||||
}
|
||||
|
||||
_getContentUrl: function() {
|
||||
onImageError() {
|
||||
this.setState({
|
||||
imgError: true,
|
||||
});
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_getThumbUrl: function() {
|
||||
_getThumbUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||
|
@ -114,11 +154,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.fixupHeight();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -152,21 +192,35 @@ module.exports = React.createClass({
|
|||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
this._afterComponentDidMount();
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||
// initialisation after componentDidMount
|
||||
_afterComponentDidMount() {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
}
|
||||
|
||||
onAction: function(payload) {
|
||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||
// cleanup after componentWillUnmount
|
||||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "timeline_resize") {
|
||||
this.fixupHeight();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fixupHeight: function() {
|
||||
fixupHeight() {
|
||||
if (!this.refs.image) {
|
||||
console.warn("Refusing to fix up height on MImageBody with no image element");
|
||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -182,10 +236,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.refs.image.style.height = thumbHeight + "px";
|
||||
// console.log("Image height now", thumbHeight);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
|
@ -216,6 +285,14 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.imgError) {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
{ _t("This image cannot be displayed.") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
|
@ -225,18 +302,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (thumbUrl) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
return this._messageContent(contentUrl, thumbUrl, content);
|
||||
} else if (content.body) {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
|
@ -250,5 +316,5 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
149
src/components/views/messages/MStickerBody.js
Normal file
149
src/components/views/messages/MStickerBody.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
import TintableSVG from '../elements/TintableSvg';
|
||||
|
||||
export default class MStickerBody extends MImageBody {
|
||||
displayName: 'MStickerBody'
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onImageLoad = this._onImageLoad.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.setState({showTooltip: true});
|
||||
}
|
||||
|
||||
_onMouseLeave() {
|
||||
this.setState({showTooltip: false});
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.setState({
|
||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
||||
});
|
||||
const hidePlaceholderTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
}, 500);
|
||||
this.setState({hidePlaceholderTimer});
|
||||
if (this.props.onWidgetLoad) {
|
||||
this.props.onWidgetLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentDidMount() {
|
||||
if (this.refs.image.complete) {
|
||||
// Image already loaded
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
} else {
|
||||
// Image not already loaded
|
||||
this.setState({
|
||||
placeholderVisible: true,
|
||||
placeholderClasses: '',
|
||||
imageClasses: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentWillUnmount() {
|
||||
if (this.state.hidePlaceholderTimer) {
|
||||
clearTimeout(this.state.hidePlaceholderTimer);
|
||||
this.setState({hidePlaceholderTimer: null});
|
||||
}
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let tooltip;
|
||||
const tooltipBody = (
|
||||
this.props.mxEvent &&
|
||||
this.props.mxEvent.getContent() &&
|
||||
this.props.mxEvent.getContent().body) ?
|
||||
this.props.mxEvent.getContent().body : null;
|
||||
if (this.state.showTooltip && tooltipBody) {
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
tooltip = <RoomTooltip
|
||||
className='mx_RoleButton_tooltip'
|
||||
label={tooltipBody} />;
|
||||
}
|
||||
|
||||
const gutterSize = 0;
|
||||
let placeholderSize = 75;
|
||||
let placeholderFixupHeight = '100px';
|
||||
let placeholderTop = 0;
|
||||
let placeholderLeft = 0;
|
||||
|
||||
if (content.info) {
|
||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
||||
placeholderFixupHeight = content.info.h + 'px';
|
||||
}
|
||||
|
||||
placeholderSize = placeholderSize + 'px';
|
||||
|
||||
// Body 'ref' required by MImageBody
|
||||
return (
|
||||
<span className='mx_MStickerBody' ref='body'
|
||||
style={{
|
||||
height: placeholderFixupHeight,
|
||||
}}>
|
||||
<div className={'mx_MStickerBody_image_container'}>
|
||||
{ this.state.placeholderVisible &&
|
||||
<div
|
||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
||||
style={{
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
}}
|
||||
>
|
||||
<TintableSVG
|
||||
src={'img/icons-show-stickers.svg'}
|
||||
width={placeholderSize}
|
||||
height={placeholderSize} />
|
||||
</div> }
|
||||
<img
|
||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
||||
src={contentUrl}
|
||||
ref='image'
|
||||
alt={content.body}
|
||||
onLoad={this._onImageLoad}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
/>
|
||||
{ tooltip }
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty to prevent default behaviour of MImageBody
|
||||
onClick() {
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MFileBody from './MFileBody';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
|
@ -29,10 +30,10 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the video has loaded */
|
||||
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -24,22 +25,22 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* a list of words to highlight */
|
||||
highlights: React.PropTypes.array,
|
||||
highlights: PropTypes.array,
|
||||
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
highlightLink: PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* the shsape of the tile, used */
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
|
@ -64,15 +65,19 @@ module.exports = React.createClass({
|
|||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
}
|
||||
|
||||
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
return <BodyType
|
||||
ref="body" mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,7 +28,7 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onAvatarClick: function(name) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,6 +19,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import * as linkify from 'linkifyjs';
|
||||
|
@ -31,8 +33,6 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
|
||||
|
@ -43,19 +43,26 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* a list of words to highlight */
|
||||
highlights: React.PropTypes.array,
|
||||
highlights: PropTypes.array,
|
||||
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
highlightLink: PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
/* callback for when our widget has loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
addRichQuote: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -179,6 +186,7 @@ module.exports = React.createClass({
|
|||
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
const Quote = sdk.getComponent('elements.Quote');
|
||||
if (Pill.isMessagePillUrl(href)) {
|
||||
const pillContainer = document.createElement('span');
|
||||
|
||||
|
@ -197,6 +205,21 @@ module.exports = React.createClass({
|
|||
|
||||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
} else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
|
||||
if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
|
||||
this.context.addRichQuote(href);
|
||||
node.remove();
|
||||
} else { // We're the first in the chain
|
||||
const quoteContainer = document.createElement('span');
|
||||
|
||||
const quote =
|
||||
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
|
||||
|
||||
ReactDOM.render(quote, quoteContainer);
|
||||
node.parentNode.replaceChild(quoteContainer, node);
|
||||
node = quoteContainer;
|
||||
}
|
||||
pillified = true;
|
||||
}
|
||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
@ -314,7 +337,7 @@ module.exports = React.createClass({
|
|||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
|
||||
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = (e) => {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
import sdk from '../../../index';
|
||||
|
@ -26,7 +27,7 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import Promise from 'bluebird';
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ObjectUtils = require("../../../ObjectUtils");
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require("../../../index");
|
||||
|
@ -26,11 +27,11 @@ module.exports = React.createClass({
|
|||
displayName: 'AliasSettings',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: React.PropTypes.bool.isRequired,
|
||||
canSetAliases: React.PropTypes.bool.isRequired,
|
||||
aliasEvents: React.PropTypes.array, // [MatrixEvent]
|
||||
canonicalAliasEvent: React.PropTypes.object, // MatrixEvent
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||
canSetAliases: PropTypes.bool.isRequired,
|
||||
aliasEvents: PropTypes.array, // [MatrixEvent]
|
||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -57,8 +58,8 @@ module.exports = React.createClass({
|
|||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
|
||||
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((alias) => {
|
||||
return alias !== localDomain;
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
||||
});
|
||||
|
||||
if (canonicalAliasEvent) {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
import Promise from 'bluebird';
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const Tinter = require('../../../Tinter');
|
||||
|
@ -42,7 +43,7 @@ module.exports = React.createClass({
|
|||
displayName: 'ColorSettings',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -15,10 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+\:\S+/;
|
||||
|
||||
|
@ -26,13 +28,13 @@ module.exports = React.createClass({
|
|||
displayName: 'RelatedGroupSettings',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: React.PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -43,13 +45,25 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
newGroupsList: this.props.relatedGroupsEvent ?
|
||||
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||
newGroupsList: this.getInitialGroupList(),
|
||||
newGroupId: null,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialGroupList: function() {
|
||||
return this.props.relatedGroupsEvent ? (this.props.relatedGroupsEvent.getContent().groups || []) : [];
|
||||
},
|
||||
|
||||
needsSaving: function() {
|
||||
const cli = this.context.matrixClient;
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
if (!room.currentState.maySendStateEvent('m.room.related_groups', cli.getUserId())) return false;
|
||||
return !isEqual(this.getInitialGroupList(), this.state.newGroupsList);
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
if (!this.needsSaving()) return Promise.resolve();
|
||||
|
||||
return this.context.matrixClient.sendStateEvent(
|
||||
this.props.roomId,
|
||||
'm.room.related_groups',
|
||||
|
@ -97,7 +111,7 @@ module.exports = React.createClass({
|
|||
|
||||
onGroupDeleted: function(index) {
|
||||
const newGroupsList = this.state.newGroupsList.slice();
|
||||
newGroupsList.splice(index, 1),
|
||||
newGroupsList.splice(index, 1);
|
||||
this.setState({ newGroupsList });
|
||||
},
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require("../../../index");
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
@ -25,7 +26,7 @@ module.exports = React.createClass({
|
|||
displayName: 'UrlPreviewSettings',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -36,7 +37,15 @@ module.exports = React.createClass({
|
|||
displayName: 'AppsDrawer',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
showApps: PropTypes.bool, // Should apps be rendered
|
||||
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
showApps: true,
|
||||
hide: false,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -47,7 +56,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -57,7 +66,7 @@ module.exports = React.createClass({
|
|||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.log("Failed to connect to integrations server");
|
||||
console.log('Failed to connect to integrations server');
|
||||
// TODO -- Handle Scalar errors
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
|
@ -71,7 +80,7 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
ScalarMessaging.stopListening();
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
@ -82,7 +91,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAction: function(action) {
|
||||
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
|
@ -110,7 +119,7 @@ module.exports = React.createClass({
|
|||
* 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" }.
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
|
@ -191,13 +200,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}, 'mx_IntegrationsManager');
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
@ -205,12 +214,12 @@ module.exports = React.createClass({
|
|||
// Display a warning dialog if the max number of widgets have already been added to the room
|
||||
const apps = this._getApps();
|
||||
if (apps && apps.length >= MAX_WIDGETS) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
|
||||
console.error(errorMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Cannot add any more widgets"),
|
||||
description: _t("The maximum permitted number of widgets have already been added to this room."),
|
||||
title: _t('Cannot add any more widgets'),
|
||||
description: _t('The maximum permitted number of widgets have already been added to this room.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -242,11 +251,11 @@ module.exports = React.createClass({
|
|||
) {
|
||||
addWidget = <div
|
||||
onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
className={this.state.apps.length<2 ?
|
||||
"mx_AddWidget_button mx_AddWidget_button_full_width" :
|
||||
"mx_AddWidget_button"
|
||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||
'mx_AddWidget_button'
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] { _t('Add a widget') }
|
||||
|
@ -254,8 +263,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||
<div id='apps' className='mx_AppsContainer'>
|
||||
{ apps }
|
||||
</div>
|
||||
{ this._canUserModify() && addWidget }
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
|
@ -29,26 +30,32 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// js-sdk room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
showApps: React.PropTypes.bool,
|
||||
room: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
showApps: PropTypes.bool, // Render apps
|
||||
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: React.PropTypes.object,
|
||||
conferenceHandler: PropTypes.object,
|
||||
|
||||
// set to true to show the file drop target
|
||||
draggingFile: React.PropTypes.bool,
|
||||
draggingFile: PropTypes.bool,
|
||||
|
||||
// set to true to show the 'active conf call' banner
|
||||
displayConfCallNotification: React.PropTypes.bool,
|
||||
displayConfCallNotification: PropTypes.bool,
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: React.PropTypes.number,
|
||||
maxHeight: PropTypes.number,
|
||||
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
showApps: true,
|
||||
hideAppsDrawer: false,
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
|
@ -133,6 +140,7 @@ module.exports = React.createClass({
|
|||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
hide={this.props.hideAppsDrawer}
|
||||
/>;
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require('../../../index');
|
||||
|
@ -31,7 +33,11 @@ const PRESENCE_CLASS = {
|
|||
};
|
||||
|
||||
|
||||
function presenceClassForMember(presenceState, lastActiveAgo) {
|
||||
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
||||
if (showPresence === false) {
|
||||
return 'mx_EntityTile_online_beenactive';
|
||||
}
|
||||
|
||||
// offline is split into two categories depending on whether we have
|
||||
// a last_active_ago for them.
|
||||
if (presenceState == 'offline') {
|
||||
|
@ -51,18 +57,19 @@ const EntityTile = React.createClass({
|
|||
displayName: 'EntityTile',
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string,
|
||||
title: React.PropTypes.string,
|
||||
avatarJsx: React.PropTypes.any, // <BaseAvatar />
|
||||
className: React.PropTypes.string,
|
||||
presenceState: React.PropTypes.string,
|
||||
presenceLastActiveAgo: React.PropTypes.number,
|
||||
presenceLastTs: React.PropTypes.number,
|
||||
presenceCurrentlyActive: React.PropTypes.bool,
|
||||
showInviteButton: React.PropTypes.bool,
|
||||
shouldComponentUpdate: React.PropTypes.func,
|
||||
onClick: React.PropTypes.func,
|
||||
suppressOnHover: React.PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
avatarJsx: PropTypes.any, // <BaseAvatar />
|
||||
className: PropTypes.string,
|
||||
presenceState: PropTypes.string,
|
||||
presenceLastActiveAgo: PropTypes.number,
|
||||
presenceLastTs: PropTypes.number,
|
||||
presenceCurrentlyActive: PropTypes.bool,
|
||||
showInviteButton: PropTypes.bool,
|
||||
shouldComponentUpdate: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
suppressOnHover: PropTypes.bool,
|
||||
showPresence: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -74,6 +81,7 @@ const EntityTile = React.createClass({
|
|||
presenceLastTs: 0,
|
||||
showInviteButton: false,
|
||||
suppressOnHover: false,
|
||||
showPresence: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -98,7 +106,7 @@ const EntityTile = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const presenceClass = presenceClassForMember(
|
||||
this.props.presenceState, this.props.presenceLastActiveAgo,
|
||||
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
|
||||
);
|
||||
|
||||
let mainClassName = "mx_EntityTile ";
|
||||
|
@ -113,15 +121,21 @@ const EntityTile = React.createClass({
|
|||
|
||||
mainClassName += " mx_EntityTile_hover";
|
||||
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
let presenceLabel = null;
|
||||
let nameClasses = 'mx_EntityTile_name';
|
||||
if (this.props.showPresence) {
|
||||
presenceLabel = <PresenceLabel activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />;
|
||||
nameClasses += ' mx_EntityTile_name_hover';
|
||||
}
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12" />
|
||||
<EmojiText element="div" className="mx_EntityTile_name mx_EntityTile_name_hover" dir="auto">
|
||||
<EmojiText element="div" className={nameClasses} dir="auto">
|
||||
{ name }
|
||||
</EmojiText>
|
||||
<PresenceLabel activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,6 +19,7 @@ limitations under the License.
|
|||
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const classNames = require("classnames");
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
const Modal = require('../../../Modal');
|
||||
|
@ -28,11 +30,13 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
|
|||
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.sticker': 'messages.MessageEvent',
|
||||
'm.call.invite': 'messages.TextualEvent',
|
||||
'm.call.answer': 'messages.TextualEvent',
|
||||
'm.call.hangup': 'messages.TextualEvent',
|
||||
|
@ -75,65 +79,65 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
propTypes: {
|
||||
/* MatrixClient instance for sender verification etc */
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
|
||||
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
|
||||
* references the same this.props.mxEvent.
|
||||
*/
|
||||
isRedacted: React.PropTypes.bool,
|
||||
isRedacted: PropTypes.bool,
|
||||
|
||||
/* true if this is a continuation of the previous event (which has the
|
||||
* effect of not showing another avatar/displayname
|
||||
*/
|
||||
continuation: React.PropTypes.bool,
|
||||
continuation: PropTypes.bool,
|
||||
|
||||
/* true if this is the last event in the timeline (which has the effect
|
||||
* of always showing the timestamp)
|
||||
*/
|
||||
last: React.PropTypes.bool,
|
||||
last: PropTypes.bool,
|
||||
|
||||
/* true if this is search context (which has the effect of greying out
|
||||
* the text
|
||||
*/
|
||||
contextual: React.PropTypes.bool,
|
||||
contextual: PropTypes.bool,
|
||||
|
||||
/* a list of words to highlight, ordered by longest first */
|
||||
highlights: React.PropTypes.array,
|
||||
highlights: PropTypes.array,
|
||||
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
highlightLink: PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
/* is this the focused event */
|
||||
isSelectedEvent: React.PropTypes.bool,
|
||||
isSelectedEvent: PropTypes.bool,
|
||||
|
||||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
|
||||
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
readReceipts: PropTypes.arrayOf(React.PropTypes.object),
|
||||
|
||||
/* opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
* to manage its animations. Should be an empty object when the room
|
||||
* first loads
|
||||
*/
|
||||
readReceiptMap: React.PropTypes.object,
|
||||
readReceiptMap: PropTypes.object,
|
||||
|
||||
/* A function which is used to check if the parent panel is being
|
||||
* unmounted, to avoid unnecessary work. Should return true if we
|
||||
* are being unmounted.
|
||||
*/
|
||||
checkUnmounting: React.PropTypes.func,
|
||||
checkUnmounting: PropTypes.func,
|
||||
|
||||
/* the status of this event - ie, mxEvent.status. Denormalised to here so
|
||||
* that we can tell when it changes. */
|
||||
eventSendStatus: React.PropTypes.string,
|
||||
eventSendStatus: PropTypes.string,
|
||||
|
||||
/* the shape of the tile. by default, the layout is intended for the
|
||||
* normal room timeline. alternative values are: "file_list", "file_grid"
|
||||
|
@ -142,14 +146,24 @@ module.exports = withMatrixClient(React.createClass({
|
|||
* boiilerplatey. So just make the necessary render decisions conditional
|
||||
* for now.
|
||||
*/
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: React.PropTypes.bool,
|
||||
isTwelveHour: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {menu: false, allReadAvatars: false, verified: null};
|
||||
return {
|
||||
// Whether the context menu is being displayed.
|
||||
menu: false,
|
||||
// Whether all read receipts are being displayed. If not, only display
|
||||
// a truncation of them.
|
||||
allReadAvatars: false,
|
||||
// Whether the event's sender has been verified.
|
||||
verified: null,
|
||||
// Whether onRequestKeysClick has been called since mounting.
|
||||
previouslyRequestedKeys: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -390,6 +404,19 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onRequestKeysClick: function() {
|
||||
this.setState({
|
||||
// Indicate in the UI that the keys have been requested (this is expected to
|
||||
// be reset if the component is mounted in the future).
|
||||
previouslyRequestedKeys: true,
|
||||
});
|
||||
|
||||
// Cancel any outgoing key request for this event and resend it. If a response
|
||||
// is received for the request with the required keys, the event could be
|
||||
// decrypted successfully.
|
||||
this.props.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
},
|
||||
|
||||
onPermalinkClicked: function(e) {
|
||||
// This allows the permalink to be opened in a new tab/window or copied as
|
||||
// matrix.to, but also for it to enable routing within Riot when clicked.
|
||||
|
@ -444,7 +471,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const eventType = this.props.mxEvent.getType();
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isInfoMessage = (eventType !== 'm.room.message');
|
||||
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
|
||||
|
||||
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
|
@ -455,6 +482,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
|
@ -471,14 +499,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
menu: this.state.menu,
|
||||
mx_EventTile_verified: this.state.verified == true,
|
||||
mx_EventTile_unverified: this.state.verified == false,
|
||||
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
const permalink = "https://matrix.to/#/" +
|
||||
this.props.mxEvent.getRoomId() + "/" +
|
||||
this.props.mxEvent.getId();
|
||||
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
|
||||
|
||||
const readAvatars = this.getReadAvatars();
|
||||
|
||||
|
@ -516,7 +542,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
if (needsSenderProfile) {
|
||||
let text = null;
|
||||
if (!this.props.tileShape) {
|
||||
if (!this.props.tileShape || this.props.tileShape === 'quote') {
|
||||
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
|
||||
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
|
||||
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
||||
|
@ -533,79 +559,139 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
if (this.props.tileShape === "notif") {
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line" >
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
</div>
|
||||
const keyRequestHelpText =
|
||||
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
|
||||
<p>
|
||||
{ this.state.previouslyRequestedKeys ?
|
||||
_t( 'Your key share request has been sent - please check your other devices ' +
|
||||
'for key share requests.') :
|
||||
_t( 'Key share requests are sent to your other devices automatically. If you ' +
|
||||
'rejected or dismissed the key share request on your other devices, click ' +
|
||||
'here to request the keys for this session again.')
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{ _t( 'If your other devices do not have the key for this message you will not ' +
|
||||
'be able to decrypt them.')
|
||||
}
|
||||
</p>
|
||||
</div>;
|
||||
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
|
||||
_t('Key request sent.') :
|
||||
_t(
|
||||
'<requestLink>Re-request encryption keys</requestLink> from your other devices.',
|
||||
{},
|
||||
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
||||
);
|
||||
} else if (this.props.tileShape === "file_grid") {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_line" >
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
|
||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||
const keyRequestInfo = isEncryptionFailure ?
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">
|
||||
{ keyRequestInfoContent }
|
||||
</span>
|
||||
<ToolTipButton helpText={keyRequestHelpText} />
|
||||
</div> : null;
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line" >
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
);
|
||||
}
|
||||
case 'file_grid': {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_line" >
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
{ editButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
case 'quote': {
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line mx_EventTile_quote">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
<EventTileType ref="tile"
|
||||
tileShape="quote"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onWidgetLoad={this.props.onWidgetLoad}
|
||||
showUrlPreview={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
{ keyRequestInfo }
|
||||
{ editButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@ -658,3 +744,5 @@ function E2ePadlockUnencrypted(props) {
|
|||
function E2ePadlock(props) {
|
||||
return <img className="mx_EventTile_e2eIcon" {...props} />;
|
||||
}
|
||||
|
||||
module.exports.getHandlerTile = getHandlerTile;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
|
@ -25,7 +26,7 @@ module.exports = React.createClass({
|
|||
displayName: 'ForwardMessage',
|
||||
|
||||
propTypes: {
|
||||
onCancelClick: React.PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
@ -32,10 +33,10 @@ module.exports = React.createClass({
|
|||
displayName: 'LinkPreviewWidget',
|
||||
|
||||
propTypes: {
|
||||
link: React.PropTypes.string.isRequired, // the URL being previewed
|
||||
mxEvent: React.PropTypes.object.isRequired, // the Event associated with the preview
|
||||
onCancelClick: React.PropTypes.func, // called when the preview's cancel ('hide') button is clicked
|
||||
onWidgetLoad: React.PropTypes.func, // called when the preview's contents has loaded
|
||||
link: PropTypes.string.isRequired, // the URL being previewed
|
||||
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
|
||||
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
|
||||
onWidgetLoad: PropTypes.func, // called when the preview's contents has loaded
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue