Merge branch 'develop' into matthew/low_bandwidth
This commit is contained in:
commit
d81804e0fe
589 changed files with 37701 additions and 15344 deletions
|
@ -144,12 +144,16 @@ module.exports = React.createClass({
|
|||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, 'mx_IntegrationsManager');
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, 'mx_IntegrationsManager');
|
||||
}, (err) => {
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
|
|
@ -60,18 +60,22 @@ export default class Autocomplete extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps, state) {
|
||||
if (this.props.room.roomId !== newProps.room.roomId) {
|
||||
componentDidMount() {
|
||||
this._applyNewProps();
|
||||
}
|
||||
|
||||
_applyNewProps(oldQuery, oldRoom) {
|
||||
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
|
||||
this.autocompleter.destroy();
|
||||
this.autocompleter = new Autocompleter(newProps.room);
|
||||
this.autocompleter = new Autocompleter(this.props.room);
|
||||
}
|
||||
|
||||
// Query hasn't changed so don't try to complete it
|
||||
if (newProps.query === this.props.query) {
|
||||
if (oldQuery === this.props.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.complete(newProps.query, newProps.selection);
|
||||
this.complete(this.props.query, this.props.selection);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -233,7 +237,8 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps) {
|
||||
this._applyNewProps(prevProps.query, prevProps.room);
|
||||
// this is the selected completion, so scroll it into view if needed
|
||||
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
||||
if (selectedCompletion && this.container) {
|
||||
|
@ -251,8 +256,6 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 1;
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
|
@ -277,7 +280,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
|
||||
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
) : null;
|
||||
|
@ -298,6 +301,9 @@ Autocomplete.propTypes = {
|
|||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
|
||||
// method invoked when selected (if any) completion changes
|
||||
onSelectionChange: PropTypes.func,
|
||||
|
||||
// The room in which we're autocompleting
|
||||
room: PropTypes.instanceOf(Room),
|
||||
};
|
||||
|
|
|
@ -122,7 +122,9 @@ module.exports = React.createClass({
|
|||
const severity = ev.getContent().severity || "normal";
|
||||
const stateKey = ev.getStateKey();
|
||||
|
||||
if (title && value && severity) {
|
||||
// We want a non-empty title but can accept falsey values (e.g.
|
||||
// zero)
|
||||
if (title && value !== undefined) {
|
||||
counters.push({
|
||||
"title": title,
|
||||
"value": value,
|
||||
|
@ -211,35 +213,33 @@ module.exports = React.createClass({
|
|||
const severity = counter.severity;
|
||||
const stateKey = counter.stateKey;
|
||||
|
||||
if (title && value && severity) {
|
||||
let span = <span>{ title }: { value }</span>
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
{ span }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
let span = <span>{ title }: { value }</span>
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_span"
|
||||
data-severity={severity}
|
||||
key={ "x-" + stateKey }
|
||||
>
|
||||
{span}
|
||||
</span>
|
||||
);
|
||||
|
||||
counters.push(span);
|
||||
counters.push(
|
||||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_delim"
|
||||
key={"delim" + idx}
|
||||
> ─ </span>
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
{ span }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
span = (
|
||||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_span"
|
||||
data-severity={severity}
|
||||
key={ "x-" + stateKey }
|
||||
>
|
||||
{span}
|
||||
</span>
|
||||
);
|
||||
|
||||
counters.push(span);
|
||||
counters.push(
|
||||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_delim"
|
||||
key={"delim" + idx}
|
||||
> ─ </span>
|
||||
);
|
||||
});
|
||||
|
||||
if (counters.length > 0) {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
export default function(props) {
|
||||
const isWarning = props.status === "warning";
|
||||
|
@ -35,5 +36,10 @@ export default function(props) {
|
|||
_t("All devices for this user are trusted") :
|
||||
_t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
return (<div className={e2eIconClasses} title={e2eTitle} />);
|
||||
const icon = (<div className={e2eIconClasses} title={e2eTitle} />);
|
||||
if (props.onClick) {
|
||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
||||
} else {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,10 @@ limitations under the License.
|
|||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require('../../../index');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
|
||||
|
||||
const PRESENCE_CLASS = {
|
||||
|
@ -97,62 +96,55 @@ const EntityTile = React.createClass({
|
|||
return this.props.shouldComponentUpdate(nextProps, nextState);
|
||||
},
|
||||
|
||||
mouseEnter: function(e) {
|
||||
this.setState({ 'hover': true });
|
||||
},
|
||||
|
||||
mouseLeave: function(e) {
|
||||
this.setState({ 'hover': false });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const mainClassNames = {
|
||||
"mx_EntityTile": true,
|
||||
"mx_EntityTile_noHover": this.props.suppressOnHover,
|
||||
};
|
||||
if (this.props.className) mainClassNames[this.props.className] = true;
|
||||
|
||||
const presenceClass = presenceClassForMember(
|
||||
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
|
||||
);
|
||||
mainClassNames[presenceClass] = true;
|
||||
|
||||
let mainClassName = "mx_EntityTile ";
|
||||
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
|
||||
let nameEl;
|
||||
const {name} = this.props;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
if (this.state.hover && !this.props.suppressOnHover) {
|
||||
if (!this.props.suppressOnHover) {
|
||||
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||
|
||||
mainClassName += " mx_EntityTile_hover";
|
||||
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
let presenceLabel = null;
|
||||
let nameClasses = 'mx_EntityTile_name';
|
||||
if (this.props.showPresence) {
|
||||
presenceLabel = <PresenceLabel activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
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">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{ name }
|
||||
</EmojiText>
|
||||
</div>
|
||||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.subtextLabel) {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">
|
||||
<div className="mx_EntityTile_name" dir="auto">
|
||||
{name}
|
||||
</EmojiText>
|
||||
</div>
|
||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
nameEl = (
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText>
|
||||
<div className="mx_EntityTile_name" dir="auto">{ name }</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -183,17 +175,19 @@ const EntityTile = React.createClass({
|
|||
|
||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
<AccessibleButton className={mainClassName} title={this.props.title}
|
||||
onClick={this.props.onClick} onMouseEnter={this.mouseEnter}
|
||||
onMouseLeave={this.mouseLeave}>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{ av }
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
{ inviteButton }
|
||||
</AccessibleButton>
|
||||
<div ref={(c) => this.container = c} >
|
||||
<AccessibleButton className={classNames(mainClassNames)} title={this.props.title}
|
||||
onClick={this.props.onClick}>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{ av }
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
{ inviteButton }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
|
||||
const React = require('react');
|
||||
|
@ -30,9 +29,7 @@ const sdk = require('../../../index');
|
|||
const TextForEvent = require('../../../TextForEvent');
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
|
@ -63,6 +60,9 @@ const stateEventTileTypes = {
|
|||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
'm.room.related_groups': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
function getHandlerTile(ev) {
|
||||
|
@ -127,7 +127,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
isSelectedEvent: PropTypes.bool,
|
||||
|
||||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: PropTypes.func,
|
||||
onHeightChanged: PropTypes.func,
|
||||
|
||||
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
|
||||
readReceipts: PropTypes.arrayOf(React.PropTypes.object),
|
||||
|
@ -159,19 +159,25 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// helper function to access relations for this event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
// no-op function because onWidgetLoad is optional yet some sub-components assume its existence
|
||||
onWidgetLoad: function() {},
|
||||
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
||||
onHeightChanged: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// Whether the context menu is being displayed.
|
||||
menu: false,
|
||||
// Whether the action bar is focused.
|
||||
actionBarFocused: false,
|
||||
// Whether all read receipts are being displayed. If not, only display
|
||||
// a truncation of them.
|
||||
allReadAvatars: false,
|
||||
|
@ -179,6 +185,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
verified: null,
|
||||
// Whether onRequestKeysClick has been called since mounting.
|
||||
previouslyRequestedKeys: false,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: this.getReactions(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -190,9 +198,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
this.props.matrixClient.on("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
const client = this.props.matrixClient;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
|
@ -215,13 +226,16 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const client = this.props.matrixClient;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
||||
/** called when the event is decrypted after we show it.
|
||||
*/
|
||||
_onDecrypted: function() {
|
||||
// we need to re-verify the sending device.
|
||||
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption
|
||||
// (we call onHeightChanged in _verifyEvent to handle the case where decryption
|
||||
// has caused a change in size of the event tile)
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
this.forceUpdate();
|
||||
|
@ -243,7 +257,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
verified: verified,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onWidgetLoad();
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -307,31 +321,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return actions.tweaks.highlight;
|
||||
},
|
||||
|
||||
onEditClicked: function(e) {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const self = this;
|
||||
|
||||
const {tile, replyThread} = this.refs;
|
||||
|
||||
ContextualMenu.createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
onFinished: function() {
|
||||
self.setState({menu: false});
|
||||
},
|
||||
});
|
||||
this.setState({menu: true});
|
||||
},
|
||||
|
||||
toggleAllReadAvatars: function() {
|
||||
this.setState({
|
||||
allReadAvatars: !this.state.allReadAvatars,
|
||||
|
@ -483,6 +472,42 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return null;
|
||||
},
|
||||
|
||||
onActionBarFocusChange(focused) {
|
||||
this.setState({
|
||||
actionBarFocused: focused,
|
||||
});
|
||||
},
|
||||
|
||||
getTile() {
|
||||
return this.refs.tile;
|
||||
},
|
||||
|
||||
getReplyThread() {
|
||||
return this.refs.replyThread;
|
||||
},
|
||||
|
||||
getReactions() {
|
||||
if (
|
||||
!this.props.showReactions ||
|
||||
!this.props.getRelationsForEvent ||
|
||||
!SettingsStore.isFeatureEnabled("feature_reactions")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||
},
|
||||
|
||||
_onReactionsCreated(relationType, eventType) {
|
||||
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
|
||||
return;
|
||||
}
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||
this.setState({
|
||||
reactions: this.getReactions(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
|
@ -499,7 +524,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
|
||||
);
|
||||
|
||||
const tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
let tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
if (!tileHandler && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
tileHandler = "messages.ViewSourceEvent";
|
||||
}
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!tileHandler) {
|
||||
|
@ -519,6 +547,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_isEditing: this.props.isEditing,
|
||||
mx_EventTile_info: isInfoMessage,
|
||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
|
||||
|
@ -529,7 +558,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
||||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
menu: this.state.menu,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: this.state.verified === true,
|
||||
mx_EventTile_unverified: this.state.verified === false,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
|
@ -537,7 +566,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
|
||||
let permalink = "#";
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
const readAvatars = this.getReadAvatars();
|
||||
|
||||
|
@ -592,9 +624,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const editButton = (
|
||||
<span className="mx_EventTile_editButton" title={_t("Options")} onClick={this.onEditClicked} />
|
||||
);
|
||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||
const actionBar = !this.props.isEditing ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
/> : undefined;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
@ -633,16 +671,24 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<ToolTipButton helpText={keyRequestHelpText} />
|
||||
</div> : null;
|
||||
|
||||
let reactionsRow;
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
|
||||
reactionsRow = <ReactionsRow
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
/>;
|
||||
}
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</EmojiText>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -657,7 +703,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
onHeightChanged={this.props.onHeightChanged} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -672,7 +718,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
onHeightChanged={this.props.onHeightChanged} />
|
||||
</div>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
|
@ -690,6 +736,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
case 'reply':
|
||||
case 'reply_preview': {
|
||||
let thread;
|
||||
if (this.props.tileShape === 'reply_preview') {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ avatar }
|
||||
|
@ -699,21 +754,24 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{
|
||||
this.props.tileShape === 'reply_preview'
|
||||
&& ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
|
||||
}
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onWidgetLoad={this.props.onWidgetLoad}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
showUrlPreview={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
|
@ -725,18 +783,21 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
onHeightChanged={this.props.onHeightChanged} />
|
||||
{ keyRequestInfo }
|
||||
{ editButton }
|
||||
{ reactionsRow }
|
||||
{ actionBar }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
|
@ -771,29 +832,31 @@ module.exports.haveTileForEvent = function(e) {
|
|||
|
||||
function E2ePadlockUndecryptable(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" />
|
||||
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnverified(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" />
|
||||
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnencrypted(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" />
|
||||
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlock(props) {
|
||||
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||
return <div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
|
||||
title={props.title} onClick={props.onClick} />;
|
||||
return (<div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
|
||||
title={props.title} onClick={props.onClick} />);
|
||||
} else {
|
||||
return <div className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" onClick={props.onClick} />;
|
||||
return (<div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden mx_EventTile_e2eIcon_${props.icon}`}
|
||||
onClick={props.onClick} />);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,19 +16,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const ImageUtils = require('../../../ImageUtils');
|
||||
const Modal = require('../../../Modal');
|
||||
|
||||
const linkify = require('linkifyjs');
|
||||
const linkifyElement = require('linkifyjs/element');
|
||||
const linkifyMatrix = require('../../../linkify-matrix');
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LinkPreviewWidget',
|
||||
|
||||
|
@ -36,7 +32,7 @@ module.exports = React.createClass({
|
|||
link: PropTypes.string.isRequired, // the URL being previewed
|
||||
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
|
||||
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
|
||||
onWidgetLoad: PropTypes.func, // called when the preview's contents has loaded
|
||||
onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -53,22 +49,22 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.setState(
|
||||
{ preview: res },
|
||||
this.props.onWidgetLoad,
|
||||
this.props.onHeightChanged,
|
||||
);
|
||||
}, (error)=>{
|
||||
console.error("Failed to get preview for " + this.props.link + " " + error);
|
||||
console.error("Failed to get URL preview: " + error);
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.description);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.description);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(),
|
||||
});
|
||||
const indicator = (<div className={iconClasses} />);
|
||||
const deviceName = this.props.device.ambiguous ?
|
||||
const deviceName = (this.props.device.ambiguous || this.props.showDeviceId) ?
|
||||
(this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" :
|
||||
this.props.device.getDisplayName();
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -588,6 +589,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
can.kick = me.powerLevel >= powerLevels.kick;
|
||||
can.ban = me.powerLevel >= powerLevels.ban;
|
||||
can.invite = me.powerLevel >= powerLevels.invite;
|
||||
can.mute = me.powerLevel >= editPowerLevel;
|
||||
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
|
||||
can.modifyLevelMax = me.powerLevel;
|
||||
|
@ -726,7 +728,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!member || !member.membership || member.membership === 'leave') {
|
||||
if (this.state.can.invite && (!member || !member.membership || member.membership === 'leave')) {
|
||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
|
||||
const onInviteUserButton = async () => {
|
||||
try {
|
||||
|
@ -734,8 +736,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// 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));
|
||||
if (inviter.getCompletionState(member.userId) !== "invited")
|
||||
throw new Error(inviter.getErrorText(member.userId));
|
||||
});
|
||||
} catch (err) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
|
@ -941,24 +943,29 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
let roomMemberDetails = null;
|
||||
let e2eIconElement;
|
||||
|
||||
if (this.props.member.roomId) { // is in room
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
roomMemberDetails = <div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ _t("Level:") } <b>
|
||||
<PowerSelector controlled={true}
|
||||
value={parseInt(this.props.member.powerLevel)}
|
||||
maxValue={this.state.can.modifyLevelMax}
|
||||
disabled={!this.state.can.modifyLevel}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={this.onPowerChange} />
|
||||
</b>
|
||||
<PowerSelector
|
||||
value={parseInt(this.props.member.powerLevel)}
|
||||
maxValue={this.state.can.modifyLevelMax}
|
||||
disabled={!this.state.can.modifyLevel}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={this.onPowerChange} />
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const isEncrypted = this.props.matrixClient.isRoomEncrypted(this.props.member.roomId);
|
||||
if (this.state.e2eStatus && isEncrypted) {
|
||||
e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />);
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = this.props.member.getMxcAvatarUrl();
|
||||
|
@ -967,20 +974,25 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let backButton;
|
||||
if (this.props.member.roomId) {
|
||||
backButton = (<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
title={_t('Close')}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
{ this.state.e2eStatus ? <E2EIcon status={this.state.e2eStatus} isUser={true} /> : undefined }
|
||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
||||
{ backButton }
|
||||
{ e2eIconElement }
|
||||
<h2>{ memberName }</h2>
|
||||
</div>
|
||||
{ avatarElement }
|
||||
<div className="mx_MemberInfo_container">
|
||||
|
@ -992,7 +1004,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ roomMemberDetails }
|
||||
</div>
|
||||
</div>
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberInfo_scrollContainer">
|
||||
<AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
|
||||
<div className="mx_MemberInfo_container">
|
||||
{ this._renderUserOptions() }
|
||||
|
||||
|
@ -1004,7 +1016,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
{ spinner }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -20,6 +20,8 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
|
@ -339,12 +341,18 @@ module.exports = React.createClass({
|
|||
return nameA.localeCompare(nameB);
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(ev) {
|
||||
const q = ev.target.value;
|
||||
onSearchQueryChanged: function(searchQuery) {
|
||||
this.setState({
|
||||
searchQuery: q,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q),
|
||||
searchQuery,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
||||
});
|
||||
},
|
||||
|
||||
_onPending3pidInviteClick: function(inviteEvent) {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
event: inviteEvent,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -373,11 +381,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (room) {
|
||||
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
|
||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
|
||||
for (let i = 0; i < requiredKeys.length; ++i) {
|
||||
if (e.getContent()[requiredKeys[i]] === undefined) return false;
|
||||
}
|
||||
if (!isValid3pidInvite(e)) return false;
|
||||
|
||||
// discard all invites which have a m.room.member event since we've
|
||||
// already added them.
|
||||
|
@ -409,6 +413,7 @@ module.exports = React.createClass({
|
|||
return <EntityTile key={e.getStateKey()}
|
||||
name={e.getContent().display_name}
|
||||
suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(e)}
|
||||
/>;
|
||||
}));
|
||||
}
|
||||
|
@ -438,17 +443,29 @@ module.exports = React.createClass({
|
|||
return <div className="mx_MemberList"><Spinner /></div>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
// assume we can invite until proven false
|
||||
let canInvite = true;
|
||||
|
||||
const plEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const me = room.getMember(cli.getUserId());
|
||||
if (plEvent && me) {
|
||||
const content = plEvent.getContent();
|
||||
if (content && content.invite > me.powerLevel) {
|
||||
canInvite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick}>
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
|
||||
<span>{ _t('Invite to this room') }</span>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
@ -467,7 +484,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inviteButton }
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AutoHideScrollbar>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
|
@ -476,10 +493,11 @@ module.exports = React.createClass({
|
|||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
<input className="mx_MemberList_query mx_textinput_icon mx_textinput_search" id="mx_MemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter room members')} />
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t('Filter room members') }
|
||||
onSearch={ this.onSearchQueryChanged } />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
|||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
},
|
||||
|
||||
componentWillUmount() {
|
||||
componentWillUnmount() {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
|
@ -139,11 +139,21 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.member_last_modified_time = member.getLastModifiedTime();
|
||||
|
||||
// We deliberately leave power levels that are not 100 or 50 undefined
|
||||
const powerStatus = {
|
||||
100: EntityTile.POWER_STATUS_ADMIN,
|
||||
50: EntityTile.POWER_STATUS_MODERATOR,
|
||||
}[this.props.member.powerLevel];
|
||||
const powerStatusMap = new Map([
|
||||
[100, EntityTile.POWER_STATUS_ADMIN],
|
||||
[50, EntityTile.POWER_STATUS_MODERATOR],
|
||||
]);
|
||||
|
||||
// Find the nearest power level with a badge
|
||||
let powerLevel = this.props.member.powerLevel;
|
||||
for (const [pl] of powerStatusMap) {
|
||||
if (this.props.member.powerLevel >= pl) {
|
||||
powerLevel = pl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const powerStatus = powerStatusMap.get(powerLevel);
|
||||
|
||||
return (
|
||||
<EntityTile {...this.props} presenceState={presenceState}
|
||||
|
|
|
@ -26,6 +26,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
|||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../matrix-to';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
@ -41,15 +42,159 @@ const formatButtonList = [
|
|||
_td("numbered-list"),
|
||||
];
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
return <div className="mx_MessageComposer_avatar">
|
||||
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
ComposerAvatar.propTypes = {
|
||||
me: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function CallButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onVoiceCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_voicecall"
|
||||
onClick={onVoiceCallClick}
|
||||
title={_t('Voice call')}
|
||||
/>
|
||||
}
|
||||
|
||||
CallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
function VideoCallButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_videocall"
|
||||
onClick={onCallClick}
|
||||
title={_t('Video call')}
|
||||
/>;
|
||||
}
|
||||
|
||||
VideoCallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function HangupButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onHangupClick = () => {
|
||||
const call = CallHandler.getCallForRoom(props.roomId);
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
});
|
||||
};
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
|
||||
onClick={onHangupClick}
|
||||
title={_t('Hangup')}
|
||||
/>;
|
||||
}
|
||||
|
||||
HangupButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
function FormattingButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton
|
||||
element="img"
|
||||
className="mx_MessageComposer_formatting"
|
||||
alt={_t("Show Text Formatting Toolbar")}
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src={require("../../../../res/img/button-text-formatting.svg")}
|
||||
style={{visibility: props.showFormatting ? 'hidden' : 'visible'}}
|
||||
onClick={props.onClickHandler}
|
||||
/>;
|
||||
}
|
||||
|
||||
FormattingButton.propTypes = {
|
||||
showFormatting: PropTypes.bool.isRequired,
|
||||
onClickHandler: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
class UploadButton extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
this.refs.uploadInput.click();
|
||||
}
|
||||
|
||||
onUploadFileInputChange(ev) {
|
||||
if (ev.target.files.length === 0) return;
|
||||
|
||||
// take a copy so we can safely reset the value of the form control
|
||||
// (Note it is a FileList: we can't use slice or sesnible iteration).
|
||||
const tfiles = [];
|
||||
for (let i = 0; i < ev.target.files.length; ++i) {
|
||||
tfiles.push(ev.target.files[i]);
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
tfiles, this.props.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
|
||||
// This is the onChange handler for a file form control, but we're
|
||||
// not keeping any state, so reset the value of the form control
|
||||
// to empty.
|
||||
// NB. we need to set 'value': the 'files' property is immutable.
|
||||
ev.target.value = '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick}
|
||||
title={_t('Upload file')}
|
||||
>
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileInputChange}
|
||||
/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||
|
@ -58,6 +203,8 @@ export default class MessageComposer extends React.Component {
|
|||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||
this.renderFormatBar = this.renderFormatBar.bind(this);
|
||||
|
||||
this.state = {
|
||||
inputState: {
|
||||
|
@ -68,6 +215,7 @@ export default class MessageComposer extends React.Component {
|
|||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -120,6 +268,9 @@ export default class MessageComposer extends React.Component {
|
|||
if (ev.getType() === 'm.room.tombstone') {
|
||||
this.setState({tombstone: this._getRoomTombstone()});
|
||||
}
|
||||
if (ev.getType() === 'm.room.power_levels') {
|
||||
this.setState({canSendMessages: this.props.room.maySendMessage()});
|
||||
}
|
||||
}
|
||||
|
||||
_getRoomTombstone() {
|
||||
|
@ -132,131 +283,10 @@ export default class MessageComposer extends React.Component {
|
|||
this.setState({ isQuoting });
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs.uploadInput.click();
|
||||
}
|
||||
|
||||
onUploadFileSelected(files) {
|
||||
const tfiles = files.target.files;
|
||||
this.uploadFiles(tfiles);
|
||||
}
|
||||
|
||||
uploadFiles(files) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
const fileList = [];
|
||||
const acceptedFiles = [];
|
||||
const failedFiles = [];
|
||||
|
||||
for (let i=0; i<files.length; i++) {
|
||||
const fileAcceptedOrError = this.props.uploadAllowed(files[i]);
|
||||
if (fileAcceptedOrError === true) {
|
||||
acceptedFiles.push(<li key={i}>
|
||||
<TintableSvg key={i} src={require("../../../../res/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={require("../../../../res/img/files.svg")} width="16" height="16" /> { files[i].name || _t('Attachment') } <p>{ _t('Reason') + ": " + fileAcceptedOrError}</p>
|
||||
</li>);
|
||||
}
|
||||
}
|
||||
|
||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
let replyToWarning = null;
|
||||
if (isQuoting) {
|
||||
replyToWarning = <p>{
|
||||
_t('At this time it is not possible to reply with a file so this will be sent without being a reply.')
|
||||
}</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>
|
||||
{ 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 (fileList) {
|
||||
for (let i=0; i<fileList.length; i++) {
|
||||
this.props.uploadFile(fileList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.refs.uploadInput.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onHangupClick() {
|
||||
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
onCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
// Merge the new input state with old to support partial updates
|
||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||
this.setState({inputState});
|
||||
}
|
||||
|
||||
|
@ -303,162 +333,117 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
|
||||
const controls = [];
|
||||
|
||||
if (this.state.me) {
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberStatusMessageAvatar member={this.state.me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.e2eStatus) {
|
||||
controls.push(<E2EIcon
|
||||
status={this.props.e2eStatus}
|
||||
key="e2eIcon"
|
||||
className="mx_MessageComposer_e2eIcon" />
|
||||
);
|
||||
}
|
||||
|
||||
let callButton;
|
||||
let videoCallButton;
|
||||
let hangupButton;
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
// Call buttons
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<AccessibleButton key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
<img src={require("../../../../res/img/hangup.svg")} alt={_t('Hangup')} title={_t('Hangup')} width="25" height="25" />
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
callButton =
|
||||
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
|
||||
<TintableSvg src={require("../../../../res/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={require("../../../../res/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>";
|
||||
renderPlaceholderText() {
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} 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}`;
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
}
|
||||
} else {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
}
|
||||
}
|
||||
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) {
|
||||
renderFormatBar() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const {marks, blockType} = this.state.inputState;
|
||||
const formatButtons = formatButtonList.map((name) => {
|
||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
||||
const markName = name === 'inline-code' ? 'code' : name;
|
||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
||||
const suffix = active ? '-on' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return (
|
||||
<img className={className}
|
||||
title={_t(name)}
|
||||
onMouseDown={onFormatButtonClicked}
|
||||
key={name}
|
||||
src={require(`../../../../res/img/button-text-${name}${suffix}.svg`)}
|
||||
height="17"
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<AccessibleButton
|
||||
className="mx_MessageComposer_formatbar_markdown mx_MessageComposer_markdownDisabled"
|
||||
onClick={this.onToggleMarkdownClicked}
|
||||
title={_t("Markdown is disabled")}
|
||||
/>
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/icon-text-cancel.svg")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
this.props.e2eStatus ? <E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> : null,
|
||||
];
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
const uploadButton = (
|
||||
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title={_t('Upload file')}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/paperclip.svg")} width="20" height="20" />
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileSelected} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
const formattingButton = this.state.inputState.isRichTextEnabled ? (
|
||||
<AccessibleButton element="img" className="mx_MessageComposer_formatting"
|
||||
alt={_t("Show Text Formatting Toolbar")}
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src={require("../../../../res/img/button-text-formatting.svg")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
|
||||
key="controls_formatting" />
|
||||
) : null;
|
||||
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
let placeholderText;
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
placeholderText = _t('Send an encrypted reply…');
|
||||
} else {
|
||||
placeholderText = _t('Send a reply (unencrypted)…');
|
||||
}
|
||||
} else {
|
||||
if (roomIsEncrypted) {
|
||||
placeholderText = _t('Send an encrypted message…');
|
||||
} else {
|
||||
placeholderText = _t('Send a message (unencrypted)…');
|
||||
}
|
||||
}
|
||||
|
||||
const stickerpickerButton = <Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />;
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
const showFormattingButton = this.state.inputState.isRichTextEnabled;
|
||||
const callInProgress = this.props.callState && this.props.callState !== 'ended';
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
videoCallButton,
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
onInputStateChanged={this.onInputStateChanged}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
showFormattingButton ? <FormattingButton key="controls_formatting"
|
||||
showFormatting={this.state.showFormatting} onClickHandler={this.onToggleFormattingClicked} /> : null,
|
||||
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
||||
callInProgress ? null : <CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? null : <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
const continuesLink = replacementRoomId ? (
|
||||
<a href={makeRoomPermalink(replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this._onTombstoneClick}
|
||||
>
|
||||
{_t("The conversation continues here.")}
|
||||
</a>
|
||||
) : '';
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{_t("This room has been replaced and is no longer active.")}
|
||||
</span><br />
|
||||
<a href={makeRoomPermalink(replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this._onTombstoneClick}
|
||||
>
|
||||
{_t("The conversation continues here.")}
|
||||
</a>
|
||||
{ continuesLink }
|
||||
</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') }
|
||||
|
@ -466,41 +451,7 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let formatBar;
|
||||
if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) {
|
||||
const {marks, blockType} = this.state.inputState;
|
||||
const formatButtons = formatButtonList.map((name) => {
|
||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
||||
const markName = name === 'inline-code' ? 'code' : name;
|
||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
||||
const suffix = active ? '-on' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return <img className={className}
|
||||
title={_t(name)}
|
||||
onMouseDown={onFormatButtonClicked}
|
||||
key={name}
|
||||
src={require(`../../../../res/img/button-text-${name}${suffix}.svg`)}
|
||||
height="17" />;
|
||||
},
|
||||
);
|
||||
|
||||
formatBar =
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichTextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={require(`../../../../res/img/button-md-${!this.state.inputState.isRichTextEnabled}.png`)} />
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/icon-text-cancel.svg")} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
|
||||
|
||||
const wrapperClasses = classNames({
|
||||
mx_MessageComposer_wrapper: true,
|
||||
|
@ -513,29 +464,19 @@ export default class MessageComposer extends React.Component {
|
|||
{ controls }
|
||||
</div>
|
||||
</div>
|
||||
{ formatBar }
|
||||
{ showFormatBar ? this.renderFormatBar() : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageComposer.propTypes = {
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: PropTypes.string,
|
||||
|
||||
// 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
|
||||
};
|
||||
|
|
|
@ -40,30 +40,28 @@ import Analytics from '../../../Analytics';
|
|||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
import Markdown from '../../../Markdown';
|
||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||
|
||||
import {
|
||||
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
||||
asciiList, mapUnicodeToShort, toShort,
|
||||
} from 'emojione';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -134,21 +132,14 @@ function rangeEquals(a: Range, b: Range): boolean {
|
|||
*/
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
static propTypes = {
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
onFilesPasted: PropTypes.func,
|
||||
|
||||
onInputStateChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -277,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
case 'emoji':
|
||||
// XXX: apparently you can't return plain strings from serializer rules
|
||||
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
||||
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
|
||||
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
|
||||
return <b>{ obj.data.get('emojiUnicode') }</b>;
|
||||
// So instead we temporarily wrap emoji from RTE in a span.
|
||||
return <span>{ obj.data.get('emojiUnicode') }</span>;
|
||||
}
|
||||
return this.renderNode({
|
||||
node: obj,
|
||||
|
@ -339,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -379,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||
forComposerQuote: true,
|
||||
returnString: true,
|
||||
emojiOne: false,
|
||||
});
|
||||
const fragment = this.html.deserialize(html);
|
||||
// FIXME: do we want to put in a permalink to the original quote here?
|
||||
|
@ -543,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
// The first matched group includes just the matched plaintext emoji
|
||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emojiMatch) {
|
||||
// plaintext -> hex unicode
|
||||
const emojiUc = asciiList[emojiMatch[1]];
|
||||
// hex unicode -> shortname -> actual unicode
|
||||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emoticonMatch) {
|
||||
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||
const unicodeEmoji = data ? data.unicode : '';
|
||||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
|
@ -566,54 +552,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
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
|
||||
// child text. This seems to happen due to selection getting
|
||||
// moved in the normalisation phase after calculating these changes
|
||||
if (editorState.selection.anchor.key &&
|
||||
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
|
||||
change = change.moveToStartOfNextText();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
||||
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
|
||||
let blockType = editorState.blocks.first().type;
|
||||
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
|
||||
|
@ -628,7 +566,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
const inputState = {
|
||||
marks: editorState.activeMarks,
|
||||
isRichTextEnabled: this.state.isRichTextEnabled,
|
||||
blockType,
|
||||
};
|
||||
this.props.onInputStateChanged(inputState);
|
||||
|
@ -686,20 +623,22 @@ export default class MessageComposerInput extends React.Component {
|
|||
enableRichtext(enabled: boolean) {
|
||||
if (enabled === this.state.isRichTextEnabled) return;
|
||||
|
||||
let editorState = null;
|
||||
if (enabled) {
|
||||
editorState = this.mdToRichEditorState(this.state.editorState);
|
||||
} else {
|
||||
editorState = this.richToMdEditorState(this.state.editorState);
|
||||
}
|
||||
|
||||
Analytics.setRichtextMode(enabled);
|
||||
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, editorState),
|
||||
editorState: this.createEditorState(
|
||||
enabled,
|
||||
this.state.editorState,
|
||||
this.state.isRichTextEnabled,
|
||||
),
|
||||
isRichTextEnabled: enabled,
|
||||
}, ()=>{
|
||||
}, () => {
|
||||
this._editor.focus();
|
||||
if (this.props.onInputStateChanged) {
|
||||
this.props.onInputStateChanged({
|
||||
isRichTextEnabled: enabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||
|
@ -1012,7 +951,13 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
switch (transfer.type) {
|
||||
case 'files':
|
||||
return this.props.onFilesPasted(transfer.files);
|
||||
// This actually not so much for 'files' as such (at time of writing
|
||||
// neither chrome nor firefox let you paste a plain file copied
|
||||
// from Finder) but more images copied from a different website
|
||||
// / word processor etc.
|
||||
return ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
transfer.files, this.props.room.roomId, this.client,
|
||||
);
|
||||
case 'html': {
|
||||
if (this.state.isRichTextEnabled) {
|
||||
// FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means
|
||||
|
@ -1039,10 +984,17 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
handleReturn = (ev, change) => {
|
||||
if (ev.shiftKey) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
if (ev.shiftKey || (isMac && ev.altKey)) {
|
||||
return change.insertText('\n');
|
||||
}
|
||||
|
||||
if (this.autocomplete.countCompletions() > 0) {
|
||||
this.autocomplete.hide();
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
const editorState = this.state.editorState;
|
||||
|
||||
const lastBlock = editorState.blocks.last();
|
||||
|
@ -1084,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
|
@ -1162,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(
|
||||
editorState,
|
||||
this.state.isRichTextEnabled ? 'rich' : 'markdown',
|
||||
);
|
||||
|
||||
if (commandText && commandText.startsWith('/me')) {
|
||||
if (replyingToEv) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1194,7 +1140,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// Part of Replies fallback support - prepend the text we're sending
|
||||
// with the text we're replying to
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv, this.props.permalinkCreator);
|
||||
if (nestedReply) {
|
||||
if (content.formatted_body) {
|
||||
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||
|
@ -1241,14 +1187,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
} else {
|
||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||
}
|
||||
|
||||
const selected = this.selectHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.moveAutocompleteSelection(up);
|
||||
|
@ -1256,54 +1204,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
selectHistory = async (up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentlyComposedEditorState: this.state.editorState,
|
||||
});
|
||||
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.setState({
|
||||
editorState: this.state.currentlyComposedEditorState,
|
||||
});
|
||||
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let editorState;
|
||||
const historyItem = this.historyManager.getItem(delta);
|
||||
if (!historyItem) return;
|
||||
|
||||
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||
editorState = this.richToMdEditorState(historyItem.value);
|
||||
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||
editorState = this.mdToRichEditorState(historyItem.value);
|
||||
} else {
|
||||
editorState = historyItem.value;
|
||||
}
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like emoji
|
||||
// should already have been done and persisted in the history.
|
||||
editorState = change.value;
|
||||
|
||||
this.suppressAutoComplete = true;
|
||||
|
||||
this.setState({ editorState }, ()=>{
|
||||
this._editor.focus();
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
|
@ -1472,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
case 'emoji': {
|
||||
const { data } = node;
|
||||
const emojiUnicode = data.get('emojiUnicode');
|
||||
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
|
||||
const shortname = toShort(emojiUnicode);
|
||||
const className = classNames('mx_emojione', {
|
||||
mx_emojione_selected: isSelected,
|
||||
});
|
||||
const style = {};
|
||||
if (props.selected) style.border = '1px solid blue';
|
||||
return <img className={ className } src={ uri }
|
||||
title={ shortname } alt={ emojiUnicode } style={style}
|
||||
/>;
|
||||
return data.get('emojiUnicode');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1583,10 +1473,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
placeholder = undefined;
|
||||
}
|
||||
|
||||
const markdownClasses = classNames({
|
||||
mx_MessageComposer_input_markdownIndicator: true,
|
||||
mx_MessageComposer_markdownDisabled: this.state.isRichTextEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<ReplyPreview />
|
||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
room={this.props.room}
|
||||
|
@ -1597,10 +1492,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
src={require(`../../../../res/img/button-md-${!this.state.isRichTextEnabled}.png`)} />
|
||||
<AccessibleButton className={markdownClasses}
|
||||
onClick={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
/>
|
||||
<Editor ref={this._collectEditor}
|
||||
dir="auto"
|
||||
className="mx_MessageComposer_editor"
|
||||
|
|
|
@ -92,7 +92,7 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
|
||||
onWidgetLoad={() => {}} // we need to give this, apparently
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,8 @@ import sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PropTypes from "prop-types";
|
||||
import {RoomPermalinkCreator} from "../../../matrix-to";
|
||||
|
||||
function cancelQuoting() {
|
||||
dis.dispatch({
|
||||
|
@ -29,6 +31,10 @@ function cancelQuoting() {
|
|||
}
|
||||
|
||||
export default class ReplyPreview extends React.Component {
|
||||
static propTypes = {
|
||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -60,13 +66,12 @@ export default class ReplyPreview extends React.Component {
|
|||
if (!this.state.event) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
{ '💬 ' + _t('Replying') }
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
onClick={cancelQuoting} />
|
||||
|
@ -75,6 +80,7 @@ export default class ReplyPreview extends React.Component {
|
|||
<EventTile last={true}
|
||||
tileShape="reply_preview"
|
||||
mxEvent={this.state.event}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||
</div>
|
||||
</div>;
|
||||
|
|
347
src/components/views/rooms/RoomBreadcrumbs.js
Normal file
347
src/components/views/rooms/RoomBreadcrumbs.js
Normal file
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
Copyright 2019 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 dis from "../../../dispatcher";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import Analytics from "../../../Analytics";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const MAX_ROOMS = 20;
|
||||
|
||||
export default class RoomBreadcrumbs extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {rooms: []};
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this._dispatcherRef = null;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
let storedRooms = SettingsStore.getValue("breadcrumb_rooms");
|
||||
if (!storedRooms || !storedRooms.length) {
|
||||
// Fallback to the rooms stored in localstorage for those who would have had this.
|
||||
// TODO: Remove this after a bit - the feature was only on develop, so a few weeks should be plenty time.
|
||||
const roomStr = localStorage.getItem("mx_breadcrumb_rooms");
|
||||
if (roomStr) {
|
||||
try {
|
||||
storedRooms = JSON.parse(roomStr);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse breadcrumbs:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._loadRoomIds(storedRooms || []);
|
||||
|
||||
this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged);
|
||||
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
|
||||
SettingsStore.unwatchSetting(this._settingWatchRef);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("Room.myMembership", this.onMyMembership);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const rooms = this.state.rooms.slice();
|
||||
|
||||
if (rooms.length) {
|
||||
const roomModel = rooms[0];
|
||||
if (!roomModel.animated) {
|
||||
roomModel.animated = true;
|
||||
setTimeout(() => this.setState({rooms}), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
this._appendRoomId(payload.room_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMyMembership = (room, membership) => {
|
||||
if (membership === "leave" || membership === "ban") {
|
||||
const rooms = this.state.rooms.slice();
|
||||
const roomState = rooms.find((r) => r.room.roomId === room.roomId);
|
||||
if (roomState) {
|
||||
roomState.left = true;
|
||||
this.setState({rooms});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onRoomReceipt = (event, room) => {
|
||||
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
|
||||
this._calculateRoomBadges(room);
|
||||
}
|
||||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
if (!room) return; // Can be null for the notification timeline, etc.
|
||||
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
|
||||
this._calculateRoomBadges(room);
|
||||
}
|
||||
};
|
||||
|
||||
onEventDecrypted = (event) => {
|
||||
if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) {
|
||||
this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId()));
|
||||
}
|
||||
};
|
||||
|
||||
onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => {
|
||||
if (!value) return;
|
||||
|
||||
const currentState = this.state.rooms.map((r) => r.room.roomId);
|
||||
if (currentState.length === value.length) {
|
||||
let changed = false;
|
||||
for (let i = 0; i < currentState.length; i++) {
|
||||
if (currentState[i] !== value[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!changed) return;
|
||||
}
|
||||
|
||||
this._loadRoomIds(value);
|
||||
};
|
||||
|
||||
_loadRoomIds(roomIds) {
|
||||
if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms
|
||||
|
||||
// If we're here, the list changed.
|
||||
const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => {
|
||||
const badges = this._calculateBadgesForRoom(r) || {};
|
||||
return {
|
||||
room: r,
|
||||
animated: false,
|
||||
...badges,
|
||||
};
|
||||
});
|
||||
this.setState({
|
||||
rooms: rooms,
|
||||
});
|
||||
}
|
||||
|
||||
_calculateBadgesForRoom(room) {
|
||||
if (!room) return null;
|
||||
|
||||
// Reset the notification variables for simplicity
|
||||
const roomModel = {
|
||||
redBadge: false,
|
||||
formattedCount: "0",
|
||||
showCount: false,
|
||||
};
|
||||
|
||||
const notifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) {
|
||||
const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight');
|
||||
const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room);
|
||||
|
||||
const redBadge = highlightNotifs > 0;
|
||||
const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState));
|
||||
|
||||
if (redBadge || greyBadge) {
|
||||
const notifCount = redBadge ? highlightNotifs : unreadNotifs;
|
||||
const limitedCount = FormattingUtils.formatCount(notifCount);
|
||||
|
||||
roomModel.redBadge = redBadge;
|
||||
roomModel.formattedCount = limitedCount;
|
||||
roomModel.showCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
return roomModel;
|
||||
}
|
||||
|
||||
_calculateRoomBadges(room) {
|
||||
if (!room) return;
|
||||
|
||||
const rooms = this.state.rooms.slice();
|
||||
const roomModel = rooms.find((r) => r.room.roomId === room.roomId);
|
||||
if (!roomModel) return; // No applicable room, so don't do math on it
|
||||
|
||||
const badges = this._calculateBadgesForRoom(room);
|
||||
if (!badges) return; // No badges for some reason
|
||||
|
||||
Object.assign(roomModel, badges);
|
||||
this.setState({rooms});
|
||||
}
|
||||
|
||||
_appendRoomId(roomId) {
|
||||
let room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const rooms = this.state.rooms.slice();
|
||||
|
||||
// If the room is upgraded, use that room instead. We'll also splice out
|
||||
// any children of the room.
|
||||
const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
|
||||
if (history.length > 1) {
|
||||
room = history[history.length - 1]; // Last room is most recent
|
||||
|
||||
// Take out any room that isn't the most recent room
|
||||
for (let i = 0; i < history.length - 1; i++) {
|
||||
const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId);
|
||||
if (idx !== -1) rooms.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId);
|
||||
if (existingIdx !== -1) {
|
||||
rooms.splice(existingIdx, 1);
|
||||
}
|
||||
|
||||
rooms.splice(0, 0, {room, animated: false});
|
||||
|
||||
if (rooms.length > MAX_ROOMS) {
|
||||
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
|
||||
}
|
||||
this.setState({rooms});
|
||||
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.moveToOrigin();
|
||||
}
|
||||
|
||||
// We don't track room aesthetics (badges, membership, etc) over the wire so we
|
||||
// don't need to do this elsewhere in the file. Just where we alter the room IDs
|
||||
// and their order.
|
||||
const roomIds = rooms.map((r) => r.room.roomId);
|
||||
if (roomIds.length > 0) {
|
||||
SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
|
||||
}
|
||||
}
|
||||
|
||||
_viewRoom(room, index) {
|
||||
Analytics.trackEvent("Breadcrumbs", "click_node", index);
|
||||
dis.dispatch({action: "view_room", room_id: room.roomId});
|
||||
}
|
||||
|
||||
_onMouseEnter(room) {
|
||||
this._onHover(room);
|
||||
}
|
||||
|
||||
_onMouseLeave(room) {
|
||||
this._onHover(null); // clear hover states
|
||||
}
|
||||
|
||||
_onHover(room) {
|
||||
const rooms = this.state.rooms.slice();
|
||||
for (const r of rooms) {
|
||||
r.hover = room && r.room.roomId === room.roomId;
|
||||
}
|
||||
this.setState({rooms});
|
||||
}
|
||||
|
||||
_isDmRoom(room) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
return Boolean(dmRooms);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
|
||||
|
||||
// check for collapsed here and not at parent so we keep rooms in our state
|
||||
// when collapsing and expanding
|
||||
if (this.props.collapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rooms = this.state.rooms;
|
||||
const avatars = rooms.map((r, i) => {
|
||||
const isFirst = i === 0;
|
||||
const classes = classNames({
|
||||
"mx_RoomBreadcrumbs_crumb": true,
|
||||
"mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated,
|
||||
"mx_RoomBreadcrumbs_animate": isFirst,
|
||||
"mx_RoomBreadcrumbs_left": r.left,
|
||||
});
|
||||
|
||||
let tooltip = null;
|
||||
if (r.hover) {
|
||||
tooltip = <Tooltip label={r.room.name} />;
|
||||
}
|
||||
|
||||
let badge;
|
||||
if (r.showCount) {
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': true,
|
||||
'mx_RoomTile_badgeRed': r.redBadge,
|
||||
'mx_RoomTile_badgeUnread': !r.redBadge,
|
||||
});
|
||||
|
||||
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
|
||||
}
|
||||
|
||||
let dmIndicator;
|
||||
if (this._isDmRoom(r.room)) {
|
||||
dmIndicator = <img
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomBreadcrumbs_dmIndicator"
|
||||
width="13"
|
||||
height="15"
|
||||
alt={_t("Direct Chat")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room, i)}
|
||||
onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
|
||||
<RoomAvatar room={r.room} width={32} height={32} />
|
||||
{badge}
|
||||
{dmIndicator}
|
||||
{tooltip}
|
||||
</AccessibleButton>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomBreadcrumbs"
|
||||
trackHorizontalOverflow={true} verticalScrollsHorizontally={true}>
|
||||
{ avatars }
|
||||
</IndicatorScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,15 +17,11 @@ limitations under the License.
|
|||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
||||
|
@ -53,7 +49,7 @@ export default React.createClass({
|
|||
|
||||
_linkifyTopic: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.topic);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import Modal from "../../../Modal";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
|
@ -35,21 +33,16 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
||||
propTypes: {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
editing: PropTypes.bool,
|
||||
saving: PropTypes.bool,
|
||||
inRoom: PropTypes.bool,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onPinnedClick: PropTypes.func,
|
||||
onSaveClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
|
@ -60,7 +53,6 @@ module.exports = React.createClass({
|
|||
return {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
onSaveClick: function() {},
|
||||
onCancelClick: null,
|
||||
};
|
||||
},
|
||||
|
@ -80,7 +72,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.topic);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -120,33 +112,6 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onAvatarPickerClick: function(ev) {
|
||||
if (this.refs.file_label) {
|
||||
this.refs.file_label.click();
|
||||
}
|
||||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
const changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
}
|
||||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set avatar: " + errMsg);
|
||||
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to set avatar."),
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
onAvatarRemoveClick: function() {
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, '');
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
|
@ -180,65 +145,14 @@ module.exports = React.createClass({
|
|||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* After editing the settings, get the new name for the room
|
||||
*
|
||||
* @return {?string} newName or undefined if we didn't let the user edit the room name
|
||||
*/
|
||||
getEditedName: function() {
|
||||
let newName;
|
||||
if (this.refs.nameEditor) {
|
||||
newName = this.refs.nameEditor.getRoomName();
|
||||
}
|
||||
return newName;
|
||||
},
|
||||
|
||||
/**
|
||||
* After editing the settings, get the new topic for the room
|
||||
*
|
||||
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
|
||||
*/
|
||||
getEditedTopic: function() {
|
||||
let newTopic;
|
||||
if (this.refs.topicEditor) {
|
||||
newTopic = this.refs.topicEditor.getTopic();
|
||||
}
|
||||
return newTopic;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let name = null;
|
||||
let searchStatus = null;
|
||||
let topicElement = null;
|
||||
let cancelButton = null;
|
||||
let spinner = null;
|
||||
let saveButton = null;
|
||||
let settingsButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
let canSetRoomName;
|
||||
let canSetRoomAvatar;
|
||||
let canSetRoomTopic;
|
||||
if (this.props.editing) {
|
||||
// calculate permissions. XXX: this should be done on mount or something
|
||||
const userId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
|
||||
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
|
||||
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
|
||||
|
||||
saveButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ?
|
||||
<E2EIcon status={this.props.e2eStatus} /> :
|
||||
undefined;
|
||||
|
@ -247,103 +161,68 @@ module.exports = React.createClass({
|
|||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (canSetRoomName) {
|
||||
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||
} else {
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
|
||||
let roomName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
roomName = this.props.oobData.name;
|
||||
} else if (this.props.room) {
|
||||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (canSetRoomTopic) {
|
||||
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||
} else {
|
||||
let topic;
|
||||
if (this.props.room) {
|
||||
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (ev) {
|
||||
topic = ev.getContent().topic;
|
||||
}
|
||||
}
|
||||
topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||
let roomName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
roomName = this.props.oobData.name;
|
||||
} else if (this.props.room) {
|
||||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
let roomAvatar = null;
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
|
||||
let topic;
|
||||
if (this.props.room) {
|
||||
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (ev) {
|
||||
topic = ev.getContent().topic;
|
||||
}
|
||||
}
|
||||
const topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||
const avatarSize = 28;
|
||||
if (canSetRoomAvatar) {
|
||||
roomAvatar = (
|
||||
<div className="mx_RoomHeader_avatarPicker">
|
||||
<div onClick={this.onAvatarPickerClick}>
|
||||
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={avatarSize} height={avatarSize} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src={require("../../../../res/img/camera.svg")}
|
||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<img src={require("../../../../res/img/cancel.svg")}
|
||||
className="mx_filterFlipColor"
|
||||
width="10"
|
||||
alt={_t("Remove avatar")}
|
||||
title={_t("Remove avatar")} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
||||
roomAvatar = (
|
||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true} />
|
||||
);
|
||||
let roomAvatar;
|
||||
if (this.props.room) {
|
||||
roomAvatar = (<RoomAvatar
|
||||
room={this.props.room}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true} />);
|
||||
}
|
||||
|
||||
if (this.props.onSettingsClick) {
|
||||
settingsButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/settings.svg")} width="20" height="20" />
|
||||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
|
||||
onClick={this.props.onSettingsClick}
|
||||
title={_t("Settings")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -359,7 +238,6 @@ module.exports = React.createClass({
|
|||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
||||
onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
|
||||
{ pinsIndicator }
|
||||
<TintableSvg src={require("../../../../res/img/icons-pin.svg")} width="16" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
@ -374,28 +252,33 @@ module.exports = React.createClass({
|
|||
let forgetButton;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={_t("Forget room")}>
|
||||
<TintableSvg src={require("../../../../res/img/leave.svg")} width="26" height="20" />
|
||||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let searchButton;
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
searchButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/search.svg")} width="20" height="20" />
|
||||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let shareRoomButton;
|
||||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/share.svg")} width="20" height="20" />
|
||||
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_shareButton"
|
||||
onClick={this.onShareRoomClick}
|
||||
title={_t('Share room')}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightRow;
|
||||
let manageIntegsButton;
|
||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
manageIntegsButton = <ManageIntegsButton
|
||||
|
@ -403,27 +286,23 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
if (!this.props.editing) {
|
||||
rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
}
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
|
||||
<div className="mx_RoomHeader light-panel">
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ e2eIcon }
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ spinner }
|
||||
{ saveButton }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Timer from "../../../utils/Timer";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
|
@ -32,14 +33,16 @@ import DMRoomMap from '../../../utils/DMRoomMap';
|
|||
const Receipt = require('../../../utils/Receipt');
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
import ResizeHandle from '../elements/ResizeHandle';
|
||||
|
||||
import {Resizer} from '../../../resizer'
|
||||
import {Resizer} from '../../../resizer';
|
||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
const HOVER_MOVE_TIMEOUT = 1000;
|
||||
|
||||
function labelForTagName(tagName) {
|
||||
if (tagName.startsWith('u.')) return tagName.slice(2);
|
||||
|
@ -72,6 +75,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
|
||||
this._hoverClearTimer = null;
|
||||
this._subListRefs = {
|
||||
// key => RoomSubList ref
|
||||
};
|
||||
|
@ -94,7 +98,7 @@ module.exports = React.createClass({
|
|||
// update overflow indicators
|
||||
this._checkSubListsOverflow();
|
||||
// don't store height for collapsed sublists
|
||||
if(!this.collapsedState[key]) {
|
||||
if (!this.collapsedState[key]) {
|
||||
this.subListSizes[key] = size;
|
||||
window.localStorage.setItem("mx_roomlist_sizes",
|
||||
JSON.stringify(this.subListSizes));
|
||||
|
@ -121,6 +125,7 @@ module.exports = React.createClass({
|
|||
incomingCall: null,
|
||||
selectedTags: [],
|
||||
hover: false,
|
||||
customTags: CustomRoomTagStore.getTags(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -170,6 +175,15 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList();
|
||||
});
|
||||
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
|
||||
this.setState({
|
||||
customTags: CustomRoomTagStore.getTags(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshRoomList();
|
||||
|
||||
// order of the sublists
|
||||
|
@ -183,7 +197,7 @@ module.exports = React.createClass({
|
|||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cfg = {
|
||||
layout: this._layout,
|
||||
getLayout: () => this._layout,
|
||||
};
|
||||
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
|
||||
this.resizer.setClassNames({
|
||||
|
@ -198,7 +212,9 @@ module.exports = React.createClass({
|
|||
this._checkSubListsOverflow();
|
||||
|
||||
this.resizer.attach();
|
||||
window.addEventListener("resize", this.onWindowResize);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("leftPanelResized", this.onResize);
|
||||
}
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
|
@ -246,7 +262,6 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
this.mounted = false;
|
||||
|
||||
window.removeEventListener("resize", this.onWindowResize);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
|
@ -259,6 +274,11 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.removeListener("leftPanelResized", this.onResize);
|
||||
}
|
||||
|
||||
|
||||
if (this._tagStoreToken) {
|
||||
this._tagStoreToken.remove();
|
||||
}
|
||||
|
@ -266,6 +286,9 @@ module.exports = React.createClass({
|
|||
if (this._roomListStoreToken) {
|
||||
this._roomListStoreToken.remove();
|
||||
}
|
||||
if (this._customTagStoreToken) {
|
||||
this._customTagStoreToken.remove();
|
||||
}
|
||||
|
||||
// NB: GroupStore is not a Flux.Store
|
||||
if (this._groupStoreToken) {
|
||||
|
@ -276,13 +299,14 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList.cancelPendingCall();
|
||||
},
|
||||
|
||||
onWindowResize: function() {
|
||||
|
||||
onResize: function() {
|
||||
if (this.mounted && this._layout && this.resizeContainer &&
|
||||
Array.isArray(this._layoutSections)
|
||||
) {
|
||||
this._layout.update(
|
||||
this._layoutSections,
|
||||
this.resizeContainer.offsetHeight
|
||||
this.resizeContainer.offsetHeight,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -343,11 +367,32 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onMouseEnter: function(ev) {
|
||||
this.setState({hover: true});
|
||||
onMouseMove: async function(ev) {
|
||||
if (!this._hoverClearTimer) {
|
||||
this.setState({hover: true});
|
||||
this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT);
|
||||
this._hoverClearTimer.start();
|
||||
let finished = true;
|
||||
try {
|
||||
await this._hoverClearTimer.finished();
|
||||
} catch (err) {
|
||||
finished = false;
|
||||
}
|
||||
this._hoverClearTimer = null;
|
||||
if (finished) {
|
||||
this.setState({hover: false});
|
||||
this._delayedRefreshRoomList();
|
||||
}
|
||||
} else {
|
||||
this._hoverClearTimer.restart();
|
||||
}
|
||||
},
|
||||
|
||||
onMouseLeave: function(ev) {
|
||||
if (this._hoverClearTimer) {
|
||||
this._hoverClearTimer.abort();
|
||||
this._hoverClearTimer = null;
|
||||
}
|
||||
this.setState({hover: false});
|
||||
|
||||
// Refresh the room list just in case the user missed something.
|
||||
|
@ -705,6 +750,7 @@ module.exports = React.createClass({
|
|||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.recent'],
|
||||
|
@ -717,7 +763,8 @@ module.exports = React.createClass({
|
|||
];
|
||||
const tagSubLists = Object.keys(this.state.lists)
|
||||
.filter((tagName) => {
|
||||
return !tagName.match(STANDARD_TAGS_REGEX);
|
||||
return (!this.state.customTags || this.state.customTags[tagName]) &&
|
||||
!tagName.match(STANDARD_TAGS_REGEX);
|
||||
}).map((tagName) => {
|
||||
return {
|
||||
list: this.state.lists[tagName],
|
||||
|
@ -759,7 +806,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div ref={this._collectResizeContainer} className="mx_RoomList"
|
||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave}>
|
||||
{ subListComponents }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,13 +17,29 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const MessageCase = Object.freeze({
|
||||
NotLoggedIn: "NotLoggedIn",
|
||||
Joining: "Joining",
|
||||
Loading: "Loading",
|
||||
Rejecting: "Rejecting",
|
||||
Kicked: "Kicked",
|
||||
Banned: "Banned",
|
||||
OtherThreePIDError: "OtherThreePIDError",
|
||||
InvitedEmailMismatch: "InvitedEmailMismatch",
|
||||
Invite: "Invite",
|
||||
ViewingRoom: "ViewingRoom",
|
||||
RoomNotFound: "RoomNotFound",
|
||||
OtherError: "OtherError",
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
||||
|
@ -31,7 +47,6 @@ module.exports = React.createClass({
|
|||
onJoinClick: PropTypes.func,
|
||||
onRejectClick: PropTypes.func,
|
||||
onForgetClick: PropTypes.func,
|
||||
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifiying inviterName
|
||||
inviterName: PropTypes.string,
|
||||
|
@ -50,7 +65,9 @@ module.exports = React.createClass({
|
|||
// purpose of the spinner.
|
||||
spinner: PropTypes.bool,
|
||||
spinnerState: PropTypes.oneOf(["joining"]),
|
||||
|
||||
loading: PropTypes.bool,
|
||||
joining: PropTypes.bool,
|
||||
rejecting: PropTypes.bool,
|
||||
// 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).
|
||||
|
@ -60,7 +77,6 @@ module.exports = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
onJoinClick: function() {},
|
||||
canPreview: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -72,7 +88,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
// If this is an invite and we've been told what email
|
||||
// address was invited, fetch the user's list of 3pids
|
||||
// address was invited, fetch the user's list of Threepids
|
||||
// so we can check them against the one that was invited
|
||||
if (this.props.inviterName && this.props.invitedEmail) {
|
||||
this.setState({busy: true});
|
||||
|
@ -88,157 +104,355 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_roomNameElement: function() {
|
||||
return this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
_getMessageCase() {
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
if (isGuest) {
|
||||
return MessageCase.NotLoggedIn;
|
||||
}
|
||||
|
||||
const myMember = this._getMyMember();
|
||||
|
||||
if (myMember) {
|
||||
if (myMember.isKicked()) {
|
||||
return MessageCase.Kicked;
|
||||
} else if (myMember.membership === "ban") {
|
||||
return MessageCase.Banned;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.joining) {
|
||||
return MessageCase.Joining;
|
||||
} else if (this.props.rejecting) {
|
||||
return MessageCase.Rejecting;
|
||||
} else if (this.props.loading || this.state.busy) {
|
||||
return MessageCase.Loading;
|
||||
}
|
||||
|
||||
if (this.props.inviterName) {
|
||||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
return MessageCase.OtherThreePIDError;
|
||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().getUserId()) {
|
||||
return MessageCase.InvitedEmailMismatch;
|
||||
}
|
||||
}
|
||||
return MessageCase.Invite;
|
||||
} else if (this.props.error) {
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
return MessageCase.RoomNotFound;
|
||||
} else {
|
||||
return MessageCase.OtherError;
|
||||
}
|
||||
} else {
|
||||
return MessageCase.ViewingRoom;
|
||||
}
|
||||
},
|
||||
|
||||
_getKickOrBanInfo() {
|
||||
const myMember = this._getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
const kickerMember = this.props.room.currentState.getMember(
|
||||
myMember.events.member.getSender(),
|
||||
);
|
||||
const memberName = kickerMember ?
|
||||
kickerMember.name : myMember.events.member.getSender();
|
||||
const reason = myMember.events.member.getContent().reason;
|
||||
return {memberName, reason};
|
||||
},
|
||||
|
||||
_joinRule: function() {
|
||||
const room = this.props.room;
|
||||
if (room) {
|
||||
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
|
||||
if (joinRules) {
|
||||
return joinRules.getContent().join_rule;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_roomName: function(atStart = false) {
|
||||
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
if (name) {
|
||||
return name;
|
||||
} else if (atStart) {
|
||||
return _t("This room");
|
||||
} else {
|
||||
return _t("this room");
|
||||
}
|
||||
},
|
||||
|
||||
_getMyMember() {
|
||||
return (
|
||||
this.props.room &&
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
||||
);
|
||||
},
|
||||
|
||||
_getInviteMember: function() {
|
||||
const {room} = this.props;
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const inviteEvent = room.currentState.getMember(myUserId);
|
||||
if (!inviteEvent) {
|
||||
return;
|
||||
}
|
||||
const inviterUserId = inviteEvent.events.member.getSender();
|
||||
return room.currentState.getMember(inviterUserId);
|
||||
},
|
||||
|
||||
_isDMInvite() {
|
||||
const myMember = this._getMyMember();
|
||||
if (!myMember) {
|
||||
return false;
|
||||
}
|
||||
const memberEvent = myMember.events.member;
|
||||
const memberContent = memberEvent.getContent();
|
||||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
},
|
||||
|
||||
onRegisterClick: function() {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let joinBlock; let previewBlock;
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
let title;
|
||||
let subTitle;
|
||||
let primaryActionHandler;
|
||||
let primaryActionLabel;
|
||||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
|
||||
if (this.props.spinner || this.state.busy) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let spinnerIntro = "";
|
||||
if (this.props.spinnerState === "joining") {
|
||||
spinnerIntro = _t("Joining room...");
|
||||
const messageCase = this._getMessageCase();
|
||||
switch (messageCase) {
|
||||
case MessageCase.Joining: {
|
||||
title = _t("Joining room …");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Loading: {
|
||||
title = _t("Loading …");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Rejecting: {
|
||||
title = _t("Rejecting invite …");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
darkStyle = true;
|
||||
title = _t("Join the conversation with an account");
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
secondaryActionLabel = _t("Sign In");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
const {memberName, reason} = this._getKickOrBanInfo();
|
||||
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
||||
{memberName, roomName: this._roomName()});
|
||||
subTitle = _t("Reason: %(reason)s", {reason});
|
||||
|
||||
if (this._joinRule() === "invite") {
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
} else {
|
||||
primaryActionLabel = _t("Re-join");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Forget this room");
|
||||
secondaryActionHandler = this.props.onForgetClick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Banned: {
|
||||
const {memberName, reason} = this._getKickOrBanInfo();
|
||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||
{memberName, roomName: this._roomName()});
|
||||
subTitle = _t("Reason: %(reason)s", {reason});
|
||||
primaryActionLabel = _t("Forget this room");
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||
{roomName: this._roomName()});
|
||||
const joinRule = this._joinRule();
|
||||
const errCodeMessage = _t("%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.",
|
||||
{errcode: this.state.threePidFetchError.errcode},
|
||||
);
|
||||
switch (joinRule) {
|
||||
case "invite":
|
||||
subTitle = [
|
||||
_t("You can only join it with a working invite."),
|
||||
errCodeMessage,
|
||||
];
|
||||
break;
|
||||
case "public":
|
||||
subTitle = _t("You can still join it because this is a public room.");
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
default:
|
||||
subTitle = errCodeMessage;
|
||||
primaryActionLabel = _t("Try to join anyway");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailMismatch: {
|
||||
title = _t("This invite to %(roomName)s wasn't sent to your account",
|
||||
{roomName: this._roomName()});
|
||||
const joinRule = this._joinRule();
|
||||
if (joinRule === "public") {
|
||||
subTitle = _t("You can still join it because this is a public room.");
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
} else {
|
||||
subTitle = _t(
|
||||
"Sign in with a different account, ask for another invite, or " +
|
||||
"add the e-mail address %(email)s to this account.",
|
||||
{email: this.props.invitedEmail},
|
||||
);
|
||||
if (joinRule !== "invite") {
|
||||
primaryActionLabel = _t("Try to join anyway");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Invite: {
|
||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||
const avatar = <RoomAvatar room={this.props.room} />;
|
||||
|
||||
const inviteMember = this._getInviteMember();
|
||||
let inviterElement;
|
||||
if (inviteMember) {
|
||||
inviterElement = <span>
|
||||
<span className="mx_RoomPreviewBar_inviter">
|
||||
{inviteMember.rawDisplayName}
|
||||
</span> ({inviteMember.userId})
|
||||
</span>;
|
||||
} else {
|
||||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);
|
||||
}
|
||||
|
||||
const isDM = this._isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this._roomName() });
|
||||
}
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
|
||||
];
|
||||
|
||||
primaryActionLabel = _t("Accept");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Reject");
|
||||
secondaryActionHandler = this.props.onRejectClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||
{roomName: this._roomName()});
|
||||
} else {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||
{roomName: this._roomName(true)});
|
||||
}
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.RoomNotFound: {
|
||||
title = _t("%(roomName)s does not exist.", {roomName: this._roomName(true)});
|
||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherError: {
|
||||
title = _t("%(roomName)s is not accessible at this time.", {roomName: this._roomName(true)});
|
||||
subTitle = [
|
||||
_t("Try again later, or ask a room admin to check if you have access."),
|
||||
_t(
|
||||
"%(errcode)s was returned while trying to access the room. " +
|
||||
"If you think you're seeing this message in error, please " +
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||
target="_blank" rel="noopener">{ label }</a> },
|
||||
),
|
||||
];
|
||||
break;
|
||||
}
|
||||
return (<div className="mx_RoomPreviewBar">
|
||||
<p className="mx_RoomPreviewBar_spinnerIntro">{ spinnerIntro }</p>
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
||||
const myMember = this.props.room ?
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
|
||||
null;
|
||||
const kicked = myMember && myMember.isKicked();
|
||||
const banned = myMember && myMember && myMember.membership == 'ban';
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
if (this.props.inviterName) {
|
||||
let emailMatchBlock;
|
||||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
emailMatchBlock = <div className="error">
|
||||
{ _t("Unable to ascertain that the address this invite was sent to matches one associated with your account.") }
|
||||
</div>;
|
||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
|
||||
emailMatchBlock =
|
||||
<div className="mx_RoomPreviewBar_warning">
|
||||
<div className="mx_RoomPreviewBar_warningIcon">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="24" height="23" title= "/!\\" alt="/!\\" />
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_warningText">
|
||||
{ _t("This invitation was sent to an email address which is not associated with this account:") }
|
||||
<b><span className="email">{ this.props.invitedEmail }</span></b>
|
||||
<br />
|
||||
{ _t("You may wish to login with a different account, or add this email to this account.") }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_invite_text">
|
||||
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ _t(
|
||||
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
|
||||
{},
|
||||
{
|
||||
'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
|
||||
'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
{ emailMatchBlock }
|
||||
</div>
|
||||
);
|
||||
} else if (kicked || banned) {
|
||||
const roomName = this._roomNameElement();
|
||||
const kickerMember = this.props.room.currentState.getMember(
|
||||
myMember.events.member.getSender(),
|
||||
);
|
||||
const kickerName = kickerMember ?
|
||||
kickerMember.name : myMember.events.member.getSender();
|
||||
let reason;
|
||||
if (myMember.events.member.getContent().reason) {
|
||||
reason = <div>{ _t("Reason: %(reasonText)s", {reasonText: myMember.events.member.getContent().reason}) }</div>;
|
||||
}
|
||||
let rejoinBlock;
|
||||
if (!banned) {
|
||||
rejoinBlock = <div><a onClick={this.props.onJoinClick}><b>{ _t("Rejoin") }</b></a></div>;
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
if (!Array.isArray(subTitle)) {
|
||||
subTitle = [subTitle];
|
||||
}
|
||||
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
|
||||
}
|
||||
|
||||
let actionText;
|
||||
if (kicked) {
|
||||
if (roomName) {
|
||||
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||
} else {
|
||||
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
|
||||
}
|
||||
} else if (banned) {
|
||||
if (roomName) {
|
||||
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||
} else {
|
||||
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
|
||||
}
|
||||
} // no other options possible due to the kicked || banned check above.
|
||||
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ actionText }
|
||||
<br />
|
||||
{ reason }
|
||||
{ rejoinBlock }
|
||||
<a onClick={this.props.onForgetClick}><b>{ _t("Forget room") }</b></a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.error) {
|
||||
const name = this.props.roomAlias || _t("This room");
|
||||
let error;
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
error = _t("%(roomName)s does not exist.", {roomName: name});
|
||||
} else {
|
||||
error = _t("%(roomName)s is not accessible at this time.", {roomName: name});
|
||||
}
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
let titleElement;
|
||||
if (showSpinner) {
|
||||
titleElement = <h3 className="mx_RoomPreviewBar_spinnerTitle"><Spinner />{ title }</h3>;
|
||||
} else {
|
||||
const name = this._roomNameElement();
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
|
||||
<br />
|
||||
{ _t("<a>Click here</a> to join the discussion!",
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
titleElement = <h3>{ title }</h3>;
|
||||
}
|
||||
|
||||
let primaryButton;
|
||||
if (primaryActionHandler) {
|
||||
primaryButton = (
|
||||
<AccessibleButton kind="primary" onClick={primaryActionHandler}>
|
||||
{ primaryActionLabel }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.canPreview) {
|
||||
previewBlock = (
|
||||
<div className="mx_RoomPreviewBar_preview_text">
|
||||
{ _t('This is a preview of this room. Room interactions have been disabled') }.
|
||||
</div>
|
||||
let secondaryButton;
|
||||
if (secondaryActionHandler) {
|
||||
secondaryButton = (
|
||||
<AccessibleButton kind="secondary" onClick={secondaryActionHandler}>
|
||||
{ secondaryActionLabel }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, {
|
||||
"mx_RoomPreviewBar_panel": this.props.canPreview,
|
||||
"mx_RoomPreviewBar_dialog": !this.props.canPreview,
|
||||
"mx_RoomPreviewBar_dark": darkStyle,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomPreviewBar">
|
||||
<div className="mx_RoomPreviewBar_wrapper">
|
||||
{ joinBlock }
|
||||
{ previewBlock }
|
||||
<div className={classes}>
|
||||
<div className="mx_RoomPreviewBar_message">
|
||||
{ titleElement }
|
||||
{ subTitleElements }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_actions">
|
||||
{ secondaryButton }
|
||||
{ primaryButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -38,7 +38,8 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
unverifiedDevice: null,
|
||||
backupInfo: null,
|
||||
notNowClicked: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -47,10 +48,12 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
let backupSigStatus;
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
this.setState({
|
||||
loading: false,
|
||||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
|
@ -59,44 +62,24 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let unverifiedDevice;
|
||||
for (const sig of backupSigStatus.sigs) {
|
||||
if (sig.device && !sig.device.isVerified()) {
|
||||
unverifiedDevice = sig.device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
unverifiedDevice,
|
||||
});
|
||||
}
|
||||
|
||||
showSetupDialog = () => {
|
||||
if (this.state.unverifiedDevice) {
|
||||
if (this.state.backupInfo) {
|
||||
// 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,
|
||||
});
|
||||
return;
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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"),
|
||||
);
|
||||
onOnNotNowClick = () => {
|
||||
this.setState({notNowClicked: true});
|
||||
}
|
||||
|
||||
onDontAskAgainClick = () => {
|
||||
|
@ -126,60 +109,55 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
// If there was an error loading just don't display the banner: we'll try again
|
||||
// next time the user switchs to the room.
|
||||
if (this.state.error || this.state.loading || this.state.notNowClicked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
|
||||
let body;
|
||||
let primaryCaption = _t("Set up");
|
||||
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 = <div>
|
||||
<p>{_t(
|
||||
"Secure Message Recovery has been set up on another device: <deviceName></deviceName>",
|
||||
{},
|
||||
{
|
||||
deviceName: () => <i>{this.state.unverifiedDevice.unsigned.device_display_name}</i>,
|
||||
},
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"To view your secure message history and ensure you can view new " +
|
||||
"messages on future devices, verify that device now.",
|
||||
)}</p>
|
||||
</div>;
|
||||
primaryCaption = _t("Verify device");
|
||||
let setupCaption;
|
||||
if (this.state.backupInfo) {
|
||||
setupCaption = _t("Connect this device to Key Backup");
|
||||
} 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.",
|
||||
);
|
||||
setupCaption = _t("Start using Key Backup");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomRecoveryReminder">
|
||||
<div className="mx_RoomRecoveryReminder_header">{_t(
|
||||
"Secure Message Recovery",
|
||||
"Never lose encrypted messages",
|
||||
)}</div>
|
||||
<div className="mx_RoomRecoveryReminder_body">{body}</div>
|
||||
<div className="mx_RoomRecoveryReminder_body">
|
||||
<p>{_t(
|
||||
"Messages in this room are secured with end-to-end " +
|
||||
"encryption. Only you and the recipient(s) have the " +
|
||||
"keys to read these messages.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Securely back up your keys to avoid losing them. " +
|
||||
"<a>Learn more.</a>", {},
|
||||
{
|
||||
// TODO: We don't have this link yet: this will prevent the translators
|
||||
// having to re-translate the string when we do.
|
||||
a: sub => '',
|
||||
},
|
||||
)}</p>
|
||||
</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}>
|
||||
{primaryCaption}
|
||||
{setupCaption}
|
||||
</AccessibleButton>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onOnNotNowClick}>
|
||||
{ _t("Not now") }
|
||||
</AccessibleButton></p>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("Don't ask me again") }
|
||||
</AccessibleButton></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -68,12 +68,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowNotifBadge: function() {
|
||||
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(this.state.notifState) > -1;
|
||||
return RoomNotifs.BADGE_STATES.includes(this.state.notifState);
|
||||
},
|
||||
|
||||
_shouldShowMentionBadge: function() {
|
||||
return this.state.notifState !== RoomNotifs.MUTE;
|
||||
return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState);
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
|
@ -108,13 +107,6 @@ module.exports = React.createClass({
|
|||
return statusUser._unstable_statusMessage;
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room) {
|
||||
if (room !== this.props.room) return;
|
||||
this.setState({
|
||||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||
});
|
||||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
if (room !== this.props.room) return;
|
||||
this.setState({
|
||||
|
@ -159,7 +151,6 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -179,7 +170,6 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
|
@ -306,7 +296,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||
const notificationCount = this.state.notificationCount;
|
||||
const notificationCount = this.props.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||
|
@ -352,7 +342,6 @@ module.exports = React.createClass({
|
|||
badge = <div className={badgeClasses}>{ badgeContent }</div>;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
let subtextLabel;
|
||||
let tooltip;
|
||||
|
@ -364,17 +353,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
|
||||
if (this.state.selected) {
|
||||
const nameSelected = <EmojiText>{ name }</EmojiText>;
|
||||
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
|
||||
}
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
||||
} else if (this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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 ReactDOM from 'react-dom';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTooltip',
|
||||
|
||||
propTypes: {
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: React.PropTypes.string.isRequired,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: React.PropTypes.string,
|
||||
// The tooltip is derived from either the room name or a label
|
||||
room: React.PropTypes.object,
|
||||
label: React.PropTypes.node,
|
||||
},
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
componentDidMount: function() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_RoomTileTooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this._renderTooltip, true);
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
|
||||
this._renderTooltip();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._renderTooltip();
|
||||
},
|
||||
|
||||
// Remove the wrapper element, as the tooltip has finished using it
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: null,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this._renderTooltip, true);
|
||||
},
|
||||
|
||||
_updatePosition(style) {
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
let offset = 0;
|
||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
|
||||
}
|
||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||
return style;
|
||||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName);
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_RoomTooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
|
||||
|
||||
// Tell the roomlist about us so it can manipulate us if it wishes
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: this.tooltip,
|
||||
parent: parent,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} >
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -20,6 +20,7 @@ import sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomUpgradeWarningBar',
|
||||
|
@ -29,6 +30,24 @@ module.exports = React.createClass({
|
|||
recommendation: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
|
||||
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
|
||||
},
|
||||
|
||||
_onStateEvents: function(event, state) {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getType() !== "m.room.tombstone") return;
|
||||
|
||||
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
|
||||
},
|
||||
|
||||
onUpgradeClick: function() {
|
||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
|
||||
|
@ -37,33 +56,40 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let upgradeText = (
|
||||
let doUpgradeWarnings = (
|
||||
<div>
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
{_t("This room is using an unstable room version. If you aren't expecting " +
|
||||
"this, please upgrade the room.")}
|
||||
<p>
|
||||
{_t(
|
||||
"Upgrading this room will shut down the current instance of the room and create " +
|
||||
"an upgraded room with the same name.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||
"version of the room - room members will have to click this link to join the new room.",
|
||||
{}, {
|
||||
"b": (sub) => <b>{sub}</b>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{_t("Click here to upgrade to the latest room version.")}
|
||||
{_t("Upgrade this room to the recommended room version")}
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
if (this.props.recommendation.urgent) {
|
||||
upgradeText = (
|
||||
<div>
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t("There is a known vulnerability affecting this room.")}
|
||||
</div>
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
{_t("This room version is vulnerable to malicious modification of room state.")}
|
||||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{_t("Click here to upgrade to the latest room version and ensure room integrity " +
|
||||
"is protected.")}
|
||||
</AccessibleButton>
|
||||
|
||||
if (this.state.upgraded) {
|
||||
doUpgradeWarnings = (
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
<p>
|
||||
{_t("This room has already been upgraded.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -71,7 +97,18 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomUpgradeWarningBar">
|
||||
{upgradeText}
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{_t(
|
||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||
"marked as <i>unstable</i>.",
|
||||
{},
|
||||
{
|
||||
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
|
||||
"i": (sub) => <i>{sub}</i>,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{doUpgradeWarnings}
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ module.exports = React.createClass({
|
|||
// href for the highlights in this result
|
||||
resultLink: PropTypes.string,
|
||||
|
||||
onWidgetLoad: PropTypes.func,
|
||||
onHeightChanged: PropTypes.func,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -56,8 +56,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (EventTile.haveTileForEvent(ev)) {
|
||||
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
highlightLink={this.props.resultLink}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />);
|
||||
onHeightChanged={this.props.onHeightChanged} />);
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -25,14 +25,20 @@ import dis from '../../../dispatcher';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
// We sit in a context menu, so the persisted element container needs to float
|
||||
// above it, so it needs a greater z-index than the ContextMenu
|
||||
const STICKERPICKER_Z_INDEX = 5000;
|
||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||
// We sit in a context menu, so this should be given to the context menu.
|
||||
const STICKERPICKER_Z_INDEX = 3500;
|
||||
|
||||
// Key to store the widget's AppTile under in PersistedElement
|
||||
const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
static currentWidget;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onShowStickersClick = this._onShowStickersClick.bind(this);
|
||||
|
@ -103,6 +109,9 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) client.removeListener('accountData', this._updateWidget);
|
||||
|
||||
window.removeEventListener('resize', this._onResize);
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -123,6 +132,29 @@ export default class Stickerpicker extends React.Component {
|
|||
|
||||
_updateWidget() {
|
||||
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
|
||||
if (!stickerpickerWidget) {
|
||||
Stickerpicker.currentWidget = null;
|
||||
this.setState({stickerpickerWidget: null, widgetId: null});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWidget = Stickerpicker.currentWidget;
|
||||
let currentUrl = null;
|
||||
if (currentWidget && currentWidget.content && currentWidget.content.url) {
|
||||
currentUrl = currentWidget.content.url;
|
||||
}
|
||||
|
||||
let newUrl = null;
|
||||
if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
|
||||
newUrl = stickerpickerWidget.content.url;
|
||||
}
|
||||
|
||||
if (newUrl !== currentUrl) {
|
||||
// Destroy the existing frame so a new one can be created
|
||||
PersistedElement.destroyElement(PERSISTED_ELEMENT_KEY);
|
||||
}
|
||||
|
||||
Stickerpicker.currentWidget = stickerpickerWidget;
|
||||
this.setState({
|
||||
stickerpickerWidget,
|
||||
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
|
||||
|
@ -208,7 +240,7 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement persistKey="stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
id={stickerpickerWidget.id}
|
||||
url={stickerpickerWidget.content.url}
|
||||
|
@ -226,6 +258,7 @@ export default class Stickerpicker extends React.Component {
|
|||
showTitle={false}
|
||||
showMinimise={true}
|
||||
showDelete={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
|
@ -306,22 +339,25 @@ export default class Stickerpicker extends React.Component {
|
|||
*/
|
||||
_launchManageIntegrations() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.connect().done(() => {
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
'type_' + widgetType,
|
||||
this.state.widgetId,
|
||||
) :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
|
||||
this.setState({showStickers: false});
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
this.setState({showStickers: false});
|
||||
}, (err) => {
|
||||
this.setState({imError: err});
|
||||
console.error('Error ensuring a valid scalar_token exists', err);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
|
||||
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
|
||||
let stickersButton;
|
||||
|
@ -339,6 +375,7 @@ export default class Stickerpicker extends React.Component {
|
|||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
/>;
|
||||
|
||||
if (this.state.showStickers) {
|
||||
|
@ -347,11 +384,10 @@ export default class Stickerpicker extends React.Component {
|
|||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers mx_Stickers_hideStickers"
|
||||
onClick={this._onHideStickersClick}
|
||||
ref='target'
|
||||
title={_t("Hide Stickers")}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/face.svg")} width="20" height="20" />
|
||||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
|
@ -359,10 +395,10 @@ export default class Stickerpicker extends React.Component {
|
|||
<AccessibleButton
|
||||
id='stickersButton'
|
||||
key="controls_show_stickers"
|
||||
className="mx_MessageComposer_stickers"
|
||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}>
|
||||
<TintableSvg src={require("../../../../res/img/feather-icons/face.svg")} width="20" height="20" />
|
||||
title={_t("Show Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
return <div>
|
||||
|
|
143
src/components/views/rooms/ThirdPartyMemberInfo.js
Normal file
143
src/components/views/rooms/ThirdPartyMemberInfo.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
Copyright 2019 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 {MatrixEvent} from "matrix-js-sdk";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher";
|
||||
import sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
|
||||
export default class ThirdPartyMemberInfo extends React.Component {
|
||||
static propTypes = {
|
||||
event: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
||||
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
|
||||
if (typeof(kickLevel) !== 'number') kickLevel = 50;
|
||||
|
||||
const sender = room.getMember(this.props.event.getSender());
|
||||
|
||||
this.state = {
|
||||
stateKey: this.props.event.getStateKey(),
|
||||
roomId: this.props.event.getRoomId(),
|
||||
displayName: this.props.event.getContent().display_name,
|
||||
invited: true,
|
||||
canKick: me ? me.powerLevel > kickLevel : false,
|
||||
senderName: sender ? sender.name : this.props.event.getSender(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = (ev) => {
|
||||
if (ev.getType() === "m.room.third_party_invite" && ev.getStateKey() === this.state.stateKey) {
|
||||
const newDisplayName = ev.getContent().display_name;
|
||||
const isInvited = isValid3pidInvite(ev);
|
||||
|
||||
const newState = {invited: isInvited};
|
||||
if (newDisplayName) newState['displayName'] = newDisplayName;
|
||||
this.setState(newState);
|
||||
}
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
dis.dispatch({
|
||||
action: "view_3pid_invite",
|
||||
event: null,
|
||||
});
|
||||
};
|
||||
|
||||
onKickClick = () => {
|
||||
MatrixClientPeg.get().sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
// Revert echo because of error
|
||||
this.setState({invited: true});
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Revoke 3pid invite failed', '', ErrorDialog, {
|
||||
title: _t("Failed to revoke invite"),
|
||||
description: _t(
|
||||
"Could not revoke the invite. The server may be experiencing a temporary problem or " +
|
||||
"you do not have sufficient permissions to revoke the invite.",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
// Local echo
|
||||
this.setState({invited: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
let adminTools = null;
|
||||
if (this.state.canKick && this.state.invited) {
|
||||
adminTools = (
|
||||
<div className="mx_MemberInfo_container">
|
||||
<h3>{_t("Admin Tools")}</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
|
||||
{_t("Revoke invite")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We shamelessly rip off the MemberInfo styles here.
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
title={_t('Close')}
|
||||
/>
|
||||
<h2>{this.state.displayName}</h2>
|
||||
</div>
|
||||
<div className="mx_MemberInfo_container">
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{_t("Invited by %(sender)s", {sender: this.state.senderName})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{adminTools}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -29,7 +28,8 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
onVisible: PropTypes.func,
|
||||
onShown: PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
@ -59,11 +59,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function(_, prevState) {
|
||||
if (this.props.onVisible &&
|
||||
!prevState.usersTyping.length &&
|
||||
this.state.usersTyping.length
|
||||
) {
|
||||
this.props.onVisible();
|
||||
const wasVisible = this._isVisible(prevState);
|
||||
const isVisible = this._isVisible(this.state);
|
||||
if (this.props.onShown && !wasVisible && isVisible) {
|
||||
this.props.onShown();
|
||||
} else if (this.props.onHidden && wasVisible && !isVisible) {
|
||||
this.props.onHidden();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -77,8 +78,12 @@ module.exports = React.createClass({
|
|||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
|
||||
},
|
||||
|
||||
_isVisible: function(state) {
|
||||
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this.state.usersTyping.length !== 0 || Object.keys(this.state.delayedStopTypingTimers).length !== 0;
|
||||
return this._isVisible(this.state);
|
||||
},
|
||||
|
||||
onRoomTimeline: function(event, room) {
|
||||
|
@ -170,6 +175,7 @@ module.exports = React.createClass({
|
|||
width={24}
|
||||
height={24}
|
||||
resizeMethod="crop"
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -205,15 +211,13 @@ module.exports = React.createClass({
|
|||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<li className="mx_WhoIsTypingTile">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
{ typingString }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue