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:
commit
c77807bd22
41 changed files with 2135 additions and 359 deletions
|
@ -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!
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -217,6 +217,8 @@ export default class Stickerpicker extends React.Component {
|
|||
element: this._getStickerpickerContent(),
|
||||
onFinished: this._onFinished,
|
||||
menuPaddingTop: 0,
|
||||
menuPaddingLeft: 0,
|
||||
menuPaddingRight: 0,
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue