Merge branches 'develop' and 't3chguy/m.relates_to' of github.com:matrix-org/matrix-react-sdk into t3chguy/m.relates_to

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

# Conflicts:
#	src/i18n/strings/en_EN.json
#	src/i18n/strings/eu.json
#	src/i18n/strings/fr.json
#	src/i18n/strings/lv.json
#	src/i18n/strings/ru.json
#	src/i18n/strings/zh_Hant.json
This commit is contained in:
Michael Telatynski 2018-04-12 09:48:06 +01:00
commit c77807bd22
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
41 changed files with 2135 additions and 359 deletions

View file

@ -30,11 +30,21 @@ module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: {
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
menuWidth: PropTypes.number,
menuHeight: PropTypes.number,
chevronOffset: PropTypes.number,
menuColour: PropTypes.string,
chevronFace: PropTypes.string, // top, bottom, left, right
// Function to be called on menu close
onFinished: PropTypes.func,
menuPaddingTop: PropTypes.number,
menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number,
},
getOrCreateContainer: function() {
@ -138,6 +148,15 @@ module.exports = {
if (!isNaN(Number(props.menuPaddingTop))) {
menuStyle["paddingTop"] = props.menuPaddingTop;
}
if (!isNaN(Number(props.menuPaddingLeft))) {
menuStyle["paddingLeft"] = props.menuPaddingLeft;
}
if (!isNaN(Number(props.menuPaddingBottom))) {
menuStyle["paddingBottom"] = props.menuPaddingBottom;
}
if (!isNaN(Number(props.menuPaddingRight))) {
menuStyle["paddingRight"] = props.menuPaddingRight;
}
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!

View file

@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
Copyright 2017, 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -399,6 +399,9 @@ FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupContext;
const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite";
export default React.createClass({
displayName: 'GroupView',
@ -463,6 +466,10 @@ export default React.createClass({
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
if (group.myMembership === 'leave') {
// Leave settings - the user might have clicked the "Leave" button
this._closeSettings();
}
this.setState({membershipBusy: false});
},
@ -545,6 +552,12 @@ export default React.createClass({
this.setState({
editing: true,
profileForm: Object.assign({}, this.state.summary.profile),
joinableForm: {
policyType:
this.state.summary.profile.is_openly_joinable ?
GROUP_JOINPOLICY_OPEN :
GROUP_JOINPOLICY_INVITE,
},
});
dis.dispatch({
action: 'panel_disable',
@ -553,6 +566,10 @@ export default React.createClass({
},
_onCancelClick: function() {
this._closeSettings();
},
_closeSettings() {
this.setState({
editing: false,
profileForm: null,
@ -607,11 +624,15 @@ export default React.createClass({
}).done();
},
_onJoinableChange: function(ev) {
this.setState({
joinableForm: { policyType: ev.target.value },
});
},
_onSaveClick: function() {
this.setState({saving: true});
const savePromise = this.state.isUserPrivileged ?
this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) :
Promise.resolve();
const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
savePromise.then((result) => {
this.setState({
saving: false,
@ -642,8 +663,20 @@ export default React.createClass({
}).done();
},
_onAcceptInviteClick: function() {
_saveGroup: async function() {
await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
type: this.state.joinableForm.policyType,
});
},
_onAcceptInviteClick: async function() {
this.setState({membershipBusy: true});
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.acceptGroupInvite().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
@ -656,9 +689,14 @@ export default React.createClass({
});
},
_onRejectInviteClick: function() {
_onRejectInviteClick: async function() {
this.setState({membershipBusy: true});
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -670,9 +708,14 @@ export default React.createClass({
});
},
_onJoinClick: function() {
_onJoinClick: async function() {
this.setState({membershipBusy: true});
this._matrixClient.joinGroup(this.props.groupId).then(() => {
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.joinGroup().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -691,11 +734,16 @@ export default React.createClass({
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
button: _t("Leave"),
danger: true,
onFinished: (confirmed) => {
onFinished: async (confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});
this._matrixClient.leaveGroup(this.props.groupId).then(() => {
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
this._groupStore.leaveGroup().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
@ -735,6 +783,7 @@ export default React.createClass({
return <div className={groupSettingsSectionClasses}>
{ header }
{ changeDelayWarning }
{ this._getJoinableNode() }
{ this._getLongDescriptionNode() }
{ this._getRoomsNode() }
</div>;
@ -927,7 +976,7 @@ export default React.createClass({
if ((!group || group.myMembership === 'leave') &&
this.state.summary &&
this.state.summary.profile &&
Boolean(this.state.summary.profile.is_joinable)
Boolean(this.state.summary.profile.is_openly_joinable)
) {
membershipButtonText = _t("Join this community");
membershipButtonOnClick = this._onJoinClick;
@ -968,8 +1017,8 @@ export default React.createClass({
return <div className={membershipContainerClasses}>
<div className="mx_GroupView_membershipSubSection">
{ /* Empty div for flex alignment */ }
<div />
{ /* The <div /> is for flex alignment */ }
{ this.state.membershipBusy ? <Spinner /> : <div /> }
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton
className={membershipButtonClasses}
@ -983,6 +1032,41 @@ export default React.createClass({
</div>;
},
_getJoinableNode: function() {
return this.state.editing ? <div>
<h3>
{ _t('Who can join this community?') }
{ this.state.groupJoinableLoading ?
<InlineSpinner /> : <div />
}
</h3>
<div>
<label>
<input type="radio"
value={GROUP_JOINPOLICY_INVITE}
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
onClick={this._onJoinableChange}
/>
<div className="mx_GroupView_label_text">
{ _t('Only people who have been invited') }
</div>
</label>
</div>
<div>
<label>
<input type="radio"
value={GROUP_JOINPOLICY_OPEN}
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
onClick={this._onJoinableChange}
/>
<div className="mx_GroupView_label_text">
{ _t('Everyone') }
</div>
</label>
</div>
</div> : null;
},
_getLongDescriptionNode: function() {
const summary = this.state.summary;
let description = null;

View file

@ -447,15 +447,18 @@ module.exports = React.createClass({
// is this a continuation of the previous message?
let continuation = false;
// Some events should appear as continuations from previous events of
// different types.
const continuedTypes = ['m.sticker', 'm.room.message'];
const eventTypeContinues =
prevEvent !== null &&
continuedTypes.includes(mxEv.getType()) &&
continuedTypes.includes(prevEvent.getType());
if (prevEvent !== null
&& prevEvent.sender && mxEv.sender
&& mxEv.sender.userId === prevEvent.sender.userId
// The preferred way of checking for 'continuation messages' is by
// checking whether subsiquent messages from the same user have a
// message body. This is because all messages intended to be displayed
// should have a 'body' whereas some (non-m.room) messages (such as
// m.sticker) may not have a message 'type'.
&& Boolean(mxEv.getContent().body) == Boolean(prevEvent.getContent().body)) {
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
continuation = true;
}

View file

@ -148,7 +148,7 @@ const TagPanel = React.createClass({
</Droppable>
</GeminiScrollbarWrapper>
<div className="mx_TagPanel_divider" />
<div className="mx_TagPanel_createGroupButton">
<div className="mx_TagPanel_groupsButton">
<GroupsButton tooltip={true} />
</div>
</div>;

View file

@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset";
@ -185,7 +186,7 @@ module.exports = React.createClass({
);
} else {
let serverConfigSection;
if (!config.disable_custom_urls) {
if (!SdkConfig.get().disable_custom_urls) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}

View file

@ -109,10 +109,15 @@ module.exports = React.createClass({
const mlist = props.room.currentState.members;
const userIds = [];
const leftUserIds = [];
// for .. in optimisation to return early if there are >2 keys
for (const uid in mlist) {
if (mlist.hasOwnProperty(uid)) {
userIds.push(uid);
if (["join", "invite"].includes(mlist[uid].membership)) {
userIds.push(uid);
} else {
leftUserIds.push(uid);
}
}
if (userIds.length > 2) {
return null;
@ -134,6 +139,14 @@ module.exports = React.createClass({
false,
);
} else if (userIds.length == 1) {
// The other 1-1 user left, leaving just the current user, so show the left user's avatar
if (leftUserIds.length === 1) {
return mlist[leftUserIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
false,
);
}
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),

View file

@ -52,6 +52,8 @@ export default class AppTile extends React.Component {
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
this._onInitialLoad = this._onInitialLoad.bind(this);
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
}
/**

View file

@ -18,6 +18,8 @@ import React from 'react';
import GeminiScrollbar from 'react-gemini-scrollbar';
function GeminiScrollbarWrapper(props) {
const {wrappedRef, ...wrappedProps} = props;
// Enable forceGemini so that gemini is always enabled. This is
// to avoid future issues where a feature is implemented without
// doing QA on every OS/browser combination.
@ -25,7 +27,7 @@ function GeminiScrollbarWrapper(props) {
// By default GeminiScrollbar allows native scrollbars to be used
// on macOS. Use forceGemini to enable Gemini's non-native
// scrollbars on all OSs.
return <GeminiScrollbar ref={props.wrappedRef} forceGemini={true} {...props}>
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
{ props.children }
</GeminiScrollbar>;
}

View file

@ -207,6 +207,7 @@ const Pill = React.createClass({
const member = this.state.member;
if (member) {
userId = member.userId;
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} />;

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
@ -55,14 +54,15 @@ export default React.createClass({
text = _t('Loading...');
} else if (groups.length > 0) {
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
const groupPublicityToggles = groups.map((groupId, index) => {
return <GroupPublicityToggle key={index} groupId={groupId} />;
});
text = _t('Display your community flair in rooms configured to show it.');
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
<GeminiScrollbar>
<GeminiScrollbarWrapper>
{ groupPublicityToggles }
</GeminiScrollbar>
</GeminiScrollbarWrapper>
</div>;
} else {
text = _t("You're not currently a member of any communities.");

View file

@ -473,8 +473,7 @@ module.exports = withMatrixClient(React.createClass({
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
// For now assume that anything that doesn't have a content body is an isInfoMessage
const isInfoMessage = !content.body; // Boolean comparison of non-boolean content body
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type

View file

@ -217,6 +217,8 @@ export default class Stickerpicker extends React.Component {
element: this._getStickerpickerContent(),
onFinished: this._onFinished,
menuPaddingTop: 0,
menuPaddingLeft: 0,
menuPaddingRight: 0,
});

View file

@ -20,6 +20,7 @@ limitations under the License.
const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
const sdk = require('../../../index');
module.exports = React.createClass({
@ -33,14 +34,19 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_TopUnreadMessagesBar">
<div className="mx_TopUnreadMessagesBar_scrollUp"
<AccessibleButton className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}>
<img src="img/scrollto.svg" width="24" height="24"
alt={_t('Scroll to unread messages')}
title={_t('Scroll to unread messages')} />
// No point on setting up non empty alt on this image
// as it only complements the text which follows it.
alt=""
title={_t('Scroll to unread messages')}
// In order not to use this title attribute for accessible name
// calculation of the parent button set the role presentation
role="presentation" />
{ _t("Jump to first unread message.") }
</div>
<img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
</AccessibleButton>
<AccessibleButton element='img' className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18"
alt={_t("Close")} title={_t("Close")}
onClick={this.props.onCloseClick} />