Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/snapshot

This commit is contained in:
Richard Lewis 2018-02-23 15:37:33 +00:00
commit b2bf4d4709
219 changed files with 9309 additions and 3694 deletions

View file

@ -15,6 +15,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
@ -44,9 +45,9 @@ export default function AccessibleButton(props) {
* implemented exactly like a normal onClick handler.
*/
AccessibleButton.propTypes = {
children: React.PropTypes.node,
element: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
children: PropTypes.node,
element: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
AccessibleButton.defaultProps = {

View file

@ -18,6 +18,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress';
@ -26,17 +27,17 @@ export default React.createClass({
displayName: 'AddressSelector',
propTypes: {
onSelected: React.PropTypes.func.isRequired,
onSelected: PropTypes.func.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
// Whether to show the address on the address tiles
showAddress: React.PropTypes.bool,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
showAddress: PropTypes.bool,
truncateAt: PropTypes.number.isRequired,
selected: PropTypes.number,
// Element to put as a header on top of the list
header: React.PropTypes.node,
header: PropTypes.node,
},
getInitialState: function() {

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
@ -28,9 +29,9 @@ export default React.createClass({
propTypes: {
address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,
canDismiss: PropTypes.bool,
onDismissed: PropTypes.func,
justified: PropTypes.bool,
},
getDefaultProps: function() {

View file

@ -19,6 +19,7 @@ limitations under the License.
import url from 'url';
import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
@ -463,6 +464,10 @@ export default class AppTile extends React.Component {
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation";
// Additional iframe feature pemissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media;";
if (this.props.show) {
const loadingElement = (
<div className='mx_AppTileBody mx_AppLoading'>
@ -482,7 +487,13 @@ export default class AppTile extends React.Component {
appTileBody = (
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
{ this.state.loading && loadingElement }
{ /*
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
"allow" attribute, which is unknown to react 15.
*/ }
<iframe
is
allow={iframeFeatures}
ref="appFrame"
src={this._getSafeUrl()}
allowFullScreen="true"

View file

@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DragSource, DropTarget } from 'react-dnd';
import TagTile from './TagTile';
import dis from '../../../dispatcher';
import { findDOMNode } from 'react-dom';
const tagTileSource = {
canDrag: function(props, monitor) {
return true;
},
import { Draggable } from 'react-beautiful-dnd';
beginDrag: function(props) {
// Return the data describing the dragged item
return {
tag: props.groupProfile.groupId,
};
},
endDrag: function(props, monitor, component) {
const dropResult = monitor.getDropResult();
if (!monitor.didDrop() || !dropResult) {
return;
}
props.onEndDrag();
},
};
const tagTileTarget = {
canDrop(props, monitor) {
return true;
},
hover(props, monitor, component) {
if (!monitor.canDrop()) return;
const draggedY = monitor.getClientOffset().y;
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
const targetY = (top + bottom) / 2;
dis.dispatch({
action: 'order_tag',
tag: monitor.getItem().tag,
targetTag: props.groupProfile.groupId,
// Note: we indicate that the tag should be after the target when
// it's being dragged over the top half of the target.
after: draggedY < targetY,
});
},
drop(props) {
// Return the data to be returned by getDropResult
return {
tag: props.groupProfile.groupId,
};
},
};
export default
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
}))((props) => {
const { connectDropTarget, connectDragSource, ...otherProps } = props;
return connectDropTarget(connectDragSource(
<div>
<TagTile {...otherProps} />
</div>,
));
}));
export default function DNDTagTile(props) {
return <div>
<Draggable
key={props.tag}
draggableId={props.tag}
index={props.index}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile {...props} />
</div>
{ provided.placeholder }
</div>
) }
</Draggable>
</div>;
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
@ -24,8 +25,8 @@ export default React.createClass({
displayName: 'DeviceVerifyButtons',
propTypes: {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
},
getInitialState: function() {

View file

@ -0,0 +1,62 @@
/*
Copyright 2017 Aidan Gauland
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler';
/**
* Basic container for buttons in modal dialogs.
*/
module.exports = React.createClass({
displayName: "DialogButtons",
propTypes: {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
// onClick handler for the cancel button.
onCancel: PropTypes.func.isRequired,
focus: PropTypes.bool,
},
render: function() {
let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
return (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
</div>
);
},
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class DirectorySearchBox extends React.Component {
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
}
DirectorySearchBox.propTypes = {
className: React.PropTypes.string,
onChange: React.PropTypes.func,
onClear: React.PropTypes.func,
onJoinClick: React.PropTypes.func,
placeholder: React.PropTypes.string,
showJoinButton: React.PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
}
MenuOption.propTypes = {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node,
children: PropTypes.oneOfType([
PropTypes.arrayOf(React.PropTypes.node),
PropTypes.node,
]),
highlighted: React.PropTypes.bool,
dropdownKey: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
onMouseEnter: React.PropTypes.func.isRequired,
highlighted: PropTypes.bool,
dropdownKey: PropTypes.string,
onClick: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
};
/*
@ -322,20 +323,20 @@ Dropdown.propTypes = {
// The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this
// width.
menuWidth: React.PropTypes.number,
menuWidth: PropTypes.number,
// Called when the selected option changes
onOptionChange: React.PropTypes.func.isRequired,
onOptionChange: PropTypes.func.isRequired,
// Called when the value of the search field changes
onSearchChange: React.PropTypes.func,
searchEnabled: React.PropTypes.bool,
onSearchChange: PropTypes.func,
searchEnabled: PropTypes.bool,
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as
// in the dropped-down menu.
getShortOption: React.PropTypes.func,
value: React.PropTypes.string,
getShortOption: PropTypes.func,
value: PropTypes.string,
// negative for consistency with HTML
disabled: React.PropTypes.bool,
disabled: PropTypes.bool,
};

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
const KEY_TAB = 9;
const KEY_SHIFT = 16;
@ -26,18 +27,18 @@ module.exports = React.createClass({
displayName: 'EditableText',
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
label: React.PropTypes.string,
placeholder: React.PropTypes.string,
className: React.PropTypes.string,
labelClassName: React.PropTypes.string,
placeholderClassName: React.PropTypes.string,
onValueChanged: PropTypes.func,
initialValue: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
className: PropTypes.string,
labelClassName: PropTypes.string,
placeholderClassName: PropTypes.string,
// Overrides blurToSubmit if true
blurToCancel: React.PropTypes.bool,
blurToCancel: PropTypes.bool,
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: React.PropTypes.bool,
editable: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
editable: PropTypes.bool,
},
Phases: {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Promise from 'bluebird';
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func,
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: React.PropTypes.string,
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: React.PropTypes.string,
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: React.PropTypes.func,
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: React.PropTypes.bool,
blurToSubmit: PropTypes.bool,
};

View file

@ -16,6 +16,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
@ -32,8 +33,8 @@ export default function EmojiText(props) {
}
EmojiText.propTypes = {
element: React.PropTypes.string,
children: React.PropTypes.string.isRequired,
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {

View file

@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
};
FlairAvatar.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
export default class Flair extends React.Component {
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
}
const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) {
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
this.setState({
profiles: profiles.filter((profile) => {
return profile ? profile.avatarUrl : false;
}),
});
}
}
@ -134,5 +138,5 @@ Flair.propTypes = {
// this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import * as languageHandler from '../../../languageHandler';
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
@ -23,19 +24,19 @@ module.exports = React.createClass({
propTypes: {
// An array of member events to summarise
events: React.PropTypes.array.isRequired,
events: PropTypes.array.isRequired,
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
children: PropTypes.array.isRequired,
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength: React.PropTypes.number,
summaryLength: PropTypes.number,
// The maximum number of avatars to display in the summary
avatarsMaxLength: React.PropTypes.number,
avatarsMaxLength: PropTypes.number,
// The minimum number of events needed to trigger summarisation
threshold: React.PropTypes.number,
threshold: PropTypes.number,
// Called when the MELS expansion is toggled
onToggle: React.PropTypes.func,
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: React.PropTypes.bool,
startExpanded: PropTypes.bool,
},
getInitialState: function() {

View file

@ -17,7 +17,7 @@ import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
@ -61,6 +61,17 @@ const Pill = React.createClass({
shouldShowPillAvatar: PropTypes.bool,
},
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() {
return {
// ID/alias of the room/user
@ -135,6 +146,7 @@ const Pill = React.createClass({
componentWillMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props);
},

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
@ -24,23 +25,23 @@ module.exports = React.createClass({
displayName: 'PowerSelector',
propTypes: {
value: React.PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
maxValue: PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
usersDefault: PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled.
//
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
controlled: PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
disabled: PropTypes.bool,
onChange: PropTypes.func,
},
getInitialState: function() {

View file

@ -17,12 +17,13 @@ limitations under the License.
'use strict';
const React = require('react');
import PropTypes from 'prop-types';
module.exports = React.createClass({
displayName: 'ProgressBar',
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number,
value: PropTypes.number,
max: PropTypes.number,
},
render: function() {

View file

@ -0,0 +1,188 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink} from "../../../matrix-to";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component {
static isMessageUrl(url) {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
}
static childContextTypes = {
matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
};
static propTypes = {
// The matrix.to url of the event
url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event
parentEv: PropTypes.instanceOf(MatrixEvent),
};
constructor(props, context) {
super(props, context);
this.state = {
// The event related to this quote and their nested rich quotes
events: [],
// Whether the top (oldest) event should be shown or spoilered
show: true,
// Whether an error was encountered fetching nested older event, show node if it does
err: false,
};
this.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
};
}
parseUrl(url) {
if (!url) return;
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
const [, roomIdentifier, eventId] = matrixToMatch;
return {roomIdentifier, eventId};
}
componentWillReceiveProps(nextProps) {
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
if (!roomIdentifier || !eventId) return;
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
this.setState({ events: [] });
if (room) this.getEvent(room, eventId, true);
}
componentWillMount() {
this.componentWillReceiveProps(this.props);
}
getRoom(id) {
const cli = MatrixClientPeg.get();
if (id[0] === '!') return cli.getRoom(id);
return cli.getRooms().find((r) => {
return r.getAliases().includes(id);
});
}
async getEvent(room, eventId, show) {
const event = room.findEventById(eventId);
if (event) {
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
this.addEvent(room.findEventById(eventId), show);
}
addEvent(event, show) {
const events = [event].concat(this.state.events);
this.setState({events, show});
}
// addRichQuote(roomId, eventId) {
addRichQuote(href) {
const {roomIdentifier, eventId} = this.parseUrl(href);
if (!roomIdentifier || !eventId) {
this.setState({ err: true });
return;
}
const room = this.getRoom(roomIdentifier);
if (!room) {
this.setState({ err: true });
return;
}
this.getEvent(room, eventId, false);
}
onQuoteClick() {
this.setState({ show: true });
}
render() {
const events = this.state.events.slice();
if (events.length) {
const evTiles = [];
if (!this.state.show) {
const oldestEv = events.shift();
const Pill = sdk.getComponent('elements.Pill');
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
evTiles.push(<blockquote className="mx_Quote" key="load">
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
})
}
</blockquote>);
}
const EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
}
// Deliberately render nothing if the URL isn't recognised
// in case we get an undefined/falsey node, replace it with null to make React happy
return this.props.node || null;
}
}

View file

@ -15,23 +15,24 @@ limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
name: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
roomId: PropTypes.string, // for per-room settings
label: PropTypes.string, // untranslated
onChange: PropTypes.func,
isExplicit: PropTypes.bool,
manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
group: PropTypes.string,
value: PropTypes.any, // the value for the radio button
},
getInitialState: function() {

View file

@ -20,36 +20,97 @@ import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import ContextualMenu from '../../structures/ContextualMenu';
import FlairStore from '../../../stores/FlairStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
displayName: 'TagTile',
propTypes: {
groupProfile: PropTypes.object,
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
componentWillMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
}
},
componentWillUnmount() {
this.unmounted = true;
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.groupProfile.groupId,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onContextButtonClick: function(e) {
e.preventDefault();
e.stopPropagation();
// Hide the (...) immediately
this.setState({ hover: false });
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = elementRect.right + window.pageXOffset + 3;
const chevronOffset = 12;
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
const self = this;
ContextualMenu.createMenu(TagTileContextMenu, {
chevronOffset: chevronOffset,
left: x,
top: y,
tag: this.props.tag,
onFinished: function() {
self.setState({ menuDisplayed: false });
},
});
this.setState({ menuDisplayed: true });
},
onMouseOver: function() {
this.setState({hover: true});
},
@ -62,8 +123,8 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.props.groupProfile || {};
const name = profile.name || profile.groupId;
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
@ -78,10 +139,15 @@ export default React.createClass({
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
{ "\u00B7\u00B7\u00B7" }
</div> : <div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
{ contextButton }
</div>
</AccessibleButton>;
},

View file

@ -18,16 +18,17 @@ limitations under the License.
const React = require('react');
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const Tinter = require("../../../Tinter");
var TintableSvg = React.createClass({
displayName: 'TintableSvg',
propTypes: {
src: React.PropTypes.string.isRequired,
width: React.PropTypes.string.isRequired,
height: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
},
statics: {

View file

@ -17,14 +17,15 @@ limitations under the License.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'UserSelector',
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
onChange: PropTypes.func,
selected_users: PropTypes.arrayOf(React.PropTypes.string),
},
getDefaultProps: function() {