Merge branch 'develop' into show-username
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
025148d9fc
454 changed files with 20625 additions and 7587 deletions
|
@ -160,7 +160,6 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
"mx_EventTile": true,
|
||||
// Note: we keep the `sending` state class for tests, not for our styles
|
||||
"mx_EventTile_sending": isSending,
|
||||
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
|
||||
});
|
||||
return (
|
||||
<li>
|
||||
|
|
|
@ -125,9 +125,9 @@ export default class MImageBody extends React.Component {
|
|||
_isGif() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -185,9 +185,8 @@ export default class MImageBody extends React.Component {
|
|||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
// thumbnail resolution will be unnecessarily reduced.
|
||||
// custom timeline widths seems preferable.
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const thumbWidth = Math.round(800 * pixelRatio);
|
||||
const thumbHeight = Math.round(600 * pixelRatio);
|
||||
const thumbWidth = 800;
|
||||
const thumbHeight = 600;
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
|
@ -218,7 +217,7 @@ export default class MImageBody extends React.Component {
|
|||
const info = content.info;
|
||||
if (
|
||||
this._isGif() ||
|
||||
pixelRatio === 1.0 ||
|
||||
window.devicePixelRatio === 1.0 ||
|
||||
(!info || !info.w || !info.h || !info.size)
|
||||
) {
|
||||
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||
|
@ -347,9 +346,9 @@ export default class MImageBody extends React.Component {
|
|||
} else {
|
||||
imageElement = (
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -385,12 +384,12 @@ export default class MImageBody extends React.Component {
|
|||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = (
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -468,9 +467,9 @@ export default class MImageBody extends React.Component {
|
|||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
|
|
|
@ -82,9 +82,7 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
}
|
||||
|
||||
// User isn't actually verified
|
||||
if (!MatrixClientPeg.get()
|
||||
.checkUserTrust(request.otherUserId)
|
||||
.isCrossSigningVerified()) {
|
||||
if (!MatrixClientPeg.get().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
110
src/components/views/messages/MVoiceMessageBody.tsx
Normal file
110
src/components/views/messages/MVoiceMessageBody.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2021 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 {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {Playback} from "../../../voice/Playback";
|
||||
import MFileBody from "./MFileBody";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
import {decryptFile} from "../../../utils/DecryptFile";
|
||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error?: Error;
|
||||
playback?: Playback;
|
||||
decryptedBlob?: Blob;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVoiceMessageBody")
|
||||
export default class MVoiceMessageBody extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
let buffer: ArrayBuffer;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
try {
|
||||
const blob = await decryptFile(content.file);
|
||||
buffer = await blob.arrayBuffer();
|
||||
this.setState({decryptedBlob: blob});
|
||||
} catch (e) {
|
||||
this.setState({error: e});
|
||||
console.warn("Unable to decrypt voice message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
|
||||
} catch (e) {
|
||||
this.setState({error: e});
|
||||
console.warn("Unable to download voice message", e);
|
||||
return; // stop processing the audio file
|
||||
}
|
||||
}
|
||||
|
||||
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
|
||||
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = new Playback(buffer, waveform);
|
||||
this.setState({ playback });
|
||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.playback?.destroy();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error) {
|
||||
// TODO: @@TR: Verify error state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
|
||||
{ _t("Error processing voice message") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.playback) {
|
||||
// TODO: @@TR: Verify loading/decrypting state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<InlineSpinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// At this point we should have a playable state
|
||||
return (
|
||||
<span className="mx_MVoiceMessageBody">
|
||||
<RecordingPlayback playback={this.state.playback} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
39
src/components/views/messages/MVoiceOrAudioBody.tsx
Normal file
39
src/components/views/messages/MVoiceOrAudioBody.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2021 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 {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import MAudioBody from "./MAudioBody";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MVoiceMessageBody from "./MVoiceMessageBody";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||
public render() {
|
||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||
if (isVoiceMessage && voiceMessagesEnabled) {
|
||||
return <MVoiceMessageBody {...this.props} />;
|
||||
} else {
|
||||
return <MAudioBody {...this.props} />;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,6 +29,9 @@ import RoomContext from "../../../contexts/RoomContext";
|
|||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {canCancel} from "../context_menus/MessageContextMenu";
|
||||
import Resend from "../../../Resend";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -120,6 +123,10 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
|
||||
this.props.mxEvent.on("Event.status", this.onSent);
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(this.props.mxEvent);
|
||||
|
||||
if (this.props.mxEvent.isBeingDecrypted()) {
|
||||
this.props.mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
@ -169,45 +176,118 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let reactButton;
|
||||
let replyButton;
|
||||
let editButton;
|
||||
/**
|
||||
* Runs a given fn on the set of possible events to test. The first event
|
||||
* that passes the checkFn will have fn executed on it. Both functions take
|
||||
* a MatrixEvent object. If no particular conditions are needed, checkFn can
|
||||
* be null/undefined. If no functions pass the checkFn, no action will be
|
||||
* taken.
|
||||
* @param {Function} fn The execution function.
|
||||
* @param {Function} checkFn The test function.
|
||||
*/
|
||||
runActionOnFailedEv(fn, checkFn) {
|
||||
if (!checkFn) checkFn = () => true;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
if (this.context.canReact) {
|
||||
reactButton = (
|
||||
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
|
||||
);
|
||||
}
|
||||
if (this.context.canReply) {
|
||||
replyButton = <RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
/>;
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const editEvent = mxEvent.replacingEvent();
|
||||
const redactEvent = mxEvent.localRedactionEvent();
|
||||
const tryOrder = [redactEvent, editEvent, mxEvent];
|
||||
for (const ev of tryOrder) {
|
||||
if (ev && checkFn(ev)) {
|
||||
fn(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResendClick = (ev) => {
|
||||
this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv));
|
||||
};
|
||||
|
||||
onCancelClick = (ev) => {
|
||||
this.runActionOnFailedEv(
|
||||
(tarEv) => Resend.removeFromQueue(tarEv),
|
||||
(testEv) => canCancel(testEv.status),
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const toolbarOpts = [];
|
||||
if (canEditContent(this.props.mxEvent)) {
|
||||
editButton = <RovingAccessibleTooltipButton
|
||||
toolbarOpts.push(<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
/>;
|
||||
key="edit"
|
||||
/>);
|
||||
}
|
||||
|
||||
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
|
||||
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
|
||||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
<OptionsButton
|
||||
const cancelSendingButton = <RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_cancelButton"
|
||||
title={_t("Delete")}
|
||||
onClick={this.onCancelClick}
|
||||
key="cancel"
|
||||
/>;
|
||||
|
||||
// We show a different toolbar for failed events, so detect that first.
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
|
||||
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
|
||||
const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus);
|
||||
const isFailed = [mxEvent.status, editStatus, redactStatus].includes("not_sent");
|
||||
if (allowCancel && isFailed) {
|
||||
// The resend button needs to appear ahead of the edit button, so insert to the
|
||||
// start of the opts
|
||||
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_resendButton"
|
||||
title={_t("Retry")}
|
||||
onClick={this.onResendClick}
|
||||
key="resend"
|
||||
/>);
|
||||
|
||||
// The delete button should appear last, so we can just drop it at the end
|
||||
toolbarOpts.push(cancelSendingButton);
|
||||
} else {
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
// Like the resend button, the react and reply buttons need to appear before the edit.
|
||||
// The only catch is we do the reply button first so that we can make sure the react
|
||||
// button is the very first button without having to do length checks for `splice()`.
|
||||
if (this.context.canReply) {
|
||||
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
key="reply"
|
||||
/>);
|
||||
}
|
||||
if (this.context.canReact) {
|
||||
toolbarOpts.splice(0, 0, <ReactButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.props.reactions}
|
||||
onFocusChange={this.onFocusChange}
|
||||
key="react"
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowCancel) {
|
||||
toolbarOpts.push(cancelSendingButton);
|
||||
}
|
||||
|
||||
// The menu button should be last, so dump it there.
|
||||
toolbarOpts.push(<OptionsButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
getReplyThread={this.props.getReplyThread}
|
||||
getTile={this.props.getTile}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>
|
||||
key="menu"
|
||||
/>);
|
||||
}
|
||||
|
||||
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
|
||||
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
|
||||
{toolbarOpts}
|
||||
</Toolbar>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,12 +72,8 @@ export default class MessageEvent extends React.Component {
|
|||
'm.emote': sdk.getComponent('messages.TextualBody'),
|
||||
'm.image': sdk.getComponent('messages.MImageBody'),
|
||||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
|
||||
// TODO: @@ TravisR: Use labs flag determination.
|
||||
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
|
||||
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 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.
|
||||
|
@ -14,35 +14,72 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
|
||||
import ReactionPicker from "../emojipicker/ReactionPicker";
|
||||
import ReactionsRowButton from "./ReactionsRowButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: PropTypes.object,
|
||||
const ReactButton = ({ mxEvent, reactions }: IProps) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className={classNames("mx_ReactionsRow_addReactionButton", {
|
||||
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
|
||||
})}
|
||||
title={_t("Add reaction")}
|
||||
onClick={openMenu}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
openMenu();
|
||||
}}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
if (props.reactions) {
|
||||
props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
props.reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions?: Relations;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
myReactions: MatrixEvent[];
|
||||
showAll: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
myReactions: this.getMyReactions(),
|
||||
|
@ -50,7 +87,33 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidMount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) {
|
||||
mxEvent.once("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
if (reactions) {
|
||||
reactions.on("Relations.add", this.onReactionsChange);
|
||||
reactions.on("Relations.remove", this.onReactionsChange);
|
||||
reactions.on("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { mxEvent, reactions } = this.props;
|
||||
|
||||
mxEvent.off("Event.decrypted", this.onDecrypted);
|
||||
|
||||
if (reactions) {
|
||||
reactions.off("Relations.add", this.onReactionsChange);
|
||||
reactions.off("Relations.remove", this.onReactionsChange);
|
||||
reactions.off("Relations.redaction", this.onReactionsChange);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (prevProps.reactions !== this.props.reactions) {
|
||||
this.props.reactions.on("Relations.add", this.onReactionsChange);
|
||||
this.props.reactions.on("Relations.remove", this.onReactionsChange);
|
||||
|
@ -59,24 +122,12 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
private onDecrypted = () => {
|
||||
// Decryption changes whether the event is actionable
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
onReactionsChange = () => {
|
||||
private onReactionsChange = () => {
|
||||
// TODO: Call `onHeightChanged` as needed
|
||||
this.setState({
|
||||
myReactions: this.getMyReactions(),
|
||||
|
@ -87,12 +138,12 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
this.forceUpdate();
|
||||
}
|
||||
|
||||
getMyReactions() {
|
||||
private getMyReactions() {
|
||||
const reactions = this.props.reactions;
|
||||
if (!reactions) {
|
||||
return null;
|
||||
}
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const userId = this.context.getUserId();
|
||||
const myReactions = reactions.getAnnotationsBySender()[userId];
|
||||
if (!myReactions) {
|
||||
return null;
|
||||
|
@ -100,7 +151,7 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return [...myReactions.values()];
|
||||
}
|
||||
|
||||
onShowAllClick = () => {
|
||||
private onShowAllClick = () => {
|
||||
this.setState({
|
||||
showAll: true,
|
||||
});
|
||||
|
@ -114,7 +165,6 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
const count = events.size;
|
||||
if (!count) {
|
||||
|
@ -136,6 +186,8 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
/>;
|
||||
}).filter(item => !!item);
|
||||
|
||||
if (!items.length) return null;
|
||||
|
||||
// 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.
|
||||
|
@ -151,13 +203,22 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
</a>;
|
||||
}
|
||||
|
||||
const cli = this.context;
|
||||
|
||||
let addReactionButton;
|
||||
const room = cli.getRoom(mxEvent.getRoomId());
|
||||
if (room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
|
||||
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
|
||||
}
|
||||
|
||||
return <div
|
||||
className="mx_ReactionsRow"
|
||||
role="toolbar"
|
||||
aria-label={_t("Reactions")}
|
||||
>
|
||||
{items}
|
||||
{showAllButton}
|
||||
{ items }
|
||||
{ showAllButton }
|
||||
{ addReactionButton }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 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.
|
||||
|
@ -14,49 +14,54 @@ 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 React from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// The count of votes for this key
|
||||
count: number;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
// A possible Matrix event if the current user has voted for this type
|
||||
myReactionEvent?: MatrixEvent;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
tooltipRendered: boolean;
|
||||
tooltipVisible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButton")
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
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
|
||||
myReactionEvent: PropTypes.object,
|
||||
}
|
||||
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
state = {
|
||||
tooltipRendered: false,
|
||||
tooltipVisible: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
tooltipVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
onClick = () => {
|
||||
const { mxEvent, myReactionEvent, content } = this.props;
|
||||
if (myReactionEvent) {
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.context.redactEvent(
|
||||
mxEvent.getRoomId(),
|
||||
myReactionEvent.getId(),
|
||||
);
|
||||
} else {
|
||||
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": mxEvent.getId(),
|
||||
|
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const ReactionsRowButtonTooltip =
|
||||
sdk.getComponent('messages.ReactionsRowButtonTooltip');
|
||||
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
/>;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let label;
|
||||
if (room) {
|
||||
const senders = [];
|
||||
|
@ -129,12 +132,12 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const isPeeking = room.getMyMembership() !== "join";
|
||||
return <AccessibleButton
|
||||
className={classes}
|
||||
aria-label={label}
|
||||
onClick={this.onClick}
|
||||
disabled={isPeeking}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 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.
|
||||
|
@ -14,33 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: MatrixEvent;
|
||||
// The reaction content / key / emoji
|
||||
content: string;
|
||||
// A Set of Matrix reaction events for this key
|
||||
reactionEvents: Set<MatrixEvent>;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
// The reaction content / key / emoji
|
||||
content: PropTypes.string.isRequired,
|
||||
// A Set of Martix reaction events for this key
|
||||
reactionEvents: PropTypes.object.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
}
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
render() {
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const { content, reactionEvents, mxEvent, visible } = this.props;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||
let tooltipLabel;
|
||||
if (room) {
|
||||
const senders = [];
|
|
@ -40,9 +40,10 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
const senderId = this.props.mxEvent.getSender();
|
||||
|
||||
this.state = {
|
||||
userGroups: null,
|
||||
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||
relatedGroups: [],
|
||||
};
|
||||
}
|
||||
|
@ -51,12 +52,10 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
if (this.state.userGroups.length === 0) {
|
||||
this.getPublicisedGroups();
|
||||
}
|
||||
|
||||
|
||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
@ -66,6 +65,15 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
async getPublicisedGroups() {
|
||||
if (!this.unmounted) {
|
||||
const userGroups = await FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
);
|
||||
this.setState({userGroups});
|
||||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = event => {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
|
@ -107,7 +115,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair;
|
||||
|
|
|
@ -36,6 +36,7 @@ import {toRightOf} from "../../structures/ContextMenu";
|
|||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
|
@ -143,7 +144,7 @@ export default class TextualBody extends React.Component {
|
|||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
||||
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||
if (percentageOfViewport < 30) return;
|
||||
|
||||
const button = document.createElement("span");
|
||||
|
@ -277,15 +278,15 @@ export default class TextualBody extends React.Component {
|
|||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||
let links = this.findLinks([this._content.current]);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
links = links.filter((link) => {
|
||||
if (seen.has(link)) return false;
|
||||
seen.add(link);
|
||||
return true;
|
||||
});
|
||||
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||
// using a set here maintains the order
|
||||
links = Array.from(new Set(links.map(link => {
|
||||
const url = new URL(link);
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
})));
|
||||
|
||||
this.setState({ links: links });
|
||||
this.setState({ links });
|
||||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
|
@ -521,11 +522,12 @@ export default class TextualBody extends React.Component {
|
|||
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||
widgets = this.state.links.map((link)=>{
|
||||
return <LinkPreviewWidget
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged} />;
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
@replaceableComponent("views.messages.ViewSourceEvent")
|
||||
export default class ViewSourceEvent extends React.PureComponent {
|
||||
|
@ -36,6 +37,10 @@ export default class ViewSourceEvent extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
const {mxEvent} = this.props;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(mxEvent);
|
||||
|
||||
if (mxEvent.isBeingDecrypted()) {
|
||||
mxEvent.once("Event.decrypted", () => this.forceUpdate());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue