Merge branch 'experimental' into bwindels/roomgridview-experimental
This commit is contained in:
commit
290dc9d8fb
146 changed files with 5292 additions and 1051 deletions
|
@ -69,6 +69,7 @@ export default class AutoHideScrollbar extends React.Component {
|
|||
this.onOverflow = this.onOverflow.bind(this);
|
||||
this.onUnderflow = this.onUnderflow.bind(this);
|
||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
||||
this._needsOverflowListener = null;
|
||||
}
|
||||
|
||||
onOverflow() {
|
||||
|
@ -81,21 +82,35 @@ export default class AutoHideScrollbar extends React.Component {
|
|||
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
if (!this._needsOverflowListener) {
|
||||
return;
|
||||
}
|
||||
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
|
||||
this.onOverflow();
|
||||
} else {
|
||||
this.onUnderflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
installBodyClassesIfNeeded();
|
||||
this._needsOverflowListener =
|
||||
document.body.classList.contains("mx_scrollbar_nooverlay");
|
||||
if (this._needsOverflowListener) {
|
||||
this.containerRef.addEventListener("overflow", this.onOverflow);
|
||||
this.containerRef.addEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
_collectContainerRef(ref) {
|
||||
if (ref && !this.containerRef) {
|
||||
this.containerRef = ref;
|
||||
const needsOverflowListener =
|
||||
document.body.classList.contains("mx_scrollbar_nooverlay");
|
||||
|
||||
if (needsOverflowListener) {
|
||||
this.containerRef.addEventListener("overflow", this.onOverflow);
|
||||
this.containerRef.addEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
if (ref.scrollHeight > ref.clientHeight) {
|
||||
this.onOverflow();
|
||||
} else {
|
||||
this.onUnderflow();
|
||||
}
|
||||
}
|
||||
if (this.props.wrappedRef) {
|
||||
this.props.wrappedRef(ref);
|
||||
|
@ -103,14 +118,13 @@ export default class AutoHideScrollbar extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.containerRef) {
|
||||
if (this._needsOverflowListener && this.containerRef) {
|
||||
this.containerRef.removeEventListener("overflow", this.onOverflow);
|
||||
this.containerRef.removeEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
installBodyClassesIfNeeded();
|
||||
return (<div
|
||||
ref={this._collectContainerRef}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
|
|
|
@ -473,7 +473,7 @@ export default React.createClass({
|
|||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
|
||||
let willDoOnboarding = false;
|
||||
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
|
||||
GroupStore.on('error', (err, errorGroupId) => {
|
||||
GroupStore.on('error', (err, errorGroupId, stateKey) => {
|
||||
if (this._unmounted || groupId !== errorGroupId) return;
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
|
||||
dis.dispatch({
|
||||
|
@ -486,11 +486,13 @@ export default React.createClass({
|
|||
dis.dispatch({action: 'require_registration'});
|
||||
willDoOnboarding = true;
|
||||
}
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: err,
|
||||
editing: false,
|
||||
});
|
||||
if (stateKey === GroupStore.STATE_KEY.Summary) {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: err,
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -514,7 +516,6 @@ export default React.createClass({
|
|||
isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
|
||||
(m) => m.userId === this._matrixClient.credentials.userId,
|
||||
),
|
||||
error: null,
|
||||
});
|
||||
// XXX: This might not work but this.props.groupIsNew unused anyway
|
||||
if (this.props.groupIsNew && firstInit) {
|
||||
|
@ -1079,6 +1080,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_getJoinableNode: function() {
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
return this.state.editing ? <div>
|
||||
<h3>
|
||||
{ _t('Who can join this community?') }
|
||||
|
@ -1160,7 +1162,7 @@ export default React.createClass({
|
|||
|
||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.summary) {
|
||||
} else if (this.state.summary && !this.state.error) {
|
||||
const summary = this.state.summary;
|
||||
|
||||
let avatarNode;
|
||||
|
@ -1272,15 +1274,6 @@ export default React.createClass({
|
|||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
if (this.props.collapsedRhs) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button"
|
||||
onClick={this._onShowRhsClick} title={_t('Show panel')} key="_maximiseButton"
|
||||
>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
|
||||
|
@ -1311,7 +1304,7 @@ export default React.createClass({
|
|||
<div className="mx_GroupView_header_rightCol">
|
||||
{ rightButtons }
|
||||
</div>
|
||||
<GroupHeaderButtons />
|
||||
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
||||
</div>
|
||||
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
|
|
|
@ -91,11 +91,15 @@ class HomePage extends React.Component {
|
|||
this._unmounted = true;
|
||||
}
|
||||
|
||||
onLoginClick() {
|
||||
onLoginClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
}
|
||||
|
||||
onRegisterClick() {
|
||||
onRegisterClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
}
|
||||
|
||||
|
|
|
@ -21,41 +21,52 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this._collectScroller = this._collectScroller.bind(this);
|
||||
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
|
||||
this.checkOverflow = this.checkOverflow.bind(this);
|
||||
this._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
}
|
||||
|
||||
_collectScroller(scroller) {
|
||||
if (scroller && !this._scroller) {
|
||||
this._scroller = scroller;
|
||||
this._scroller.addEventListener("scroll", this.checkOverflow);
|
||||
if (scroller && !this._scrollElement) {
|
||||
this._scrollElement = scroller;
|
||||
this._scrollElement.addEventListener("scroll", this.checkOverflow);
|
||||
this.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
_collectScrollerComponent(autoHideScrollbar) {
|
||||
this._autoHideScrollbar = autoHideScrollbar;
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
const hasTopOverflow = this._scroller.scrollTop > 0;
|
||||
const hasBottomOverflow = this._scroller.scrollHeight >
|
||||
(this._scroller.scrollTop + this._scroller.clientHeight);
|
||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
||||
if (hasTopOverflow) {
|
||||
this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||
} else {
|
||||
this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||
}
|
||||
if (hasBottomOverflow) {
|
||||
this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||
} else {
|
||||
this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||
}
|
||||
|
||||
if (this._autoHideScrollbar) {
|
||||
this._autoHideScrollbar.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._scroller) {
|
||||
this._scroller.removeEventListener("scroll", this.checkOverflow);
|
||||
if (this._scrollElement) {
|
||||
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<AutoHideScrollbar wrappedRef={this._collectScroller} {... this.props}>
|
||||
return (<AutoHideScrollbar ref={this._collectScrollerComponent} wrappedRef={this._collectScroller} {... this.props}>
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>);
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ const LeftPanel = React.createClass({
|
|||
}
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_SearchBox_search")));
|
||||
classes.contains("mx_textinput_search")));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
|
|
|
@ -63,7 +63,7 @@ const LoggedInView = React.createClass({
|
|||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
collapsedRhs: PropTypes.bool,
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// Used by the RoomView to handle joining rooms
|
||||
|
@ -447,7 +447,7 @@ const LoggedInView = React.createClass({
|
|||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
/>;
|
||||
break;
|
||||
|
@ -499,7 +499,7 @@ const LoggedInView = React.createClass({
|
|||
page_element = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
isNew={this.props.currentGroupIsNew}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -41,10 +41,13 @@ export default class MainSplit extends React.Component {
|
|||
{onResized: this._onResized},
|
||||
);
|
||||
resizer.setClassNames(classNames);
|
||||
const rhsSize = window.localStorage.getItem("mx_rhs_size");
|
||||
let rhsSize = window.localStorage.getItem("mx_rhs_size");
|
||||
if (rhsSize !== null) {
|
||||
resizer.forHandleAt(0).resize(parseInt(rhsSize, 10));
|
||||
rhsSize = parseInt(rhsSize, 10);
|
||||
} else {
|
||||
rhsSize = 350;
|
||||
}
|
||||
resizer.forHandleAt(0).resize(rhsSize);
|
||||
|
||||
resizer.attach();
|
||||
this.resizer = resizer;
|
||||
|
|
|
@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
|||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
|
||||
const AutoDiscovery = Matrix.AutoDiscovery;
|
||||
|
||||
// Disable warnings for now: we use deprecated bluebird functions
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
Promise.config({warnings: false});
|
||||
|
@ -161,7 +163,7 @@ export default React.createClass({
|
|||
viewUserId: null,
|
||||
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
|
||||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
rightDisabled: false,
|
||||
|
@ -181,6 +183,12 @@ export default React.createClass({
|
|||
register_is_url: null,
|
||||
register_id_sid: null,
|
||||
|
||||
// Parameters used for setting up the login/registration views
|
||||
defaultServerName: this.props.config.default_server_name,
|
||||
defaultHsUrl: this.props.config.default_hs_url,
|
||||
defaultIsUrl: this.props.config.default_is_url,
|
||||
defaultServerDiscoveryError: 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,
|
||||
|
@ -199,20 +207,24 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
getDefaultServerName: function() {
|
||||
return this.state.defaultServerName;
|
||||
},
|
||||
|
||||
getCurrentHsUrl: function() {
|
||||
if (this.state.register_hs_url) {
|
||||
return this.state.register_hs_url;
|
||||
} else if (MatrixClientPeg.get()) {
|
||||
return MatrixClientPeg.get().getHomeserverUrl();
|
||||
} else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
|
||||
return window.localStorage.getItem("mx_hs_url");
|
||||
} else {
|
||||
return this.getDefaultHsUrl();
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultHsUrl() {
|
||||
return this.props.config.default_hs_url || "https://matrix.org";
|
||||
getDefaultHsUrl(defaultToMatrixDotOrg) {
|
||||
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
|
||||
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
|
||||
return this.state.defaultHsUrl;
|
||||
},
|
||||
|
||||
getFallbackHsUrl: function() {
|
||||
|
@ -224,15 +236,13 @@ export default React.createClass({
|
|||
return this.state.register_is_url;
|
||||
} else if (MatrixClientPeg.get()) {
|
||||
return MatrixClientPeg.get().getIdentityServerUrl();
|
||||
} else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
|
||||
return window.localStorage.getItem("mx_is_url");
|
||||
} else {
|
||||
return this.getDefaultIsUrl();
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultIsUrl() {
|
||||
return this.props.config.default_is_url || "https://vector.im";
|
||||
return this.state.defaultIsUrl || "https://vector.im";
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -282,6 +292,20 @@ export default React.createClass({
|
|||
console.info(`Team token set to ${this._teamToken}`);
|
||||
}
|
||||
|
||||
// Set up the default URLs (async)
|
||||
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
|
||||
this.setState({loadingDefaultHomeserver: true});
|
||||
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
|
||||
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
|
||||
// Ideally we would somehow only communicate this to the server admins, but
|
||||
// given this is at login time we can't really do much besides hope that people
|
||||
// will check their settings.
|
||||
this.setState({
|
||||
defaultServerName: null, // To un-hide any secrets people might be keeping
|
||||
defaultServerDiscoveryError: _t("Invalid configuration: Cannot supply a default homeserver URL and a default server name"),
|
||||
});
|
||||
}
|
||||
|
||||
// Set a default HS with query param `hs_url`
|
||||
const paramHs = this.props.startingFragmentQueryParams.hs_url;
|
||||
if (paramHs) {
|
||||
|
@ -555,7 +579,7 @@ export default React.createClass({
|
|||
break;
|
||||
case 'view_user':
|
||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||
if (this.state.collapseRhs) {
|
||||
if (this.state.collapsedRhs) {
|
||||
setTimeout(()=>{
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
|
@ -659,13 +683,15 @@ export default React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", true);
|
||||
this.setState({
|
||||
collapseRhs: true,
|
||||
collapsedRhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", false);
|
||||
this.setState({
|
||||
collapseRhs: false,
|
||||
collapsedRhs: false,
|
||||
});
|
||||
break;
|
||||
case 'panel_disable': {
|
||||
|
@ -676,9 +702,11 @@ export default React.createClass({
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
// case 'set_theme':
|
||||
// disable changing the theme for now
|
||||
// as other themes are not compatible with dharma
|
||||
// this._onSetTheme(payload.value);
|
||||
// break;
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
|
@ -911,6 +939,10 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_viewHome: function() {
|
||||
// The home page requires the "logged in" view, so we'll set that.
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGGED_IN,
|
||||
});
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
},
|
||||
|
@ -1167,10 +1199,7 @@ export default React.createClass({
|
|||
* @param {string} teamToken
|
||||
*/
|
||||
_onLoggedIn: async function(teamToken) {
|
||||
this.setState({
|
||||
view: VIEWS.LOGGED_IN,
|
||||
});
|
||||
|
||||
this.setStateForNewView({view: VIEWS.LOGGED_IN});
|
||||
if (teamToken) {
|
||||
// A team member has logged in, not a guest
|
||||
this._teamToken = teamToken;
|
||||
|
@ -1227,7 +1256,7 @@ export default React.createClass({
|
|||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
collapsedRhs: false,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
|
@ -1418,6 +1447,11 @@ export default React.createClass({
|
|||
break;
|
||||
}
|
||||
});
|
||||
cli.on("crypto.keyBackupFailed", () => {
|
||||
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
|
||||
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
|
||||
);
|
||||
});
|
||||
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
|
@ -1744,6 +1778,36 @@ export default React.createClass({
|
|||
this.setState(newState);
|
||||
},
|
||||
|
||||
_tryDiscoverDefaultHomeserver: async function(serverName) {
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS) {
|
||||
console.error("Failed to discover homeserver on startup:", discovery);
|
||||
this.setState({
|
||||
defaultServerDiscoveryError: discovery["m.homeserver"].error,
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
} else {
|
||||
const hsUrl = discovery["m.homeserver"].base_url;
|
||||
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "https://vector.im";
|
||||
this.setState({
|
||||
defaultHsUrl: hsUrl,
|
||||
defaultIsUrl: isUrl,
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
|
||||
loadingDefaultHomeserver: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
if (this.props.startingFragmentQueryParams.referrer) {
|
||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||
|
@ -1758,7 +1822,7 @@ export default React.createClass({
|
|||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
|
||||
if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN || this.state.loadingDefaultHomeserver) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
|
@ -1832,6 +1896,8 @@ export default React.createClass({
|
|||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
brand={this.props.config.brand}
|
||||
|
@ -1854,6 +1920,8 @@ export default React.createClass({
|
|||
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
return (
|
||||
<ForgotPassword
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
|
@ -1870,6 +1938,8 @@ export default React.createClass({
|
|||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
|
|
|
@ -110,8 +110,9 @@ const RoomSubList = React.createClass({
|
|||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden});
|
||||
this.props.onHeaderClick(isHidden);
|
||||
this.setState({hidden: isHidden}, () => {
|
||||
this.props.onHeaderClick(isHidden);
|
||||
});
|
||||
} else {
|
||||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
||||
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
|
||||
|
@ -268,17 +269,10 @@ const RoomSubList = React.createClass({
|
|||
|
||||
let incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
const self = this;
|
||||
// Check if the incoming call is for this section
|
||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
// We can assume that if we have an incoming call then it is for this list
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
|
||||
let addRoomButton;
|
||||
|
@ -313,6 +307,12 @@ const RoomSubList = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
checkOverflow: function() {
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.checkOverflow();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
if (len) {
|
||||
|
@ -330,7 +330,7 @@ const RoomSubList = React.createClass({
|
|||
tiles.push(...this.props.extraTiles);
|
||||
return <div className={subListClasses}>
|
||||
{this._getHeaderJsx()}
|
||||
<IndicatorScrollbar className="mx_RoomSubList_scroll">
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
|
|
|
@ -27,6 +27,7 @@ const React = require("react");
|
|||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import filesize from 'filesize';
|
||||
const classNames = require("classnames");
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
|
@ -103,6 +104,10 @@ module.exports = React.createClass({
|
|||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
|
||||
// Media limits for uploading.
|
||||
mediaConfig: undefined,
|
||||
|
||||
// used to trigger a rerender in TimelinePanel once the members are loaded,
|
||||
// so RR are rendered again (now with the members available), ...
|
||||
membersLoaded: !llMembers,
|
||||
|
@ -158,7 +163,8 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
this._fetchMediaConfig();
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -166,6 +172,27 @@ module.exports = React.createClass({
|
|||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
},
|
||||
|
||||
_fetchMediaConfig: function(invalidateCache: boolean = false) {
|
||||
/// NOTE: Using global here so we don't make repeated requests for the
|
||||
/// config every time we swap room.
|
||||
if(global.mediaConfig !== undefined && !invalidateCache) {
|
||||
this.setState({mediaConfig: global.mediaConfig});
|
||||
return;
|
||||
}
|
||||
console.log("[Media Config] Fetching");
|
||||
MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
global.mediaConfig = config;
|
||||
this.setState({mediaConfig: config});
|
||||
});
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
|
@ -424,6 +451,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -500,6 +528,10 @@ module.exports = React.createClass({
|
|||
break;
|
||||
case 'notifier_enabled':
|
||||
case 'upload_failed':
|
||||
// 413: File was too big or upset the server in some way.
|
||||
if(payload.error.http_status === 413) {
|
||||
this._fetchMediaConfig(true);
|
||||
}
|
||||
case 'upload_started':
|
||||
case 'upload_finished':
|
||||
this.forceUpdate();
|
||||
|
@ -578,6 +610,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
async onRoomRecoveryReminderFinished(backupCreated) {
|
||||
// If the user cancelled the key backup dialog, it suggests they don't
|
||||
// want to be reminded anymore.
|
||||
if (!backupCreated) {
|
||||
await SettingsStore.setValue(
|
||||
"showRoomRecoveryReminder",
|
||||
null,
|
||||
SettingLevel.ACCOUNT,
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onKeyBackupStatus() {
|
||||
// Key backup status changes affect whether the in-room recovery
|
||||
// reminder is displayed.
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
return true;
|
||||
|
@ -932,6 +983,15 @@ module.exports = React.createClass({
|
|||
this.setState({ draggingFile: false });
|
||||
},
|
||||
|
||||
isFileUploadAllowed(file) {
|
||||
if (this.state.mediaConfig !== undefined &&
|
||||
this.state.mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this.state.mediaConfig["m.upload.size"]) {
|
||||
return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
uploadFile: async function(file) {
|
||||
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
|
||||
|
||||
|
@ -1483,6 +1543,7 @@ module.exports = React.createClass({
|
|||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.state.roomLoading || this.state.peekLoading) {
|
||||
|
@ -1622,6 +1683,13 @@ module.exports = React.createClass({
|
|||
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
||||
);
|
||||
|
||||
const showRoomRecoveryReminder = (
|
||||
SettingsStore.isFeatureEnabled("feature_keybackup") &&
|
||||
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
|
||||
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||
);
|
||||
|
||||
let aux = null;
|
||||
let hideCancel = false;
|
||||
if (this.state.editingRoomSettings) {
|
||||
|
@ -1636,6 +1704,9 @@ module.exports = React.createClass({
|
|||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||
hideCancel = true;
|
||||
} else if (showRoomRecoveryReminder) {
|
||||
aux = <RoomRecoveryReminder onFinished={this.onRoomRecoveryReminderFinished} />;
|
||||
hideCancel = true;
|
||||
} else if (this.state.showingPinned) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||
|
@ -1696,6 +1767,7 @@ module.exports = React.createClass({
|
|||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
uploadAllowed={this.isFileUploadAllowed}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ module.exports = React.createClass({
|
|||
case 'focus_room_filter':
|
||||
if (this.refs.search) {
|
||||
this.refs.search.focus();
|
||||
this.refs.search.select();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -83,6 +82,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onFocus: function(ev) {
|
||||
ev.target.select();
|
||||
},
|
||||
|
||||
_clearSearch: function(source) {
|
||||
this.refs.search.value = "";
|
||||
this.onChange();
|
||||
|
@ -108,6 +111,7 @@ module.exports = React.createClass({
|
|||
ref="search"
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
|
|
|
@ -23,6 +23,7 @@ import GroupActions from '../../actions/GroupActions';
|
|||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
@ -47,6 +48,8 @@ const TagPanel = React.createClass({
|
|||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
|
@ -67,6 +70,9 @@ const TagPanel = React.createClass({
|
|||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership() {
|
||||
|
@ -100,13 +106,21 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createDialog(RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
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 GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const ActionButton = sdk.getComponent("elements.ActionButton");
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
|
@ -162,7 +176,10 @@ const TagPanel = React.createClass({
|
|||
</GeminiScrollbarWrapper>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_groupsButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
<GroupsButton />
|
||||
<ActionButton
|
||||
className="mx_TagPanel_report" action="show_redesign_feedback_dialog"
|
||||
label={_t("Report bugs & give feedback")} tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "urlPreviewsEnabled" },
|
||||
{ id: "autoplayGifsAndVideos" },
|
||||
{ id: "alwaysShowEncryptionIcons" },
|
||||
{ id: "showRoomRecoveryReminder" },
|
||||
{ id: "hideReadReceipts" },
|
||||
{ id: "dontSendTypingNotifications" },
|
||||
{ id: "alwaysShowTimestamps" },
|
||||
|
@ -188,9 +189,11 @@ module.exports = React.createClass({
|
|||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||
email_add_pending: false,
|
||||
vectorVersion: undefined,
|
||||
canSelfUpdate: null,
|
||||
rejectingInvites: false,
|
||||
mediaDevices: null,
|
||||
ignoredUsers: [],
|
||||
autoLaunchEnabled: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -209,6 +212,13 @@ module.exports = React.createClass({
|
|||
}, (e) => {
|
||||
console.log("Failed to fetch app version", e);
|
||||
});
|
||||
|
||||
PlatformPeg.get().canSelfUpdate().then((canUpdate) => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
canSelfUpdate: canUpdate,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._refreshMediaDevices();
|
||||
|
@ -227,11 +237,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
this._refreshFromServer();
|
||||
|
||||
if (PlatformPeg.get().isElectron()) {
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
ipcRenderer.on('settings', this._electronSettings);
|
||||
ipcRenderer.send('settings_get');
|
||||
if (PlatformPeg.get().supportsAutoLaunch()) {
|
||||
PlatformPeg.get().getAutoLaunchEnabled().then(enabled => {
|
||||
this.setState({
|
||||
autoLaunchEnabled: enabled,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -262,11 +273,6 @@ module.exports = React.createClass({
|
|||
if (cli) {
|
||||
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
||||
}
|
||||
|
||||
if (PlatformPeg.get().isElectron()) {
|
||||
const {ipcRenderer} = require('electron');
|
||||
ipcRenderer.removeListener('settings', this._electronSettings);
|
||||
}
|
||||
},
|
||||
|
||||
// `UserSettings` assumes that the client peg will not be null, so give it some
|
||||
|
@ -285,10 +291,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_electronSettings: function(ev, settings) {
|
||||
this.setState({ electron_settings: settings });
|
||||
},
|
||||
|
||||
_refreshMediaDevices: function(stream) {
|
||||
if (stream) {
|
||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||
|
@ -943,7 +945,7 @@ module.exports = React.createClass({
|
|||
|
||||
_renderCheckUpdate: function() {
|
||||
const platform = PlatformPeg.get();
|
||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||
if (this.state.canSelfUpdate) {
|
||||
return <div>
|
||||
<h3>{ _t('Updates') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -988,8 +990,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderElectronSettings: function() {
|
||||
const settings = this.state.electron_settings;
|
||||
if (!settings) return;
|
||||
if (!PlatformPeg.get().supportsAutoLaunch()) return;
|
||||
|
||||
// TODO: This should probably be a granular setting, but it only applies to electron
|
||||
// and ends up being get/set outside of matrix anyways (local system setting).
|
||||
|
@ -999,7 +1000,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_toggle">
|
||||
<input type="checkbox"
|
||||
name="auto-launch"
|
||||
defaultChecked={settings['auto-launch']}
|
||||
defaultChecked={this.state.autoLaunchEnabled}
|
||||
onChange={this._onAutoLaunchChanged}
|
||||
/>
|
||||
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
|
||||
|
@ -1009,8 +1010,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onAutoLaunchChanged: function(e) {
|
||||
const {ipcRenderer} = require('electron');
|
||||
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
|
||||
PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => {
|
||||
this.setState({
|
||||
autoLaunchEnabled: e.target.checked,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_mapWebRtcDevicesToSpans: function(devices) {
|
||||
|
@ -1369,7 +1373,7 @@ module.exports = React.createClass({
|
|||
{ this._renderBulkOptions() }
|
||||
{ this._renderBugReport() }
|
||||
|
||||
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||
{ this._renderElectronSettings() }
|
||||
|
||||
{ this._renderAnalyticsControl() }
|
||||
|
||||
|
|
|
@ -36,6 +36,14 @@ module.exports = React.createClass({
|
|||
onLoginClick: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. This is used when displaying the defaultHsUrl in the UI.
|
||||
defaultServerName: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -45,6 +53,7 @@ module.exports = React.createClass({
|
|||
progress: null,
|
||||
password: null,
|
||||
password2: null,
|
||||
errorText: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -81,6 +90,13 @@ module.exports = React.createClass({
|
|||
onSubmitForm: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
|
@ -146,6 +162,18 @@ module.exports = React.createClass({
|
|||
this.setState(newState);
|
||||
},
|
||||
|
||||
onLoginClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onLoginClick();
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||
|
@ -200,6 +228,12 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let errorText = null;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
resetPasswordJsx = (
|
||||
|
@ -230,10 +264,11 @@ module.exports = React.createClass({
|
|||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
{ serverConfigSection }
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{ errorText }
|
||||
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Return to login screen') }
|
||||
</a>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
<LanguageSelector />
|
||||
|
|
|
@ -26,10 +26,17 @@ import Login from '../../../Login';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
// These are used in several places, and come from the js-sdk's autodiscovery
|
||||
// stuff. We define them here so that they'll be picked up by i18n.
|
||||
_td("Invalid homeserver discovery response");
|
||||
_td("Invalid identity server discovery response");
|
||||
_td("General failure");
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
|
@ -50,6 +57,14 @@ module.exports = React.createClass({
|
|||
// different home server without confusing users.
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. This is used when displaying the defaultHsUrl in the UI.
|
||||
defaultServerName: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
|
@ -74,6 +89,12 @@ module.exports = React.createClass({
|
|||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
currentFlow: "m.login.password",
|
||||
|
||||
// .well-known discovery
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -105,6 +126,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
// Prevent people from submitting their password when homeserver
|
||||
// discovery went wrong
|
||||
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
|
@ -189,7 +214,10 @@ module.exports = React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
_onLoginAsGuestClick: function() {
|
||||
_onLoginAsGuestClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const self = this;
|
||||
self.setState({
|
||||
busy: true,
|
||||
|
@ -221,6 +249,22 @@ module.exports = React.createClass({
|
|||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onUsernameBlur: function(username) {
|
||||
this.setState({ username: username });
|
||||
if (username[0] === "@") {
|
||||
const serverName = username.split(':').slice(1).join(':');
|
||||
try {
|
||||
// we have to append 'https://' to make the URL constructor happy
|
||||
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
|
||||
const url = new URL("https://" + serverName);
|
||||
this._tryWellKnownDiscovery(url.hostname);
|
||||
} catch (e) {
|
||||
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPhoneCountryChanged: function(phoneCountry) {
|
||||
this.setState({ phoneCountry: phoneCountry });
|
||||
},
|
||||
|
@ -256,6 +300,65 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({findingHomeserver: true});
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: discovery["m.homeserver"].error,
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.SUCCESS) {
|
||||
this.setState({
|
||||
discoveredHsUrl: discovery["m.homeserver"].base_url,
|
||||
discoveredIsUrl:
|
||||
discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else {
|
||||
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: _t("Unknown failure discovering homeserver"),
|
||||
findingHomeserver: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
findingHomeserver: false,
|
||||
discoveryError: _t("Unknown error discovering homeserver"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
|
@ -393,11 +496,14 @@ module.exports = React.createClass({
|
|||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsUrl={this.state.enteredHomeserverUrl}
|
||||
hsName={this.props.defaultServerName}
|
||||
disableSubmit={this.state.findingHomeserver}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -416,6 +522,8 @@ module.exports = React.createClass({
|
|||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
|
||||
|
||||
let loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
|
@ -430,8 +538,8 @@ module.exports = React.createClass({
|
|||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
|
@ -443,16 +551,16 @@ module.exports = React.createClass({
|
|||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
if (!errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
let errorTextSection;
|
||||
if (this.state.errorText) {
|
||||
if (errorText) {
|
||||
errorTextSection = (
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
{ errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -468,7 +576,7 @@ module.exports = React.createClass({
|
|||
{ errorTextSection }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
{ serverConfig }
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
<a className="mx_Login_create" onClick={this.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
|
|
|
@ -57,6 +57,14 @@ module.exports = React.createClass({
|
|||
}),
|
||||
teamSelected: PropTypes.object,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. This is used when displaying the defaultHsUrl in the UI.
|
||||
defaultServerName: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
|
@ -170,6 +178,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true,
|
||||
|
@ -328,7 +342,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t('A phone number is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
|
||||
errMsg = _t("Only use lower case letters, numbers and '=_-./'");
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = _t('You need to enter a user name.');
|
||||
|
@ -349,6 +363,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onLoginClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onLoginClick();
|
||||
},
|
||||
|
||||
_makeRegisterRequest: function(auth) {
|
||||
// Only send the bind params if we're sending username / pw params
|
||||
// (Since we need to send no params at all to use the ones saved in the
|
||||
|
@ -441,19 +461,20 @@ module.exports = React.createClass({
|
|||
let header;
|
||||
let errorText;
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
if (theme === 'status' && this.state.errorText) {
|
||||
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
if (theme === 'status' && err) {
|
||||
header = <div className="mx_Login_error">{ err }</div>;
|
||||
} else {
|
||||
header = <h2>{ _t('Create an account') }</h2>;
|
||||
if (this.state.errorText) {
|
||||
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let signIn;
|
||||
if (!this.state.doingUIAuth) {
|
||||
signIn = (
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
<a className="mx_Login_create" onClick={this.onLoginClick} href="#">
|
||||
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
|
||||
</a>
|
||||
);
|
||||
|
|
120
src/components/views/avatars/MemberStatusMessageAvatar.js
Normal file
120
src/components/views/avatars/MemberStatusMessageAvatar.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import classNames from 'classnames';
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class MemberStatusMessageAvatar extends React.Component {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
|
||||
if (this.props.member.user) {
|
||||
this.setState({message: this.props.member.user._unstable_statusMessage});
|
||||
} else {
|
||||
this.setState({message: ""});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomStateEvents = (ev, state) => {
|
||||
if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return;
|
||||
if (ev.getType() !== "im.vector.user_status") return;
|
||||
// TODO: We should be relying on `this.props.member.user._unstable_statusMessage`
|
||||
// We don't currently because the js-sdk doesn't emit a specific event for this
|
||||
// change, and we don't want to race it. This should be improved when we rip out
|
||||
// the im.vector.user_status stuff and replace it with a complete solution.
|
||||
this.setState({message: ev.getContent()["status"]});
|
||||
};
|
||||
|
||||
_onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
|
||||
|
||||
ContextualMenu.createMenu(StatusMessageContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: 190,
|
||||
user: this.props.member.user,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
return <MemberAvatar member={this.props.member}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod} />;
|
||||
}
|
||||
|
||||
const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false;
|
||||
|
||||
const classes = classNames({
|
||||
"mx_MemberStatusMessageAvatar": true,
|
||||
"mx_MemberStatusMessageAvatar_hasStatus": hasStatus,
|
||||
});
|
||||
|
||||
return <AccessibleButton onClick={this._onClick} className={classes} element="div">
|
||||
<MemberAvatar member={this.props.member}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod} />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
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 PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class StatusMessageContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
// js-sdk User object. Not required because it might not exist.
|
||||
user: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
message: props.user ? props.user._unstable_statusMessage : "",
|
||||
};
|
||||
}
|
||||
|
||||
_onClearClick = async(e) => {
|
||||
await MatrixClientPeg.get()._unstable_setStatusMessage("");
|
||||
this.setState({message: ""});
|
||||
};
|
||||
|
||||
_onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
||||
};
|
||||
|
||||
_onStatusChange = (e) => {
|
||||
this.setState({message: e.target.value});
|
||||
};
|
||||
|
||||
render() {
|
||||
const formSubmitClasses = classNames({
|
||||
"mx_StatusMessageContextMenu_submit": true,
|
||||
"mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded
|
||||
});
|
||||
|
||||
const form = <form className="mx_StatusMessageContextMenu_form" onSubmit={this._onSubmit} autoComplete="off">
|
||||
<input type="text" key="message" placeholder={_t("Set a new status...")} autoFocus={true}
|
||||
className="mx_StatusMessageContextMenu_message"
|
||||
value={this.state.message} onChange={this._onStatusChange} maxLength="60" />
|
||||
<AccessibleButton onClick={this._onSubmit} element="div" className={formSubmitClasses}>
|
||||
<img src="img/icons-checkmark.svg" width="22" height="22" />
|
||||
</AccessibleButton>
|
||||
</form>;
|
||||
|
||||
const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg";
|
||||
const clearButton = <AccessibleButton onClick={this._onClearClick} disabled={!this.state.message}
|
||||
className="mx_StatusMessageContextMenu_clear">
|
||||
<img src={clearIcon} alt={_t('Clear status')} width="12" height="12"
|
||||
className="mx_filterFlipColor mx_StatusMessageContextMenu_clearIcon" />
|
||||
<span>{_t("Clear status")}</span>
|
||||
</AccessibleButton>;
|
||||
|
||||
const menuClasses = classNames({
|
||||
"mx_StatusMessageContextMenu": true,
|
||||
"mx_StatusMessageContextMenu_hasStatus": this.state.message,
|
||||
});
|
||||
|
||||
return <div className={menuClasses}>
|
||||
{ form }
|
||||
<hr />
|
||||
{ clearButton }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from "../../../email";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
@ -419,6 +420,10 @@ module.exports = React.createClass({
|
|||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
if (addrType === 'email' && !Email.looksValid(query)) {
|
||||
this.setState({searchError: _t("That doesn't look like a valid email address")});
|
||||
return;
|
||||
}
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
|
|
|
@ -57,8 +57,7 @@ export default React.createClass({
|
|||
className: PropTypes.string,
|
||||
|
||||
// Title for the dialog.
|
||||
// (could probably actually be something more complicated than a string if desired)
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: PropTypes.node,
|
||||
|
|
|
@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component {
|
|||
for (let i=0; i<REPOS.length; i++) {
|
||||
const oldVersion = version2[2*i];
|
||||
const newVersion = version[2*i];
|
||||
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
|
||||
if (body == null) return;
|
||||
const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
|
||||
request(url, (err, response, body) => {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
this.setState({ [REPOS[i]]: response.statusText });
|
||||
return;
|
||||
}
|
||||
this.setState({[REPOS[i]]: JSON.parse(body).commits});
|
||||
});
|
||||
}
|
||||
|
@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component {
|
|||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
|
||||
const logs = REPOS.map(repo => {
|
||||
if (this.state[repo] == null) return <Spinner key={repo} />;
|
||||
let content;
|
||||
if (this.state[repo] == null) {
|
||||
content = <Spinner key={repo} />;
|
||||
} else if (typeof this.state[repo] === "string") {
|
||||
content = _t("Unable to load commit detail: %(msg)s", {
|
||||
msg: this.state[repo],
|
||||
});
|
||||
} else {
|
||||
content = this.state[repo].map(this._elementsForCommit);
|
||||
}
|
||||
return (
|
||||
<div key={repo}>
|
||||
<h2>{repo}</h2>
|
||||
<ul>
|
||||
{this.state[repo].map(this._elementsForCommit)}
|
||||
</ul>
|
||||
<ul>{content}</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
const deactivationPreferences =
|
||||
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
||||
|
||||
const shouldErase = (
|
||||
deactivationPreferences &&
|
||||
deactivationPreferences.getContent() &&
|
||||
deactivationPreferences.getContent().shouldErase
|
||||
) || false;
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
busy: false,
|
||||
shouldErase,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
|
||||
// Before we deactivate the account insert an event into
|
||||
// the user's account data indicating that they wish to be
|
||||
// erased from the homeserver.
|
||||
//
|
||||
// We do this because the API for erasing after deactivation
|
||||
// might not be supported by the connected homeserver. Leaving
|
||||
// an indication in account data is only best-effort, and
|
||||
// in the worse case, the HS maintainer would have to run a
|
||||
// script to erase deactivated accounts that have shouldErase
|
||||
// set to true in im.riot.account_deactivation_preferences.
|
||||
//
|
||||
// Note: The preferences are scoped to Riot, hence the
|
||||
// "im.riot..." event type.
|
||||
//
|
||||
// Note: This may have already been set on previous attempts
|
||||
// where, for example, the user entered the wrong password.
|
||||
// This is fine because the UI always indicates the preference
|
||||
// prior to us calling `deactivateAccount`.
|
||||
try {
|
||||
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
||||
shouldErase: this.state.shouldErase,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: _t('Failed to indicate account erasure'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
|
|
51
src/components/views/dialogs/RedesignFeedbackDialog.js
Normal file
51
src/components/views/dialogs/RedesignFeedbackDialog.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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 QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default (props) => {
|
||||
const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" +
|
||||
"?q=is%3Aopen+is%3Aissue+label%3Aredesign+sort%3Areactions-%2B1-desc";
|
||||
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new" +
|
||||
"?assignees=&labels=redesign&template=redesign_issue.md&title=";
|
||||
|
||||
const description1 =
|
||||
_t("Thanks for testing the Riot Redesign. " +
|
||||
"If you run into any bugs or visual issues, " +
|
||||
"please let us know on GitHub.");
|
||||
const description2 = _t("To help avoid duplicate issues, " +
|
||||
"please <existingIssuesLink>view existing issues</existingIssuesLink> " +
|
||||
"first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> " +
|
||||
"if you can't find it.", {},
|
||||
{
|
||||
existingIssuesLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={existingIssuesUrl}>{ sub }</a>;
|
||||
},
|
||||
newIssueLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
|
||||
},
|
||||
});
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Report bugs & give feedback")}
|
||||
description={<div><p>{description1}</p><p>{description2}</p></div>}
|
||||
button={_t("Go back")}
|
||||
onFinished={props.onFinished}
|
||||
/>);
|
||||
};
|
|
@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import classnames from 'classnames';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
|
@ -110,12 +111,11 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_doUsernameCheck: function() {
|
||||
// XXX: SPEC-1
|
||||
// Check if username is valid
|
||||
// Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190
|
||||
if (encodeURIComponent(this.state.username) !== this.state.username) {
|
||||
// We do a quick check ahead of the username availability API to ensure the
|
||||
// user ID roughly looks okay from a Matrix perspective.
|
||||
if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
|
||||
this.setState({
|
||||
usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'),
|
||||
usernameError: _t("Only use lower case letters, numbers and '=_-./'"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
|
@ -210,7 +210,6 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let auth;
|
||||
if (this.state.doingUIAuth) {
|
||||
|
@ -230,9 +229,8 @@ export default React.createClass({
|
|||
});
|
||||
|
||||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||
usernameIndicator = <div>{_t("Checking...")}</div>;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
|
@ -270,7 +268,6 @@ export default React.createClass({
|
|||
size="30"
|
||||
className={inputClasses}
|
||||
/>
|
||||
{ usernameBusyIndicator }
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
|
|
|
@ -31,6 +31,7 @@ export default React.createClass({
|
|||
mouseOverAction: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
iconPath: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -76,8 +77,13 @@ export default React.createClass({
|
|||
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
|
||||
undefined;
|
||||
|
||||
const classNames = ["mx_RoleButton"];
|
||||
if (this.props.className) {
|
||||
classNames.push(this.props.className);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoleButton"
|
||||
<AccessibleButton className={classNames.join(" ")}
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
|
|
|
@ -22,7 +22,6 @@ 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';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
|
@ -49,7 +48,6 @@ export default class AppTile extends React.Component {
|
|||
this.state = this._getNewState(props);
|
||||
|
||||
this._onAction = this._onAction.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);
|
||||
|
@ -143,10 +141,6 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
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._onAction);
|
||||
}
|
||||
|
@ -155,9 +149,6 @@ export default class AppTile extends React.Component {
|
|||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
|
@ -233,32 +224,6 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// Legacy Jitsi widget messaging
|
||||
// TODO -- This should be replaced with the new widget postMessaging API
|
||||
_onMessage(event) {
|
||||
if (this.props.type !== 'jitsi') {
|
||||
return;
|
||||
}
|
||||
if (!event.origin) {
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||
const eventOrigin = url.parse(event.origin);
|
||||
if (
|
||||
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||
eventOrigin.host !== widgetUrlObj.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
|
||||
const iframe = this.refs.appFrame.contentWindow
|
||||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||
}
|
||||
}
|
||||
|
||||
_canUserModify() {
|
||||
// User widgets should always be modifiable by their creator
|
||||
if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) {
|
||||
|
@ -544,7 +509,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// 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;";
|
||||
const iframeFeatures = "microphone; camera; encrypted-media; autoplay;";
|
||||
|
||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
|
||||
|
|
|
@ -22,17 +22,16 @@ import { _t } from '../../../languageHandler';
|
|||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_my_groups"
|
||||
<ActionButton className="mx_GroupsButton" action="view_my_groups"
|
||||
label={_t("Communities")}
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
tooltip={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GroupsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default GroupsButton;
|
||||
|
|
|
@ -91,7 +91,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
|
||||
integrationsButton = (
|
||||
<AccessibleButton className={integrationsButtonClasses} onClick={this.onManageIntegrations} title={_t('Manage Integrations')}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35" />
|
||||
<TintableSvg src="img/feather-icons/grid.svg" width="20" height="20" />
|
||||
{ integrationsWarningTriangle }
|
||||
{ integrationsErrorPopup }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -14,13 +14,14 @@ const ResizeHandle = (props) => {
|
|||
classNames.push('mx_ResizeHandle_reverse');
|
||||
}
|
||||
return (
|
||||
<div className={classNames.join(' ')} data-id={props.id} />
|
||||
<div className={classNames.join(' ')} data-id={props.id}><div /></div>
|
||||
);
|
||||
};
|
||||
|
||||
ResizeHandle.propTypes = {
|
||||
vertical: PropTypes.bool,
|
||||
reverse: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ResizeHandle;
|
||||
|
|
|
@ -37,7 +37,9 @@ export default React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
members: null,
|
||||
membersError: null,
|
||||
invitedMembers: null,
|
||||
invitedMembersError: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
};
|
||||
},
|
||||
|
@ -55,6 +57,19 @@ export default React.createClass({
|
|||
GroupStore.registerListener(groupId, () => {
|
||||
this._fetchMembers();
|
||||
});
|
||||
GroupStore.on('error', (err, errorGroupId, stateKey) => {
|
||||
if (this._unmounted || groupId !== errorGroupId) return;
|
||||
if (stateKey === GroupStore.STATE_KEY.GroupMembers) {
|
||||
this.setState({
|
||||
membersError: err,
|
||||
});
|
||||
}
|
||||
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) {
|
||||
this.setState({
|
||||
invitedMembersError: err,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_fetchMembers: function() {
|
||||
|
@ -88,7 +103,11 @@ export default React.createClass({
|
|||
this.setState({ searchQuery: ev.target.value });
|
||||
},
|
||||
|
||||
makeGroupMemberTiles: function(query, memberList) {
|
||||
makeGroupMemberTiles: function(query, memberList, memberListError) {
|
||||
if (memberListError) {
|
||||
return <div className="warning">{ _t("Failed to load group members") }</div>;
|
||||
}
|
||||
|
||||
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
query = (query || "").toLowerCase();
|
||||
|
@ -166,13 +185,25 @@ export default React.createClass({
|
|||
);
|
||||
|
||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) }
|
||||
{
|
||||
this.makeGroupMemberTiles(
|
||||
this.state.searchQuery,
|
||||
this.state.members,
|
||||
this.state.membersError,
|
||||
)
|
||||
}
|
||||
</div> : <div />;
|
||||
|
||||
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) }
|
||||
<h2>{_t("Invited")}</h2>
|
||||
{
|
||||
this.makeGroupMemberTiles(
|
||||
this.state.searchQuery,
|
||||
this.state.invitedMembers,
|
||||
this.state.invitedMembersError,
|
||||
)
|
||||
}
|
||||
</div> : <div />;
|
||||
|
||||
let inviteButton;
|
||||
|
|
|
@ -30,6 +30,7 @@ class PasswordLogin extends React.Component {
|
|||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
|
@ -39,6 +40,8 @@ class PasswordLogin extends React.Component {
|
|||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
hsDomain: "",
|
||||
hsName: null,
|
||||
disableSubmit: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -53,6 +56,7 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||
this.onUsernameBlur = this.onUsernameBlur.bind(this);
|
||||
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
|
@ -124,6 +128,10 @@ class PasswordLogin extends React.Component {
|
|||
this.props.onUsernameChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onUsernameBlur(ev) {
|
||||
this.props.onUsernameBlur(this.state.username);
|
||||
}
|
||||
|
||||
onLoginTypeChange(loginType) {
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
|
@ -167,6 +175,7 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
|
@ -182,6 +191,7 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
|
@ -242,13 +252,15 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let matrixIdText = '';
|
||||
if (this.props.hsUrl) {
|
||||
let matrixIdText = _t('Matrix ID');
|
||||
if (this.props.hsName) {
|
||||
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
|
||||
} catch (e) {
|
||||
// pass
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,6 +292,8 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const disableSubmit = this.props.disableSubmit || matrixIdText === '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
|
@ -293,7 +307,7 @@ class PasswordLogin extends React.Component {
|
|||
/>
|
||||
<br />
|
||||
{ forgotPasswordJsx }
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={matrixIdText === ''} />
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={disableSubmit} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
@ -317,6 +331,8 @@ PasswordLogin.propTypes = {
|
|||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
hsName: PropTypes.string,
|
||||
disableSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = PasswordLogin;
|
||||
|
|
|
@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
|||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
|
@ -194,9 +194,8 @@ module.exports = React.createClass({
|
|||
} else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
var username = this.refs.username.value.trim();
|
||||
if (encodeURIComponent(username) != username) {
|
||||
const username = this.refs.username.value.trim();
|
||||
if (!SAFE_LOCALPART_REGEX.test(username)) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
|
|
|
@ -70,6 +70,23 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hs_url &&
|
||||
newProps.customIsUrl === this.state.is_url) return;
|
||||
|
||||
this.setState({
|
||||
hs_url: newProps.customHsUrl,
|
||||
is_url: newProps.customIsUrl,
|
||||
configVisible: !newProps.withToggleButton ||
|
||||
(newProps.customHsUrl !== newProps.defaultHsUrl) ||
|
||||
(newProps.customIsUrl !== newProps.defaultIsUrl),
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
},
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
this.setState({hs_url: ev.target.value}, function() {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
|
|
|
@ -85,8 +85,8 @@ export default React.createClass({
|
|||
_getDisplayedGroups(userGroups, relatedGroups) {
|
||||
let displayedGroups = userGroups || [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
displayedGroups = displayedGroups.filter((groupId) => {
|
||||
return relatedGroups.includes(groupId);
|
||||
displayedGroups = relatedGroups.filter((groupId) => {
|
||||
return displayedGroups.includes(groupId);
|
||||
});
|
||||
} else {
|
||||
displayedGroups = [];
|
||||
|
|
|
@ -55,23 +55,23 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
|
||||
renderButtons() {
|
||||
const isPhaseGroup = [
|
||||
const groupPhases = [
|
||||
RightPanel.Phase.GroupMemberInfo,
|
||||
RightPanel.Phase.GroupMemberList,
|
||||
].includes(this.state.phase);
|
||||
const isPhaseRoom = [
|
||||
];
|
||||
const roomPhases = [
|
||||
RightPanel.Phase.GroupRoomList,
|
||||
RightPanel.Phase.GroupRoomInfo,
|
||||
].includes(this.state.phase);
|
||||
];
|
||||
|
||||
return [
|
||||
<HeaderButton key="_groupMembersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
|
||||
isHighlighted={isPhaseGroup}
|
||||
isHighlighted={this.isPhase(groupPhases)}
|
||||
clickPhase={RightPanel.Phase.GroupMemberList}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room-nobg.svg"
|
||||
isHighlighted={isPhaseRoom}
|
||||
isHighlighted={this.isPhase(roomPhases)}
|
||||
clickPhase={RightPanel.Phase.GroupRoomList}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>,
|
||||
|
|
|
@ -36,6 +36,7 @@ export default class HeaderButton extends React.Component {
|
|||
dis.dispatch({
|
||||
action: 'view_right_panel_phase',
|
||||
phase: this.props.clickPhase,
|
||||
fromHeader: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
export default class HeaderButtons extends React.Component {
|
||||
|
@ -25,7 +26,7 @@ export default class HeaderButtons extends React.Component {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
phase: initialPhase,
|
||||
phase: props.collapsedRhs ? null : initialPhase,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
|
@ -47,11 +48,42 @@ export default class HeaderButtons extends React.Component {
|
|||
}, extras));
|
||||
}
|
||||
|
||||
isPhase(phases) {
|
||||
if (this.props.collapsedRhs) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(phases)) {
|
||||
return phases.includes(this.state.phase);
|
||||
} else {
|
||||
return phases === this.state.phase;
|
||||
}
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "view_right_panel_phase") {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
});
|
||||
// only actions coming from header buttons should collapse the right panel
|
||||
if (this.state.phase === payload.phase && payload.fromHeader) {
|
||||
dis.dispatch({
|
||||
action: 'hide_right_panel',
|
||||
});
|
||||
this.setState({
|
||||
phase: null,
|
||||
});
|
||||
} else {
|
||||
if (this.props.collapsedRhs && payload.fromHeader) {
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
});
|
||||
// emit payload again as the RightPanel didn't exist up
|
||||
// till show_right_panel, just without the fromHeader flag
|
||||
// as that would hide the right panel again
|
||||
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
|
||||
|
||||
}
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,3 +94,7 @@ export default class HeaderButtons extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
HeaderButtons.propTypes = {
|
||||
collapsedRhs: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -46,24 +46,24 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
|
||||
renderButtons() {
|
||||
const isMembersPhase = [
|
||||
const membersPhases = [
|
||||
RightPanel.Phase.RoomMemberList,
|
||||
RightPanel.Phase.RoomMemberInfo,
|
||||
].includes(this.state.phase);
|
||||
];
|
||||
|
||||
return [
|
||||
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
|
||||
isHighlighted={isMembersPhase}
|
||||
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/feather-icons/user.svg"
|
||||
isHighlighted={this.isPhase(membersPhases)}
|
||||
clickPhase={RightPanel.Phase.RoomMemberList}
|
||||
analytics={['Right Panel', 'Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/icons-files.svg"
|
||||
isHighlighted={this.state.phase === RightPanel.Phase.FilePanel}
|
||||
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/feather-icons/files.svg"
|
||||
isHighlighted={this.isPhase(RightPanel.Phase.FilePanel)}
|
||||
clickPhase={RightPanel.Phase.FilePanel}
|
||||
analytics={['Right Panel', 'File List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/icons-notifications.svg"
|
||||
isHighlighted={this.state.phase === RightPanel.Phase.NotificationPanel}
|
||||
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/feather-icons/notifications.svg"
|
||||
isHighlighted={this.isPhase(RightPanel.Phase.NotificationPanel)}
|
||||
clickPhase={RightPanel.Phase.NotificationPanel}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
|
|
|
@ -130,7 +130,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
isAliasValid: function(alias) {
|
||||
// XXX: FIXME SPEC-1
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||
},
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ const EntityTile = React.createClass({
|
|||
onClick: PropTypes.func,
|
||||
suppressOnHover: PropTypes.bool,
|
||||
showPresence: PropTypes.bool,
|
||||
subtextLabel: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -129,6 +130,9 @@ const EntityTile = React.createClass({
|
|||
presenceState={this.props.presenceState} />;
|
||||
nameClasses += ' mx_EntityTile_name_hover';
|
||||
}
|
||||
if (this.props.subtextLabel) {
|
||||
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
|
||||
}
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className={nameClasses} dir="auto">
|
||||
|
@ -137,6 +141,15 @@ const EntityTile = React.createClass({
|
|||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.subtextLabel) {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
{name}
|
||||
</EmojiText>
|
||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
nameEl = (
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
|
||||
|
|
|
@ -40,6 +40,8 @@ import { findReadReceiptFromUserId } from '../../../utils/Receipt';
|
|||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -714,12 +716,18 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const roomId = member && member.roomId ? member.roomId : this.props.roomId;
|
||||
const onInviteUserButton = async() => {
|
||||
try {
|
||||
await cli.invite(roomId, member.userId);
|
||||
// We use a MultiInviter to re-use the invite logic, even though
|
||||
// we're only inviting one user.
|
||||
const inviter = new MultiInviter(roomId);
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(userId) !== "invited")
|
||||
throw new Error(inviter.getErrorText(userId));
|
||||
});
|
||||
} catch (err) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||
title: _t('Failed to invite'),
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -882,11 +890,16 @@ module.exports = withMatrixClient(React.createClass({
|
|||
let presenceState;
|
||||
let presenceLastActiveAgo;
|
||||
let presenceCurrentlyActive;
|
||||
let statusMessage;
|
||||
|
||||
if (this.props.member.user) {
|
||||
presenceState = this.props.member.user.presence;
|
||||
presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
statusMessage = this.props.member.user._unstable_statusMessage;
|
||||
}
|
||||
}
|
||||
|
||||
const room = this.props.matrixClient.getRoom(this.props.member.roomId);
|
||||
|
@ -908,6 +921,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
presenceState={presenceState} />;
|
||||
}
|
||||
|
||||
let statusLabel = null;
|
||||
if (statusMessage) {
|
||||
statusLabel = <span className="mx_MemberInfo_statusMessage">{ statusMessage }</span>;
|
||||
}
|
||||
|
||||
let roomMemberDetails = null;
|
||||
if (this.props.member.roomId) { // is in room
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
|
@ -924,6 +942,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,9 @@ module.exports = React.createClass({
|
|||
// We listen for changes to the lastPresenceTs which is essentially
|
||||
// listening for all presence events (we display most of not all of
|
||||
// the information contained in presence events).
|
||||
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
cli.on("User.lastPresenceTs", this.onUserPresenceChange);
|
||||
cli.on("User.presence", this.onUserPresenceChange);
|
||||
cli.on("User.currentlyActive", this.onUserPresenceChange);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
||||
|
@ -81,7 +83,9 @@ module.exports = React.createClass({
|
|||
cli.removeListener("Room.myMembership", this.onMyMembership);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserPresenceChange);
|
||||
cli.removeListener("User.presence", this.onUserPresenceChange);
|
||||
cli.removeListener("User.currentlyActive", this.onUserPresenceChange);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
|
@ -132,12 +136,12 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
onUserLastPresenceTs(event, user) {
|
||||
onUserPresenceChange(event, user) {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
// console.log("explicit presence from " + user.userId);
|
||||
// ever attaching their own listener.
|
||||
const tile = this.refs[user.userId];
|
||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||
if (tile) {
|
||||
this._updateList(); // reorder the membership list
|
||||
}
|
||||
|
@ -181,6 +185,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updateList: new rate_limited_func(function() {
|
||||
this._updateListNow();
|
||||
}, 500),
|
||||
|
||||
_updateListNow: function() {
|
||||
// console.log("Updating memberlist");
|
||||
const newState = {
|
||||
loading: false,
|
||||
|
@ -189,7 +197,7 @@ module.exports = React.createClass({
|
|||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
||||
this.setState(newState);
|
||||
}, 500),
|
||||
},
|
||||
|
||||
getMembersWithUser: function() {
|
||||
if (!this.props.roomId) return [];
|
||||
|
@ -267,7 +275,8 @@ module.exports = React.createClass({
|
|||
if (!member) {
|
||||
return "(null)";
|
||||
} else {
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")";
|
||||
const u = member.user;
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -275,48 +284,59 @@ module.exports = React.createClass({
|
|||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort: function(memberA, memberB) {
|
||||
// order by last active, with "active now" first.
|
||||
// ...and then by power
|
||||
// ...and then alphabetically.
|
||||
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
|
||||
// order by presence, with "active now" first.
|
||||
// ...and then by power level
|
||||
// ...and then by last active
|
||||
// ...and then alphabetically.
|
||||
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
|
||||
|
||||
const userA = memberA.user;
|
||||
const userB = memberB.user;
|
||||
// console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`);
|
||||
|
||||
// if (!userA || !userB) {
|
||||
// console.log("comparing " + memberA.name + " user=" + memberA.user + " with " + memberB.name + " user=" + memberB.user);
|
||||
// }
|
||||
const userA = memberA.user;
|
||||
const userB = memberB.user;
|
||||
|
||||
if (!userA && !userB) return 0;
|
||||
if (userA && !userB) return -1;
|
||||
if (!userA && userB) return 1;
|
||||
// if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!");
|
||||
// if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!");
|
||||
|
||||
// console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB));
|
||||
if (!userA && !userB) return 0;
|
||||
if (userA && !userB) return -1;
|
||||
if (!userA && userB) return 1;
|
||||
|
||||
if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) {
|
||||
// console.log(memberA.name + " and " + memberB.name + " are both active");
|
||||
if (memberA.powerLevel === memberB.powerLevel) {
|
||||
// console.log(memberA + " and " + memberB + " have same power level");
|
||||
if (memberA.name && memberB.name) {
|
||||
// console.log("comparing names: " + memberA.name + " and " + memberB.name);
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
return nameA.localeCompare(nameB);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
// console.log("comparing power: " + memberA.powerLevel + " and " + memberB.powerLevel);
|
||||
return memberB.powerLevel - memberA.powerLevel;
|
||||
}
|
||||
// First by presence
|
||||
if (this._showPresence) {
|
||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||
const presenceIndex = p => {
|
||||
const order = ['active', 'online', 'offline'];
|
||||
const idx = order.indexOf(convertPresence(p));
|
||||
return idx === -1 ? order.length : idx; // unknown states at the end
|
||||
};
|
||||
|
||||
const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence);
|
||||
const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence);
|
||||
// console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`);
|
||||
if (idxA !== idxB) {
|
||||
// console.log("Comparing on presence group - returning");
|
||||
return idxA - idxB;
|
||||
}
|
||||
}
|
||||
|
||||
if (userA.currentlyActive && !userB.currentlyActive) return -1;
|
||||
if (!userA.currentlyActive && userB.currentlyActive) return 1;
|
||||
// Second by power level
|
||||
if (memberA.powerLevel !== memberB.powerLevel) {
|
||||
// console.log("Comparing on power level - returning");
|
||||
return memberB.powerLevel - memberA.powerLevel;
|
||||
}
|
||||
|
||||
// For now, let's just order things by timestamp. It's really annoying
|
||||
// that a user disappears from sight just because they temporarily go offline
|
||||
// Third by last active
|
||||
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
// console.log("Comparing on last active timestamp - returning");
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
||||
return nameA.localeCompare(nameB);
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(ev) {
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -85,6 +87,11 @@ module.exports = React.createClass({
|
|||
const active = -1;
|
||||
const presenceState = member.user ? member.user.presence : null;
|
||||
|
||||
let statusMessage = null;
|
||||
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
statusMessage = member.user._unstable_statusMessage;
|
||||
}
|
||||
|
||||
const av = (
|
||||
<MemberAvatar member={member} width={36} height={36} />
|
||||
);
|
||||
|
@ -106,7 +113,9 @@ module.exports = React.createClass({
|
|||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} />
|
||||
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence}
|
||||
subtextLabel={statusMessage}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -138,7 +138,8 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onUploadFileSelected(files) {
|
||||
this.uploadFiles(files.target.files);
|
||||
const tfiles = files.target.files;
|
||||
this.uploadFiles(tfiles);
|
||||
}
|
||||
|
||||
uploadFiles(files) {
|
||||
|
@ -146,10 +147,21 @@ export default class MessageComposer extends React.Component {
|
|||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
const fileList = [];
|
||||
const acceptedFiles = [];
|
||||
const failedFiles = [];
|
||||
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') }
|
||||
</li>);
|
||||
const fileAcceptedOrError = this.props.uploadAllowed(files[i]);
|
||||
if (fileAcceptedOrError === true) {
|
||||
acceptedFiles.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') }
|
||||
</li>);
|
||||
fileList.push(files[i]);
|
||||
} else {
|
||||
failedFiles.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> { files[i].name || _t('Attachment') } <p>{ _t('Reason') + ": " + fileAcceptedOrError}</p>
|
||||
</li>);
|
||||
}
|
||||
}
|
||||
|
||||
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
|
||||
|
@ -160,23 +172,47 @@ export default class MessageComposer extends React.Component {
|
|||
}</p>;
|
||||
}
|
||||
|
||||
const acceptedFilesPart = acceptedFiles.length === 0 ? null : (
|
||||
<div>
|
||||
<p>{ _t('Are you sure you want to upload the following files?') }</p>
|
||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||
{ acceptedFiles }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const failedFilesPart = failedFiles.length === 0 ? null : (
|
||||
<div>
|
||||
<p>{ _t('The following files cannot be uploaded:') }</p>
|
||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||
{ failedFiles }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
let buttonText;
|
||||
if (acceptedFiles.length > 0 && failedFiles.length > 0) {
|
||||
buttonText = "Upload selected"
|
||||
} else if (failedFiles.length > 0) {
|
||||
buttonText = "Close"
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
|
||||
title: _t('Upload Files'),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t('Are you sure you want to upload the following files?') }</p>
|
||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||
{ fileList }
|
||||
</ul>
|
||||
{ acceptedFilesPart }
|
||||
{ failedFilesPart }
|
||||
{ replyToWarning }
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: acceptedFiles.length > 0,
|
||||
button: buttonText,
|
||||
onFinished: (shouldUpload) => {
|
||||
if (shouldUpload) {
|
||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||
if (files) {
|
||||
for (let i=0; i<files.length; i++) {
|
||||
this.props.uploadFile(files[i]);
|
||||
if (fileList) {
|
||||
for (let i=0; i<fileList.length; i++) {
|
||||
this.props.uploadFile(fileList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +290,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
render() {
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
|
||||
|
@ -263,7 +299,7 @@ export default class MessageComposer extends React.Component {
|
|||
if (this.state.me) {
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={this.state.me} width={24} height={24} />
|
||||
<MemberStatusMessageAvatar member={this.state.me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
@ -301,17 +337,45 @@ export default class MessageComposer extends React.Component {
|
|||
} else {
|
||||
callButton =
|
||||
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<TintableSvg src="img/icon-call.svg" width="25" height="25" />
|
||||
<TintableSvg src="img/feather-icons/phone.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
videoCallButton =
|
||||
<AccessibleButton key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
|
||||
<TintableSvg src="img/icons-video.svg" width="25" height="25" />
|
||||
<TintableSvg src="img/feather-icons/video.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const canSendMessages = !this.state.tombstone &&
|
||||
this.props.room.maySendMessage();
|
||||
|
||||
// TODO: Remove temporary logging for riot-web#7838
|
||||
// Note: we rip apart the power level event ourselves because we don't want to
|
||||
// log too much data about it - just the bits we care about. Many of the variables
|
||||
// logged here are to help figure out where in the stack the 'cannot post in room'
|
||||
// warning is coming from. This means logging various numbers from the PL event to
|
||||
// verify RoomState._maySendEventOfType is doing the right thing.
|
||||
const room = this.props.room;
|
||||
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
let plEventString = "<no power level event>";
|
||||
if (plEvent) {
|
||||
const content = plEvent.getContent();
|
||||
if (!content) {
|
||||
plEventString = "<no event content>";
|
||||
} else {
|
||||
const stringifyFalsey = (v) => v === null ? '<null>' : (v === undefined ? '<undefined>' : v);
|
||||
const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "<no users in content>");
|
||||
const usersPl = stringifyFalsey(content.users_default);
|
||||
const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "<no events in content>");
|
||||
const eventPl = stringifyFalsey(content.events_default);
|
||||
plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` +
|
||||
` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` +
|
||||
` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'`
|
||||
);
|
||||
|
||||
if (canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
|
@ -319,7 +383,7 @@ export default class MessageComposer extends React.Component {
|
|||
const uploadButton = (
|
||||
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={_t('Upload file')}>
|
||||
<TintableSvg src="img/icons-upload.svg" width="25" height="25" />
|
||||
<TintableSvg src="img/feather-icons/paperclip.svg" width="20" height="20" />
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
|
@ -390,6 +454,8 @@ export default class MessageComposer extends React.Component {
|
|||
</div>
|
||||
</div>);
|
||||
} else {
|
||||
// TODO: Remove temporary logging for riot-web#7838
|
||||
console.log("[riot-web#7838] Falling back to showing cannot post in room error");
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
|
@ -460,6 +526,9 @@ MessageComposer.propTypes = {
|
|||
// callback when a file to upload is chosen
|
||||
uploadFile: PropTypes.func.isRequired,
|
||||
|
||||
// function to test whether a file should be allowed to be uploaded.
|
||||
uploadAllowed: PropTypes.func.isRequired,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: PropTypes.bool,
|
||||
roomViewStore: PropTypes.object.isRequired,
|
||||
|
|
|
@ -23,7 +23,6 @@ import sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from "../../../Modal";
|
||||
import dis from "../../../dispatcher";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import * as linkify from 'linkifyjs';
|
||||
|
@ -146,10 +145,6 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, '');
|
||||
},
|
||||
|
||||
onShowRhsClick: function(ev) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
|
@ -342,7 +337,7 @@ module.exports = React.createClass({
|
|||
if (this.props.onSettingsClick) {
|
||||
settingsButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
|
||||
<TintableSvg src="img/feather-icons/settings.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -382,7 +377,7 @@ module.exports = React.createClass({
|
|||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
searchButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
|
||||
<TintableSvg src="img/icons-search.svg" width="16" height="16" />
|
||||
<TintableSvg src="img/feather-icons/search.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -390,15 +385,7 @@ module.exports = React.createClass({
|
|||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanelButtons =
|
||||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_showPanel" onClick={this.onShowRhsClick} title={_t('Show panel')}>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" />
|
||||
<TintableSvg src="img/feather-icons/share.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -419,7 +406,6 @@ module.exports = React.createClass({
|
|||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
{ rightPanelButtons }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -433,7 +419,7 @@ module.exports = React.createClass({
|
|||
{ saveButton }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
{ !this.props.isGrid ? <RoomHeaderButtons /> : undefined }
|
||||
{ !this.props.isGrid ? <RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} /> : undefined }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -71,6 +71,10 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
|
||||
this._subListRefs = {
|
||||
// key => RoomSubList ref
|
||||
};
|
||||
|
||||
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
|
||||
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
||||
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
|
||||
|
@ -79,8 +83,10 @@ module.exports = React.createClass({
|
|||
isLoadingLeftRooms: false,
|
||||
totalRoomCount: null,
|
||||
lists: {},
|
||||
incomingCallTag: null,
|
||||
incomingCall: null,
|
||||
selectedTags: [],
|
||||
hover: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -148,6 +154,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.subListSizes[id] = newSize;
|
||||
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
|
||||
// update overflow indicators
|
||||
this._checkSubListsOverflow();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -163,19 +171,24 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
// load stored sizes
|
||||
Object.entries(this.subListSizes).forEach(([id, size]) => {
|
||||
const handle = this.resizer.forHandleWithId(id);
|
||||
if (handle) {
|
||||
handle.resize(size);
|
||||
}
|
||||
Object.keys(this.subListSizes).forEach((key) => {
|
||||
this._restoreSubListSize(key);
|
||||
});
|
||||
this._checkSubListsOverflow();
|
||||
|
||||
this.resizer.attach();
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
componentDidUpdate: function(prevProps) {
|
||||
this._repositionIncomingCallBox(undefined, false);
|
||||
if (this.props.searchFilter !== prevProps.searchFilter) {
|
||||
// restore sizes
|
||||
Object.keys(this.subListSizes).forEach((key) => {
|
||||
this._restoreSubListSize(key);
|
||||
});
|
||||
this._checkSubListsOverflow();
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
@ -188,11 +201,13 @@ module.exports = React.createClass({
|
|||
if (call && call.call_state === 'ringing') {
|
||||
this.setState({
|
||||
incomingCall: call,
|
||||
incomingCallTag: this.getTagNameForRoomId(payload.room_id),
|
||||
});
|
||||
this._repositionIncomingCallBox(undefined, true);
|
||||
} else {
|
||||
this.setState({
|
||||
incomingCall: null,
|
||||
incomingCallTag: null,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -280,6 +295,17 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onMouseEnter: function(ev) {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
||||
onMouseLeave: function(ev) {
|
||||
this.setState({hover: false});
|
||||
|
||||
// Refresh the room list just in case the user missed something.
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||
this.refreshRoomList();
|
||||
}, 500),
|
||||
|
@ -332,6 +358,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
refreshRoomList: function() {
|
||||
if (this.state.hover) {
|
||||
// Don't re-sort the list if we're hovering over the list
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||
// any changes to it incrementally, updating the appropriate sublists
|
||||
// as needed.
|
||||
|
@ -347,11 +378,36 @@ module.exports = React.createClass({
|
|||
// Do this here so as to not render every time the selected tags
|
||||
// themselves change.
|
||||
selectedTags: TagOrderStore.getSelectedTags(),
|
||||
}, () => {
|
||||
// we don't need to restore any size here, do we?
|
||||
// i guess we could have triggered a new group to appear
|
||||
// that already an explicit size the last time it appeared ...
|
||||
this._checkSubListsOverflow();
|
||||
});
|
||||
|
||||
// this._lastRefreshRoomListTs = Date.now();
|
||||
},
|
||||
|
||||
getTagNameForRoomId: function(roomId) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
for (const tagName of Object.keys(lists)) {
|
||||
for (const room of lists[tagName]) {
|
||||
// Should be impossible, but guard anyways.
|
||||
if (!room) {
|
||||
continue;
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (room.roomId === roomId) return tagName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
|
||||
|
@ -478,9 +534,38 @@ module.exports = React.createClass({
|
|||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
},
|
||||
|
||||
_persistCollapsedState: function(key, collapsed) {
|
||||
_handleCollapsedState: function(key, collapsed) {
|
||||
// persist collapsed state
|
||||
this.collapsedState[key] = collapsed;
|
||||
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
|
||||
// load the persisted size configuration of the expanded sub list
|
||||
if (!collapsed) {
|
||||
this._restoreSubListSize(key);
|
||||
}
|
||||
// check overflow, as sub lists sizes have changed
|
||||
// important this happens after calling resize above
|
||||
this._checkSubListsOverflow();
|
||||
},
|
||||
|
||||
_restoreSubListSize(key) {
|
||||
const size = this.subListSizes[key];
|
||||
const handle = this.resizer.forHandleWithId(key);
|
||||
if (handle) {
|
||||
handle.resize(size);
|
||||
}
|
||||
},
|
||||
|
||||
// check overflow for scroll indicator gradient
|
||||
_checkSubListsOverflow() {
|
||||
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
|
||||
},
|
||||
|
||||
_subListRef: function(key, ref) {
|
||||
if (!ref) {
|
||||
delete this._subListRefs[key];
|
||||
} else {
|
||||
this._subListRefs[key] = ref;
|
||||
}
|
||||
},
|
||||
|
||||
_mapSubListProps: function(subListsProps) {
|
||||
|
@ -505,7 +590,7 @@ module.exports = React.createClass({
|
|||
const {key, label, onHeaderClick, ... otherProps} = props;
|
||||
const chosenKey = key || label;
|
||||
const onSubListHeaderClick = (collapsed) => {
|
||||
this._persistCollapsedState(chosenKey, collapsed);
|
||||
this._handleCollapsedState(chosenKey, collapsed);
|
||||
if (onHeaderClick) {
|
||||
onHeaderClick(collapsed);
|
||||
}
|
||||
|
@ -513,6 +598,7 @@ module.exports = React.createClass({
|
|||
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
|
||||
let subList = (<RoomSubList
|
||||
ref={this._subListRef.bind(this, chosenKey)}
|
||||
startAsHidden={startAsHidden}
|
||||
onHeaderClick={onSubListHeaderClick}
|
||||
key={chosenKey}
|
||||
|
@ -535,6 +621,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const incomingCallIfTaggedAs = (tagName) => {
|
||||
if (!this.state.incomingCall) return null;
|
||||
if (this.state.incomingCallTag !== tagName) return null;
|
||||
return this.state.incomingCall;
|
||||
};
|
||||
|
||||
let subLists = [
|
||||
{
|
||||
list: [],
|
||||
|
@ -547,6 +639,7 @@ module.exports = React.createClass({
|
|||
list: this.state.lists['im.vector.fake.invite'],
|
||||
label: _t('Invites'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
|
||||
isInvite: true,
|
||||
},
|
||||
{
|
||||
|
@ -554,6 +647,7 @@ module.exports = React.createClass({
|
|||
label: _t('Favourites'),
|
||||
tagName: "m.favourite",
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.direct'],
|
||||
|
@ -561,6 +655,7 @@ module.exports = React.createClass({
|
|||
tagName: "im.vector.fake.direct",
|
||||
headerItems: this._getHeaderItems('im.vector.fake.direct'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
||||
},
|
||||
{
|
||||
|
@ -568,6 +663,7 @@ module.exports = React.createClass({
|
|||
label: _t('Rooms'),
|
||||
headerItems: this._getHeaderItems('im.vector.fake.recent'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_room'})},
|
||||
},
|
||||
];
|
||||
|
@ -581,6 +677,7 @@ module.exports = React.createClass({
|
|||
label: labelForTagName(tagName),
|
||||
tagName: tagName,
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs(tagName),
|
||||
};
|
||||
});
|
||||
subLists = subLists.concat(tagSubLists);
|
||||
|
@ -590,11 +687,13 @@ module.exports = React.createClass({
|
|||
label: _t('Low priority'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.archived'],
|
||||
label: _t('Historical'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
|
||||
startAsHidden: true,
|
||||
showSpinner: this.state.isLoadingLeftRooms,
|
||||
onHeaderClick: this.onArchivedHeaderClick,
|
||||
|
@ -604,15 +703,17 @@ module.exports = React.createClass({
|
|||
label: _t('System Alerts'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
|
||||
},
|
||||
]);
|
||||
|
||||
const subListComponents = this._mapSubListProps(subLists);
|
||||
|
||||
return (
|
||||
<div ref={this._collectResizeContainer} className="mx_RoomList">
|
||||
<div ref={this._collectResizeContainer} className="mx_RoomList"
|
||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
{ subListComponents }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
170
src/components/views/rooms/RoomRecoveryReminder.js
Normal file
170
src/components/views/rooms/RoomRecoveryReminder.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
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 PropTypes from "prop-types";
|
||||
import sdk from "../../../index";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
export default class RoomRecoveryReminder extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
unverifiedDevice: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
let backupSigStatus;
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let unverifiedDevice;
|
||||
for (const sig of backupSigStatus.sigs) {
|
||||
if (!sig.device.isVerified()) {
|
||||
unverifiedDevice = sig.device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
unverifiedDevice,
|
||||
});
|
||||
}
|
||||
|
||||
showSetupDialog = () => {
|
||||
if (this.state.unverifiedDevice) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so we'll show the device verify dialog.
|
||||
// TODO: Should change to a restore key backup flow that checks the recovery
|
||||
// passphrase while at the same time also cross-signing the device as well in
|
||||
// a single flow (for cases where a key backup exists but the backup creating
|
||||
// device is unverified). Since we don't have that yet, we'll look for an
|
||||
// unverified device and verify it. Note that this means we won't restore
|
||||
// keys yet; instead we'll only trust the backup for sending our own new keys
|
||||
// to it.
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
device: this.state.unverifiedDevice,
|
||||
onFinished: this.props.onFinished,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// The default case assumes that a key backup doesn't exist for this account, so
|
||||
// we'll show the create key backup flow.
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
{
|
||||
onFinished: this.props.onFinished,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onDontAskAgainClick = () => {
|
||||
// When you choose "Don't ask again" from the room reminder, we show a
|
||||
// dialog to confirm the choice.
|
||||
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
|
||||
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
|
||||
{
|
||||
onDontAskAgain: () => {
|
||||
// Report false to the caller, who should prevent the
|
||||
// reminder from appearing in the future.
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
onSetup: () => {
|
||||
this.showSetupDialog();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onSetupClick = () => {
|
||||
this.showSetupDialog();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
|
||||
let body;
|
||||
if (this.state.error) {
|
||||
body = <div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
</div>;
|
||||
} else if (this.state.unverifiedDevice) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified.
|
||||
body = _t(
|
||||
"To view your secure message history and ensure you can view new " +
|
||||
"messages on future devices, set up Secure Message Recovery.",
|
||||
);
|
||||
} else {
|
||||
// The default case assumes that a key backup doesn't exist for this account.
|
||||
// (This component doesn't currently check that itself.)
|
||||
body = _t(
|
||||
"If you log out or use another device, you'll lose your " +
|
||||
"secure message history. To prevent this, set up Secure " +
|
||||
"Message Recovery.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomRecoveryReminder">
|
||||
<div className="mx_RoomRecoveryReminder_header">{_t(
|
||||
"Secure Message Recovery",
|
||||
)}</div>
|
||||
<div className="mx_RoomRecoveryReminder_body">{body}</div>
|
||||
<div className="mx_RoomRecoveryReminder_buttons">
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("Don't ask again") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_button"
|
||||
onClick={this.onSetupClick}>
|
||||
{ _t("Set up") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ import * as RoomNotifs from '../../../RoomNotifs';
|
|||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -250,6 +251,17 @@ module.exports = React.createClass({
|
|||
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
const isJoined = this.props.room.getMyMembership() === "join";
|
||||
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
|
||||
let subtext = null;
|
||||
if (!isInvite && isJoined && looksLikeDm && SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
const selfId = MatrixClientPeg.get().getUserId();
|
||||
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
|
||||
if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) {
|
||||
subtext = otherMember.user._unstable_statusMessage;
|
||||
}
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
|
@ -260,6 +272,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const avatarClasses = classNames({
|
||||
|
@ -285,6 +298,7 @@ module.exports = React.createClass({
|
|||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
let subtextLabel;
|
||||
let tooltip;
|
||||
if (!this.props.collapsed) {
|
||||
const nameClasses = classNames({
|
||||
|
@ -293,6 +307,8 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
|
||||
if (this.state.selected) {
|
||||
const nameSelected = <EmojiText>{ name }</EmojiText>;
|
||||
|
||||
|
@ -332,13 +348,18 @@ module.exports = React.createClass({
|
|||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={32} height={32} />
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
{ label }
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>;
|
||||
|
|
|
@ -351,7 +351,7 @@ export default class Stickerpicker extends React.Component {
|
|||
onClick={this._onHideStickersClick}
|
||||
ref='target'
|
||||
title={_t("Hide Stickers")}>
|
||||
<TintableSvg src="img/icons-stickers.svg" width="25" height="25" />
|
||||
<TintableSvg src="img/feather-icons/face.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
|
@ -362,7 +362,7 @@ export default class Stickerpicker extends React.Component {
|
|||
className="mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}>
|
||||
<TintableSvg src="img/icons-stickers.svg" width="25" height="25" />
|
||||
<TintableSvg src="img/feather-icons/face.svg" width="20" height="20" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
return <div>
|
||||
|
|
|
@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component {
|
|||
}
|
||||
|
||||
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
||||
const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
|
||||
const sigStatusSubstitutions = {
|
||||
validity: sub =>
|
||||
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
||||
|
@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component {
|
|||
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
</span>,
|
||||
device: sub => <span className="mx_KeyBackupPanel_deviceName">{sig.device.getDisplayName()}</span>,
|
||||
device: sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>,
|
||||
};
|
||||
let sigStatus;
|
||||
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
|
||||
|
@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component {
|
|||
} else if (sig.valid && sig.device.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from " +
|
||||
"<verify>verified</verify> device <device>x</device>",
|
||||
"<verify>verified</verify> device <device></device>",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
} else if (sig.valid && !sig.device.isVerified()) {
|
||||
|
|
|
@ -483,8 +483,11 @@ module.exports = React.createClass({
|
|||
// The default push rules displayed by Vector UI
|
||||
'.m.rule.contains_display_name': 'vector',
|
||||
'.m.rule.contains_user_name': 'vector',
|
||||
'.m.rule.roomnotif': 'vector',
|
||||
'.m.rule.room_one_to_one': 'vector',
|
||||
'.m.rule.encrypted_room_one_to_one': 'vector',
|
||||
'.m.rule.message': 'vector',
|
||||
'.m.rule.encrypted': 'vector',
|
||||
'.m.rule.invite_for_me': 'vector',
|
||||
//'.m.rule.member_event': 'vector',
|
||||
'.m.rule.call': 'vector',
|
||||
|
@ -534,9 +537,12 @@ module.exports = React.createClass({
|
|||
const vectorRuleIds = [
|
||||
'.m.rule.contains_display_name',
|
||||
'.m.rule.contains_user_name',
|
||||
'.m.rule.roomnotif',
|
||||
'_keywords',
|
||||
'.m.rule.room_one_to_one',
|
||||
'.m.rule.encrypted_room_one_to_one',
|
||||
'.m.rule.message',
|
||||
'.m.rule.encrypted',
|
||||
'.m.rule.invite_for_me',
|
||||
//'im.vector.rule.member_event',
|
||||
'.m.rule.call',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue