Merge branch 'develop' into travis/pinned-room-list

This commit is contained in:
Travis Ralston 2018-10-25 10:57:08 -06:00 committed by GitHub
commit 9cdb527163
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 999 additions and 416 deletions

View file

@ -746,13 +746,37 @@ export default React.createClass({
});
},
_leaveGroupWarnings: function() {
const warnings = [];
if (this.state.isUserPrivileged) {
warnings.push((
<span className="warning">
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
{ _t("You are an administrator of this community. You will not be " +
"able to rejoin without an invite from another administrator.") }
</span>
));
}
return warnings;
},
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const warnings = this._leaveGroupWarnings();
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
title: _t("Leave Community"),
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
description: (
<span>
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
{ warnings }
</span>
),
button: _t("Leave"),
danger: true,
danger: this.state.isUserPrivileged,
onFinished: async (confirmed) => {
if (!confirmed) return;

View file

@ -181,14 +181,8 @@ var LeftPanel = React.createClass({
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
const CallPreview = sdk.getComponent('voip.CallPreview');
let topBox;
if (this.context.matrixClient.isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox');
topBox = <LoginBox collapsed={ this.props.collapsed }/>;
} else {
const SearchBox = sdk.getComponent('structures.SearchBox');
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
}
const SearchBox = sdk.getComponent('structures.SearchBox');
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
const classes = classNames(
"mx_LeftPanel",

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,31 +17,15 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../languageHandler';
var sdk = require('../../index')
var dis = require('../../dispatcher');
var rate_limited_func = require('../../ratelimitedfunc');
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
const dis = require('../../dispatcher');
const AccessibleButton = require('../../components/views/elements/AccessibleButton');
module.exports = React.createClass({
displayName: 'LoginBox',
propTypes: {
collapsed: React.PropTypes.bool,
},
onToggleCollapse: function(show) {
if (show) {
dis.dispatch({
action: 'show_left_panel',
});
}
else {
dis.dispatch({
action: 'hide_left_panel',
});
}
},
onLoginClick: function() {
@ -52,41 +37,20 @@ module.exports = React.createClass({
},
render: function() {
var TintableSvg = sdk.getComponent('elements.TintableSvg');
var toggleCollapse;
if (this.props.collapsed) {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="Expand panel"/>
const loginButton = (
<div className="mx_LoginBox_loginButton_wrapper">
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
{ _t("Login") }
</AccessibleButton>
}
else {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="Collapse panel"/>
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
{ _t("Register") }
</AccessibleButton>
}
</div>
);
var loginButton;
if (!this.props.collapsed) {
loginButton = (
<div className="mx_LoginBox_loginButton_wrapper">
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
{ _t("Login") }
</AccessibleButton>
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
{ _t("Register") }
</AccessibleButton>
</div>
);
}
var self = this;
return (
<div className="mx_SearchBox mx_LoginBox">
<div className="mx_LoginBox">
{ loginButton }
{ toggleCollapse }
</div>
);
}

View file

@ -1034,6 +1034,7 @@ export default React.createClass({
{ warnings }
</span>
),
button: _t("Leave"),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
@ -1403,6 +1404,11 @@ export default React.createClass({
break;
}
});
// 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
const colorScheme = SettingsStore.getValue("roomColor");
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
},
/**

View file

@ -66,6 +66,10 @@ module.exports = React.createClass({
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number,
// true if the room is being peeked at. This affects components that shouldn't
// logically be shown when peeking, such as a prompt to invite people to a room.
isPeeking: PropTypes.bool,
// callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar
onResendAllClick: PropTypes.func,
@ -457,7 +461,7 @@ module.exports = React.createClass({
}
// If you're alone in the room, and have sent a message, suggest to invite someone
if (this.props.sentMessageAndIsAlone) {
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
return (
<div className="mx_RoomStatusBar_isAlone">
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +

View file

@ -678,8 +678,8 @@ module.exports = React.createClass({
if (!room) return;
console.log("Tinter.tint from updateTint");
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
},
onAccountData: function(event) {
@ -694,10 +694,10 @@ module.exports = React.createClass({
if (room.roomId == this.state.roomId) {
const type = event.getType();
if (type === "org.matrix.room.color_scheme") {
const color_scheme = event.getContent();
const colorScheme = event.getContent();
// XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this._updatePreviewUrlVisibility(room);
@ -1514,6 +1514,7 @@ module.exports = React.createClass({
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
@ -1558,6 +1559,7 @@ module.exports = React.createClass({
inviterName={inviterName}
canPreview={false}
spinner={this.state.joining}
spinnerState="joining"
room={this.state.room}
/>
</div>
@ -1595,6 +1597,7 @@ module.exports = React.createClass({
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall}
isPeeking={myMembership !== "join"}
onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
@ -1644,6 +1647,7 @@ module.exports = React.createClass({
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
canPreview={this.state.canPeek}
@ -1669,7 +1673,7 @@ module.exports = React.createClass({
let messageComposer, searchInfo;
const canSpeak = (
// joined and not showing search results
myMembership == 'join' && !this.state.searchResults
myMembership === 'join' && !this.state.searchResults
);
if (canSpeak) {
messageComposer =
@ -1683,6 +1687,11 @@ module.exports = React.createClass({
/>;
}
if (MatrixClientPeg.get().isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox');
messageComposer = <LoginBox/>;
}
// TODO: Why aren't we storing the term/scope/count in this format
// in this.state if this is what RoomHeader desires?
if (this.state.searchResults) {

View file

@ -84,6 +84,7 @@ const SIMPLE_SETTINGS = [
{ id: "RoomSubList.showEmpty" },
{ id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" },
{ id: "showDeveloperTools" },
];
// These settings must be defined in SettingsStore

View file

@ -625,7 +625,7 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body = <div>
body = <div className="mx_DevTools_dialog">
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
@ -634,7 +634,7 @@ export default class DevtoolsDialog extends React.Component {
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
<div>
<div className="mx_DevTools_dialog">
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />

View file

@ -70,15 +70,15 @@ module.exports = React.createClass({
}
return (
<div className="mx_Dialog_buttons">
{ cancelButton }
{ this.props.children }
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
disabled={this.props.disabled}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
{ cancelButton }
</div>
);
},

View file

@ -165,7 +165,7 @@ export default React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true}>
{ joined }
{ invited }
</GeminiScrollbarWrapper>

View file

@ -22,6 +22,7 @@ import classnames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -209,6 +210,125 @@ export const RecaptchaAuthEntry = React.createClass({
},
});
export const TermsAuthEntry = React.createClass({
displayName: 'TermsAuthEntry',
statics: {
LOGIN_TYPE: "m.login.terms",
},
propTypes: {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
},
componentWillMount: function() {
// example stageParams:
//
// {
// "policies": {
// "privacy_policy": {
// "version": "1.0",
// "en": {
// "name": "Privacy Policy",
// "url": "https://example.org/privacy-1.0-en.html",
// },
// "fr": {
// "name": "Politique de confidentialité",
// "url": "https://example.org/privacy-1.0-fr.html",
// },
// },
// "other_policy": { ... },
// }
// }
const allPolicies = this.props.stageParams.policies || {};
const prefLang = SettingsStore.getValue("language");
const initToggles = {};
const pickedPolicies = [];
for (const policyId of Object.keys(allPolicies)) {
const policy = allPolicies[policyId];
// Pick a language based on the user's language, falling back to english,
// and finally to the first language available. If there's still no policy
// available then the homeserver isn't respecting the spec.
let langPolicy = policy[prefLang];
if (!langPolicy) langPolicy = policy["en"];
if (!langPolicy) {
// last resort
const firstLang = Object.keys(policy).find(e => e !== "version");
langPolicy = policy[firstLang];
}
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
initToggles[policyId] = false;
langPolicy.id = policyId;
pickedPolicies.push(langPolicy);
}
this.setState({
"toggledPolicies": initToggles,
"policies": pickedPolicies,
});
},
_trySubmit: function(policyId) {
const newToggles = {};
let allChecked = true;
for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id];
if (policy.id === policyId) checked = !checked;
newToggles[policy.id] = checked;
allChecked = allChecked && checked;
}
this.setState({"toggledPolicies": newToggles});
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
},
render: function() {
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
}
let checkboxes = [];
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
allChecked = allChecked && checked;
checkboxes.push(
<label key={"policy_checkbox_" + policy.id}>
<input type="checkbox" onClick={() => this._trySubmit(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
</label>
);
}
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
}
return (
<div>
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
{ checkboxes }
{ errorSection }
</div>
);
},
});
export const EmailIdentityAuthEntry = React.createClass({
displayName: 'EmailIdentityAuthEntry',
@ -496,6 +616,7 @@ const AuthEntryComponents = [
RecaptchaAuthEntry,
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {

View file

@ -220,8 +220,9 @@ module.exports = React.createClass({
let canonical_alias_section;
if (this.props.canSetCanonicalAlias) {
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
canonical_alias_section = (
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
<select onChange={this.onCanonicalAliasChange} value={canonicalValue}>
<option value="" key="unset">{ _t('not specified') }</option>
{
Object.keys(self.state.domainToAliases).map((domain, i) => {

View file

@ -447,7 +447,7 @@ module.exports = React.createClass({
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}

View file

@ -573,29 +573,42 @@ export default class MessageComposerInput extends React.Component {
}
// emojioneify any emoji
editorState.document.getTexts().forEach(node => {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
let foundEmoji;
do {
foundEmoji = false;
for (const node of editorState.document.getTexts()) {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
EMOJI_REGEX.lastIndex = 0;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
// if we replaced an emoji, start again looking for more
// emoji in the new editor state since doing the replacement
// will change the node structure & offsets so we can't compute
// insertion ranges from node.key / match.index anymore.
foundEmoji = true;
break;
}
}
}
});
} while (foundEmoji);
// work around weird bug where inserting emoji via the macOS
// emoji picker can leave the selection stuck in the emoji's

View file

@ -44,9 +44,13 @@ module.exports = React.createClass({
error: PropTypes.object,
canPreview: PropTypes.bool,
spinner: PropTypes.bool,
room: PropTypes.object,
// When a spinner is present, a spinnerState can be specified to indicate the
// purpose of the spinner.
spinner: PropTypes.bool,
spinnerState: PropTypes.oneOf(["joining"]),
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
@ -93,7 +97,12 @@ module.exports = React.createClass({
if (this.props.spinner || this.state.busy) {
const Spinner = sdk.getComponent("elements.Spinner");
let spinnerIntro = "";
if (this.props.spinnerState === "joining") {
spinnerIntro = _t("Joining room...");
}
return (<div className="mx_RoomPreviewBar">
<p className="mx_RoomPreviewBar_spinnerIntro">{ spinnerIntro }</p>
<Spinner />
</div>);
}

View file

@ -590,6 +590,11 @@ module.exports = React.createClass({
}
},
_openDevtools: function() {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, {roomId: this.props.room.roomId});
},
_renderEncryptionSection: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
@ -942,6 +947,11 @@ module.exports = React.createClass({
</AccessibleButton>;
}
const devtoolsButton = SettingsStore.getValue("showDeveloperTools") ?
(<AccessibleButton className="mx_RoomSettings_devtoolsButton" onClick={this._openDevtools}>
{ _t("Open Devtools") }
</AccessibleButton>) : null;
return (
<div className="mx_RoomSettings">
@ -1055,6 +1065,7 @@ module.exports = React.createClass({
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
{ roomUpgradeButton }
{ devtoolsButton }
</div>
</div>
);