Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into soru/spoilers
This commit is contained in:
commit
fe9ae46ffb
389 changed files with 23061 additions and 5544 deletions
|
@ -56,6 +56,6 @@ export default class DateSeparator extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <h2 className="mx_DateSeparator"><hr /><date>{ this.getLabel() }</date><hr /></h2>;
|
||||
return <h2 className="mx_DateSeparator"><hr /><div>{ this.getLabel() }</div><hr /></h2>;
|
||||
}
|
||||
}
|
||||
|
|
173
src/components/views/messages/EditHistoryMessage.js
Normal file
173
src/components/views/messages/EditHistoryMessage.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import classNames from 'classnames';
|
||||
|
||||
function getReplacedContent(event) {
|
||||
const originalContent = event.getOriginalContent();
|
||||
return originalContent["m.new_content"] || originalContent;
|
||||
}
|
||||
|
||||
export default class EditHistoryMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
previousEdit: PropTypes.instanceOf(MatrixEvent),
|
||||
isBaseEvent: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const cli = MatrixClientPeg.get();
|
||||
const {userId} = cli.credentials;
|
||||
const event = this.props.mxEvent;
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged);
|
||||
}
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
|
||||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
this.setState({sendStatus: this.props.mxEvent.getAssociatedStatus()});
|
||||
};
|
||||
|
||||
_onRedactClick = async () => {
|
||||
const event = this.props.mxEvent;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
|
||||
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
|
||||
redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
};
|
||||
|
||||
_onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
};
|
||||
|
||||
pillifyLinks() {
|
||||
// not present for redacted events
|
||||
if (this.refs.content) {
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.pillifyLinks();
|
||||
}
|
||||
|
||||
_renderActionBar() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
// hide the button when already redacted
|
||||
let redactButton;
|
||||
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent) {
|
||||
redactButton = (
|
||||
<AccessibleButton onClick={this._onRedactClick} disabled={!this.state.canRedact}>
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
const viewSourceButton = (
|
||||
<AccessibleButton onClick={this._onViewSourceClick}>
|
||||
{_t("View Source")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
// disabled remove button when not allowed
|
||||
return (
|
||||
<div className="mx_MessageActionBar">
|
||||
{redactButton}
|
||||
{viewSourceButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
const content = getReplacedContent(mxEvent);
|
||||
let contentContainer;
|
||||
if (mxEvent.isRedacted()) {
|
||||
const UnknownBody = sdk.getComponent('messages.UnknownBody');
|
||||
contentContainer = <UnknownBody mxEvent={this.props.mxEvent} />;
|
||||
} else {
|
||||
let contentElements;
|
||||
if (this.props.previousEdit) {
|
||||
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
|
||||
} else {
|
||||
contentElements = HtmlUtils.bodyToHtml(content, null, {stripReplyFallback: true});
|
||||
}
|
||||
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
contentContainer = (
|
||||
<div className="mx_EventTile_content" ref="content">*
|
||||
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||
{contentElements}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
contentContainer = <div className="mx_EventTile_content" ref="content">{contentElements}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
|
||||
const classes = classNames({
|
||||
"mx_EventTile": true,
|
||||
"mx_EventTile_redacted": mxEvent.isRedacted(),
|
||||
"mx_EventTile_sending": isSending,
|
||||
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
|
||||
});
|
||||
return (
|
||||
<li>
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_line">
|
||||
<span className="mx_MessageTimestamp">{timestamp}</span>
|
||||
{ contentContainer }
|
||||
{ this._renderActionBar() }
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -145,6 +145,7 @@ function remoteRender(event) {
|
|||
a.target = data.target;
|
||||
a.download = data.download;
|
||||
a.style = data.style;
|
||||
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
|
||||
a.href = window.URL.createObjectURL(data.blob);
|
||||
a.appendChild(img);
|
||||
a.appendChild(document.createTextNode(data.textContent));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -172,8 +172,8 @@ export default class MImageBody extends React.Component {
|
|||
// thumbnail resolution will be unnecessarily reduced.
|
||||
// custom timeline widths seems preferable.
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const thumbWidth = 800 * pixelRatio;
|
||||
const thumbHeight = 600 * pixelRatio;
|
||||
const thumbWidth = Math.round(800 * pixelRatio);
|
||||
const thumbHeight = Math.round(600 * pixelRatio);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
|
@ -264,6 +264,7 @@ export default class MImageBody extends React.Component {
|
|||
decryptedBlob = blob;
|
||||
return URL.createObjectURL(blob);
|
||||
}).then((contentUrl) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
|
@ -271,6 +272,7 @@ export default class MImageBody extends React.Component {
|
|||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
if (this.unmounted) return;
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.setState({
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,8 +23,8 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import { createMenu } from '../../structures/ContextualMenu';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import {RoomContext} from "../../structures/RoomView";
|
||||
|
||||
export default class MessageActionBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -36,57 +37,69 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
onFocusChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
room: RoomContext,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
// When an event decrypts, it is likely to change the set of available
|
||||
// actions, so we force an update to check again.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
}
|
||||
this.props.onFocusChange(focused);
|
||||
}
|
||||
};
|
||||
|
||||
onCryptoClicked = () => {
|
||||
onCryptoClick = () => {
|
||||
const event = this.props.mxEvent;
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onReplyClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onEditClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onOptionsClick = (ev) => {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
const buttonRect = ev.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 { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClicked();
|
||||
e2eInfoCallback = () => this.onCryptoClick();
|
||||
}
|
||||
|
||||
createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
const menuOptions = {
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
chevronFace: "none",
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
|
@ -94,62 +107,55 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const buttonRight = buttonRect.right + window.pageXOffset;
|
||||
const buttonBottom = buttonRect.bottom + window.pageYOffset;
|
||||
const buttonTop = buttonRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more
|
||||
// space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
}
|
||||
|
||||
createMenu(MessageContextMenu, menuOptions);
|
||||
|
||||
this.onFocusChange(true);
|
||||
}
|
||||
};
|
||||
|
||||
isReactionsEnabled() {
|
||||
return SettingsStore.isFeatureEnabled("feature_reactions");
|
||||
}
|
||||
renderReactButton() {
|
||||
const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction');
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
isEditingEnabled() {
|
||||
return SettingsStore.isFeatureEnabled("feature_message_editing");
|
||||
}
|
||||
|
||||
renderAgreeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Agree or Disagree")}
|
||||
options={["👍", "👎"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>;
|
||||
}
|
||||
|
||||
renderLikeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
|
||||
return <ReactionDimension
|
||||
title={_t("Like or Dislike")}
|
||||
options={["🙂", "😔"]}
|
||||
reactions={this.props.reactions}
|
||||
mxEvent={this.props.mxEvent}
|
||||
return <ReactMessageAction
|
||||
mxEvent={mxEvent}
|
||||
reactions={reactions}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let agreeDimensionReactionButtons;
|
||||
let likeDimensionReactionButtons;
|
||||
let reactButton;
|
||||
let replyButton;
|
||||
let editButton;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
agreeDimensionReactionButtons = this.renderAgreeDimension();
|
||||
likeDimensionReactionButtons = this.renderLikeDimension();
|
||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
/>;
|
||||
if (this.context.room.canReact) {
|
||||
reactButton = this.renderReactButton();
|
||||
}
|
||||
if (this.context.room.canReply) {
|
||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) {
|
||||
if (canEditContent(this.props.mxEvent)) {
|
||||
editButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
|
@ -157,8 +163,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_MessageActionBar">
|
||||
{agreeDimensionReactionButtons}
|
||||
{likeDimensionReactionButtons}
|
||||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = React.createClass({
|
|||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
editState={this.props.editState}
|
||||
onHeightChanged={this.props.onHeightChanged} />;
|
||||
},
|
||||
});
|
||||
|
|
97
src/components/views/messages/ReactMessageAction.js
Normal file
97
src/components/views/messages/ReactMessageAction.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 sdk from '../../../index';
|
||||
|
||||
export default class ReactMessageAction extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
onFocusChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
}
|
||||
this.props.onFocusChange(focused);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
// Force a re-render of the tooltip because a change in the reactions
|
||||
// set means the event tile's layout may have changed and possibly
|
||||
// altered the location where the tooltip should be shown.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip');
|
||||
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
const content = <ReactionsQuickTooltip
|
||||
mxEvent={mxEvent}
|
||||
reactions={reactions}
|
||||
/>;
|
||||
|
||||
return <InteractiveTooltip
|
||||
content={content}
|
||||
onVisibilityChange={this.onFocusChange}
|
||||
>
|
||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||
</InteractiveTooltip>;
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class ReactionDimension extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// Array of strings containing the emoji for each option
|
||||
options: PropTypes.array.isRequired,
|
||||
title: PropTypes.string,
|
||||
// The Relations model from the JS SDK for reactions
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getSelection();
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
this.setState(this.getSelection());
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
const myReactions = this.getMyReactions();
|
||||
if (!myReactions) {
|
||||
return {
|
||||
selectedOption: null,
|
||||
selectedReactionEvent: null,
|
||||
};
|
||||
}
|
||||
const { options } = this.props;
|
||||
let selectedOption = null;
|
||||
let selectedReactionEvent = null;
|
||||
for (const option of options) {
|
||||
const reactionForOption = myReactions.find(mxEvent => {
|
||||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getRelation().key === option;
|
||||
});
|
||||
if (!reactionForOption) {
|
||||
continue;
|
||||
}
|
||||
if (selectedOption) {
|
||||
// If there are multiple selected values (only expected to occur via
|
||||
// non-Riot clients), then act as if none are selected.
|
||||
return {
|
||||
selectedOption: null,
|
||||
selectedReactionEvent: null,
|
||||
};
|
||||
}
|
||||
selectedOption = option;
|
||||
selectedReactionEvent = reactionForOption;
|
||||
}
|
||||
return { selectedOption, selectedReactionEvent };
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onOptionClick = (ev) => {
|
||||
const { key } = ev.target.dataset;
|
||||
this.toggleDimension(key);
|
||||
}
|
||||
|
||||
toggleDimension(key) {
|
||||
const { selectedOption, selectedReactionEvent } = this.state;
|
||||
const newSelectedOption = selectedOption !== key ? key : null;
|
||||
this.setState({
|
||||
selectedOption: newSelectedOption,
|
||||
});
|
||||
if (selectedReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
selectedReactionEvent.getId(),
|
||||
);
|
||||
}
|
||||
if (newSelectedOption) {
|
||||
MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": this.props.mxEvent.getId(),
|
||||
"key": newSelectedOption,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedOption } = this.state;
|
||||
const { options } = this.props;
|
||||
|
||||
const items = options.map(option => {
|
||||
const disabled = selectedOption && selectedOption !== option;
|
||||
const classes = classNames({
|
||||
mx_ReactionDimension_disabled: disabled,
|
||||
});
|
||||
return <span key={option}
|
||||
data-key={option}
|
||||
className={classes}
|
||||
onClick={this.onOptionClick}
|
||||
>
|
||||
{option}
|
||||
</span>;
|
||||
});
|
||||
|
||||
return <span className="mx_ReactionDimension"
|
||||
title={this.props.title}
|
||||
aria-hidden={true}
|
||||
>
|
||||
{items}
|
||||
</span>;
|
||||
}
|
||||
}
|
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
68
src/components/views/messages/ReactionTooltipButton.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class ReactionTooltipButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent: PropTypes.object,
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
myReactionEvent.getId(),
|
||||
);
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": mxEvent.getId(),
|
||||
"key": content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { content, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionTooltipButton: true,
|
||||
mx_ReactionTooltipButton_selected: !!myReactionEvent,
|
||||
});
|
||||
|
||||
return <span className={classes}
|
||||
data-key={content}
|
||||
title={this.props.title}
|
||||
aria-hidden={true}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{content}
|
||||
</span>;
|
||||
}
|
||||
}
|
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
195
src/components/views/messages/ReactionsQuickTooltip.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
|
||||
export default class ReactionsQuickTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
hoveredItem: null,
|
||||
myReactions: this.getMyReactions(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
this.onReactionsChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.reactions) {
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.add",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.remove",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
this.props.reactions.removeListener(
|
||||
"Relations.redaction",
|
||||
this.onReactionsChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
this.setState({
|
||||
myReactions: this.getMyReactions(),
|
||||
});
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
}
|
||||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onMouseOver = (ev) => {
|
||||
const { key } = ev.target.dataset;
|
||||
const item = this.items.find(({ content }) => content === key);
|
||||
this.setState({
|
||||
hoveredItem: item,
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut = (ev) => {
|
||||
this.setState({
|
||||
hoveredItem: null,
|
||||
});
|
||||
}
|
||||
|
||||
get items() {
|
||||
return [
|
||||
{
|
||||
content: "👍",
|
||||
title: _t("Agree"),
|
||||
},
|
||||
{
|
||||
content: "👎",
|
||||
title: _t("Disagree"),
|
||||
},
|
||||
{
|
||||
content: "😄",
|
||||
title: _t("Happy"),
|
||||
},
|
||||
{
|
||||
content: "🎉",
|
||||
title: _t("Party Popper"),
|
||||
},
|
||||
{
|
||||
content: "😕",
|
||||
title: _t("Confused"),
|
||||
},
|
||||
{
|
||||
content: "❤️",
|
||||
title: _t("Heart"),
|
||||
},
|
||||
{
|
||||
content: "🚀",
|
||||
title: _t("Rocket"),
|
||||
},
|
||||
{
|
||||
content: "👀",
|
||||
title: _t("Eyes"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent } = this.props;
|
||||
const { myReactions, hoveredItem } = this.state;
|
||||
const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton');
|
||||
|
||||
const buttons = this.items.map(({ content, title }) => {
|
||||
const myReactionEvent = myReactions && myReactions.find(mxEvent => {
|
||||
if (mxEvent.isRedacted()) {
|
||||
return false;
|
||||
}
|
||||
return mxEvent.getRelation().key === content;
|
||||
});
|
||||
|
||||
return <ReactionTooltipButton
|
||||
key={content}
|
||||
content={content}
|
||||
title={title}
|
||||
mxEvent={mxEvent}
|
||||
myReactionEvent={myReactionEvent}
|
||||
/>;
|
||||
});
|
||||
|
||||
let label = " "; // non-breaking space to keep layout the same when empty
|
||||
if (hoveredItem) {
|
||||
const { content, title } = hoveredItem;
|
||||
|
||||
let shortcodeLabel;
|
||||
const shortcode = unicodeToShortcode(content);
|
||||
if (shortcode) {
|
||||
shortcodeLabel = <span className="mx_ReactionsQuickTooltip_shortcode">
|
||||
{shortcode}
|
||||
</span>;
|
||||
}
|
||||
|
||||
label = <div className="mx_ReactionsQuickTooltip_label">
|
||||
<span className="mx_ReactionsQuickTooltip_title">
|
||||
{title}
|
||||
</span>
|
||||
{shortcodeLabel}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_ReactionsQuickTooltip"
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
<div className="mx_ReactionsQuickTooltip_buttons">
|
||||
{buttons}
|
||||
</div>
|
||||
{label}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -18,9 +18,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
@ -40,6 +44,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
showAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,16 +98,22 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
const { myReactions } = this.state;
|
||||
const { myReactions, showAll } = this.state;
|
||||
|
||||
if (!reactions || !isContentActionable(mxEvent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
const count = events.size;
|
||||
if (!count) {
|
||||
return null;
|
||||
|
@ -116,14 +127,31 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return <ReactionsRowButton
|
||||
key={content}
|
||||
content={content}
|
||||
count={count}
|
||||
mxEvent={mxEvent}
|
||||
reactionEvents={events}
|
||||
myReactionEvent={myReactionEvent}
|
||||
/>;
|
||||
});
|
||||
}).filter(item => !!item);
|
||||
|
||||
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
|
||||
// The "+ 1" ensure that the "show all" reveals something that takes up
|
||||
// more space than the button itself.
|
||||
let showAllButton;
|
||||
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
|
||||
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
|
||||
showAllButton = <a
|
||||
className="mx_ReactionsRow_showAll"
|
||||
href="#"
|
||||
onClick={this.onShowAllClick}
|
||||
>
|
||||
{_t("Show all")}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <div className="mx_ReactionsRow">
|
||||
{items}
|
||||
{showAllButton}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// The count of votes for this key
|
||||
count: PropTypes.number.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
|
@ -77,12 +79,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
render() {
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { content, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const count = reactionEvents.size;
|
||||
if (!count) {
|
||||
return null;
|
||||
}
|
||||
const { content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionsRowButton: true,
|
||||
|
@ -104,7 +101,12 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
onMouseOver={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
{content} {count}
|
||||
<span className="mx_ReactionsRowButton_content">
|
||||
{content}
|
||||
</span>
|
||||
<span className="mx_ReactionsRowButton_count">
|
||||
{count}
|
||||
</span>
|
||||
{tooltip}
|
||||
</span>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,7 +18,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -31,12 +31,21 @@ module.exports = React.createClass({
|
|||
mxEvent: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onAvatarClick: function(name) {
|
||||
const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
|
||||
onAvatarClick: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
|
||||
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(),
|
||||
roomName: room ? room.name : '',
|
||||
});
|
||||
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: name,
|
||||
name: text,
|
||||
};
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
@ -44,29 +53,22 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
senderDisplayName: senderDisplayName,
|
||||
roomName: room ? room.name : '',
|
||||
});
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
|
||||
return (
|
||||
<div className="mx_TextualEvent">
|
||||
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
|
||||
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const url = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
ev.getContent().url,
|
||||
Math.ceil(14 * window.devicePixelRatio),
|
||||
Math.ceil(14 * window.devicePixelRatio),
|
||||
'crop',
|
||||
);
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
// Provide all arguments to RoomAvatar via oobData because the avatar is historic
|
||||
const oobData = {
|
||||
avatarUrl: ev.getContent().url,
|
||||
name: room ? room.name : "",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx_RoomAvatarEvent">
|
||||
|
@ -75,8 +77,8 @@ module.exports = React.createClass({
|
|||
{
|
||||
'img': () =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={this.onAvatarClick.bind(this, name)}>
|
||||
<BaseAvatar width={14} height={14} url={url} name={name} />
|
||||
onClick={this.onAvatarClick}>
|
||||
<RoomAvatar width={14} height={14} oobData={oobData} />
|
||||
</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -25,17 +25,16 @@ import highlight from 'highlight.js';
|
|||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualBody',
|
||||
|
@ -90,7 +89,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
if (!this.props.isEditing) {
|
||||
if (!this.props.editState) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
},
|
||||
|
@ -101,7 +100,7 @@ module.exports = React.createClass({
|
|||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
this.pillifyLinks(this.refs.content.children);
|
||||
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||
HtmlUtils.linkifyElement(this.refs.content);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
|
@ -133,8 +132,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function(prevProps) {
|
||||
if (!this.props.isEditing) {
|
||||
const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
|
||||
if (!this.props.editState) {
|
||||
const stoppedEditing = prevProps.editState && !this.props.editState;
|
||||
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
|
||||
if (messageWasEdited || stoppedEditing) {
|
||||
this._applyFormatting();
|
||||
|
@ -155,7 +154,7 @@ module.exports = React.createClass({
|
|||
nextProps.replacingEventId !== this.props.replacingEventId ||
|
||||
nextProps.highlightLink !== this.props.highlightLink ||
|
||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||
nextProps.isEditing !== this.props.isEditing ||
|
||||
nextProps.editState !== this.props.editState ||
|
||||
nextState.links !== this.state.links ||
|
||||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
|
||||
nextState.widgetHidden !== this.state.widgetHidden);
|
||||
|
@ -214,98 +213,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
let pillified = false;
|
||||
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
|
||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
if (Pill.isMessagePillUrl(href)) {
|
||||
const pillContainer = document.createElement('span');
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
url={href}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
node.parentNode.replaceChild(pillContainer, node);
|
||||
// Pills within pills aren't going to go well, so move on
|
||||
pillified = true;
|
||||
|
||||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
}
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
const roomNotifTextNodes = [];
|
||||
|
||||
// Take a textNode and break it up to make all the instances of @room their
|
||||
// own textNode, adding those nodes to roomNotifTextNodes
|
||||
while (currentTextNode !== null) {
|
||||
const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent);
|
||||
let nextTextNode = null;
|
||||
if (roomNotifPos > -1) {
|
||||
let roomTextNode = currentTextNode;
|
||||
|
||||
if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos);
|
||||
if (roomTextNode.textContent.length > Pill.roomNotifLen()) {
|
||||
nextTextNode = roomTextNode.splitText(Pill.roomNotifLen());
|
||||
}
|
||||
roomNotifTextNodes.push(roomTextNode);
|
||||
}
|
||||
currentTextNode = nextTextNode;
|
||||
}
|
||||
|
||||
if (roomNotifTextNodes.length > 0) {
|
||||
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
||||
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
||||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
||||
// Now replace all those nodes with Pills
|
||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
|
||||
const pillContainer = document.createElement('span');
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={true}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
}
|
||||
// Nothing else to do for a text node (and we don't need to advance
|
||||
// the loop pointer because we did it above)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.childNodes && node.childNodes.length && !pillified) {
|
||||
this.pillifyLinks(node.childNodes);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
},
|
||||
|
||||
findLinks: function(nodes) {
|
||||
let links = [];
|
||||
|
||||
|
@ -441,12 +348,19 @@ module.exports = React.createClass({
|
|||
// which requires the user to click through and THEN we can open the link in a new tab because
|
||||
// the window.open command occurs in the same stack frame as the onClick callback.
|
||||
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
if (!managers.hasManager()) {
|
||||
managers.openNoManagerDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// Go fetch a scalar token
|
||||
const scalarClient = new ScalarAuthClient();
|
||||
const integrationManager = managers.getPrimaryManager();
|
||||
const scalarClient = integrationManager.getScalarClient();
|
||||
scalarClient.connect().then(() => {
|
||||
const completeUrl = scalarClient.getStarterLink(starterLink);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||
const integrationsUrl = integrationManager.uiUrl;
|
||||
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
|
||||
title: _t("Add an Integration"),
|
||||
description:
|
||||
|
@ -478,20 +392,26 @@ module.exports = React.createClass({
|
|||
this.setState({editedMarkerHovered: false});
|
||||
},
|
||||
|
||||
_openHistoryDialog: async function() {
|
||||
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
|
||||
},
|
||||
|
||||
_renderEditedMarker: function() {
|
||||
let editedTooltip;
|
||||
if (this.state.editedMarkerHovered) {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const editEvent = this.props.mxEvent.replacingEvent();
|
||||
const date = editEvent && formatDate(editEvent.getDate());
|
||||
const date = this.props.mxEvent.replacingEventDate();
|
||||
const dateString = date && formatDate(date);
|
||||
editedTooltip = <Tooltip
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
label={_t("Edited at %(date)s", {date})}
|
||||
label={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key="editedMarker" className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||
|
@ -499,9 +419,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.isEditing) {
|
||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
||||
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
|
||||
if (this.props.editState) {
|
||||
const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
|
||||
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
||||
}
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue