Merge branch 'develop' into gsouquet/message-bubbles-4635
This commit is contained in:
commit
06c9a060b4
171 changed files with 3649 additions and 2694 deletions
|
@ -90,6 +90,35 @@ function computedStyle(element) {
|
|||
return cssText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a human readable label for the file attachment to use as
|
||||
* link text.
|
||||
*
|
||||
* @param {Object} content The "content" key of the matrix event.
|
||||
* @param {boolean} withSize Whether to include size information. Default true.
|
||||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
export function presentableTextForFile(content, withSize = true) {
|
||||
let linkText = _t("Attachment");
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
// file extension.
|
||||
linkText = content.body;
|
||||
}
|
||||
|
||||
if (content.info && content.info.size && withSize) {
|
||||
// If we know the size of the file then add it as human readable
|
||||
// string to the end of the link text so that the user knows how
|
||||
// big a file they are downloading.
|
||||
// The content.info also contains a MIME-type but we don't display
|
||||
// it since it is "ugly", users generally aren't aware what it
|
||||
// means and the type of the attachment can usually be inferrered
|
||||
// from the file extension.
|
||||
linkText += ' (' + filesize(content.info.size) + ')';
|
||||
}
|
||||
return linkText;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MFileBody")
|
||||
export default class MFileBody extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -120,35 +149,6 @@ export default class MFileBody extends React.Component {
|
|||
this._dummyLink = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a human readable label for the file attachment to use as
|
||||
* link text.
|
||||
*
|
||||
* @param {Object} content The "content" key of the matrix event.
|
||||
* @param {boolean} withSize Whether to include size information. Default true.
|
||||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
presentableTextForFile(content, withSize = true) {
|
||||
let linkText = _t("Attachment");
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
// file extension.
|
||||
linkText = content.body;
|
||||
}
|
||||
|
||||
if (content.info && content.info.size && withSize) {
|
||||
// If we know the size of the file then add it as human readable
|
||||
// string to the end of the link text so that the user knows how
|
||||
// big a file they are downloading.
|
||||
// The content.info also contains a MIME-type but we don't display
|
||||
// it since it is "ugly", users generally aren't aware what it
|
||||
// means and the type of the attachment can usually be inferrered
|
||||
// from the file extension.
|
||||
linkText += ' (' + filesize(content.info.size) + ')';
|
||||
}
|
||||
return linkText;
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
|
@ -162,7 +162,7 @@ export default class MFileBody extends React.Component {
|
|||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const text = this.presentableTextForFile(content);
|
||||
const text = presentableTextForFile(content);
|
||||
const isEncrypted = content.file !== undefined;
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
||||
const contentUrl = this._getContentUrl();
|
||||
|
@ -174,7 +174,9 @@ export default class MFileBody extends React.Component {
|
|||
placeholder = (
|
||||
<div className="mx_MFileBody_info">
|
||||
<span className="mx_MFileBody_info_icon" />
|
||||
<span className="mx_MFileBody_info_filename">{this.presentableTextForFile(content, false)}</span>
|
||||
<span className="mx_MFileBody_info_filename">
|
||||
{ presentableTextForFile(content, false) }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,13 +16,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ComponentProps, createRef } from 'react';
|
||||
import { Blurhash } from "react-blurhash";
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -31,36 +29,49 @@ import InlineSpinner from '../elements/InlineSpinner';
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||
|
||||
export interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: MatrixEvent;
|
||||
/* called when the image has loaded */
|
||||
onHeightChanged(): void;
|
||||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight?: number;
|
||||
|
||||
/* the permalinkCreator */
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
decryptedUrl?: string;
|
||||
decryptedThumbnailUrl?: string;
|
||||
decryptedBlob?: Blob;
|
||||
error;
|
||||
imgError: boolean;
|
||||
imgLoaded: boolean;
|
||||
loadedImageDimensions?: {
|
||||
naturalWidth: number;
|
||||
naturalHeight: number;
|
||||
};
|
||||
hover: boolean;
|
||||
showImage: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MImageBody")
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onHeightChanged: PropTypes.func.isRequired,
|
||||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight: PropTypes.number,
|
||||
|
||||
/* the permalinkCreator */
|
||||
permalinkCreator: PropTypes.object,
|
||||
};
|
||||
|
||||
export default class MImageBody extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
private unmounted = true;
|
||||
private image = createRef<HTMLImageElement>();
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.onImageError = this.onImageError.bind(this);
|
||||
this.onImageLoad = this.onImageLoad.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
|
@ -72,12 +83,10 @@ export default class MImageBody extends React.Component {
|
|||
hover: false,
|
||||
showImage: SettingsStore.getValue("showImages"),
|
||||
};
|
||||
|
||||
this._image = createRef();
|
||||
}
|
||||
|
||||
// FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
|
||||
onClientSync(syncState, prevState) {
|
||||
private onClientSync = (syncState: SyncState, prevState: SyncState): void => {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
|
@ -88,15 +97,15 @@ export default class MImageBody extends React.Component {
|
|||
imgError: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
showImage() {
|
||||
protected showImage(): void {
|
||||
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
|
||||
this.setState({ showImage: true });
|
||||
this._downloadImage();
|
||||
this.downloadImage();
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
protected onClick = (ev: React.MouseEvent): void => {
|
||||
if (ev.button === 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
if (!this.state.showImage) {
|
||||
|
@ -104,12 +113,11 @@ export default class MImageBody extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const httpUrl = this._getContentUrl();
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
const httpUrl = this.getContentUrl();
|
||||
const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
|
||||
src: httpUrl,
|
||||
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
|
||||
name: content.body?.length > 0 ? content.body : _t('Attachment'),
|
||||
mxEvent: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
};
|
||||
|
@ -122,58 +130,54 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_isGif() {
|
||||
private isGif = (): boolean => {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
}
|
||||
return content.info?.mimetype === "image/gif";
|
||||
};
|
||||
|
||||
onImageEnter(e) {
|
||||
private onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => {
|
||||
this.setState({ hover: true });
|
||||
|
||||
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getContentUrl();
|
||||
}
|
||||
const imgElement = e.currentTarget;
|
||||
imgElement.src = this.getContentUrl();
|
||||
};
|
||||
|
||||
onImageLeave(e) {
|
||||
private onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => {
|
||||
this.setState({ hover: false });
|
||||
|
||||
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getThumbUrl();
|
||||
}
|
||||
const imgElement = e.currentTarget;
|
||||
imgElement.src = this.getThumbUrl();
|
||||
};
|
||||
|
||||
onImageError() {
|
||||
private onImageError = (): void => {
|
||||
this.setState({
|
||||
imgError: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onImageLoad() {
|
||||
private onImageLoad = (): void => {
|
||||
this.props.onHeightChanged();
|
||||
|
||||
let loadedImageDimensions;
|
||||
|
||||
if (this._image.current) {
|
||||
const { naturalWidth, naturalHeight } = this._image.current;
|
||||
if (this.image.current) {
|
||||
const { naturalWidth, naturalHeight } = this.image.current;
|
||||
// this is only used as a fallback in case content.info.w/h is missing
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
}
|
||||
};
|
||||
|
||||
_getContentUrl() {
|
||||
protected getContentUrl(): string {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
|
@ -182,7 +186,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getThumbUrl() {
|
||||
protected getThumbUrl(): string {
|
||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
// thumbnail resolution will be unnecessarily reduced.
|
||||
|
@ -190,7 +194,7 @@ export default class MImageBody extends React.Component {
|
|||
const thumbWidth = 800;
|
||||
const thumbHeight = 600;
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
const media = mediaFromContent(content);
|
||||
|
||||
if (media.isEncrypted) {
|
||||
|
@ -218,7 +222,7 @@ export default class MImageBody extends React.Component {
|
|||
// - If there's no sizing info in the event, default to thumbnail
|
||||
const info = content.info;
|
||||
if (
|
||||
this._isGif() ||
|
||||
this.isGif() ||
|
||||
window.devicePixelRatio === 1.0 ||
|
||||
(!info || !info.w || !info.h || !info.size)
|
||||
) {
|
||||
|
@ -253,7 +257,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_downloadImage() {
|
||||
private downloadImage(): void {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
|
@ -297,7 +301,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
if (showImage) {
|
||||
// Don't download anything becaue we don't want to display anything.
|
||||
this._downloadImage();
|
||||
this.downloadImage();
|
||||
this.setState({ showImage: true });
|
||||
}
|
||||
|
||||
|
@ -312,7 +316,6 @@ export default class MImageBody extends React.Component {
|
|||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
|
||||
if (this.state.decryptedUrl) {
|
||||
URL.revokeObjectURL(this.state.decryptedUrl);
|
||||
|
@ -322,12 +325,12 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||
// cleanup after componentWillUnmount
|
||||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
protected messageContent(
|
||||
contentUrl: string,
|
||||
thumbUrl: string,
|
||||
content: IMediaEventContent,
|
||||
forcedHeight?: number,
|
||||
): JSX.Element {
|
||||
let infoWidth;
|
||||
let infoHeight;
|
||||
|
||||
|
@ -348,7 +351,7 @@ export default class MImageBody extends React.Component {
|
|||
imageElement = <HiddenImagePlaceholder />;
|
||||
} else {
|
||||
imageElement = (
|
||||
<img style={{ display: 'none' }} src={thumbUrl} ref={this._image}
|
||||
<img style={{ display: 'none' }} src={thumbUrl} ref={this.image}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
|
@ -362,7 +365,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
const maxHeight = forcedHeight || Math.min((this.props.maxImageHeight || 600), infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
@ -382,7 +385,7 @@ export default class MImageBody extends React.Component {
|
|||
// which has the same width as the timeline
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = (
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image}
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this.image}
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
|
@ -393,18 +396,18 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
if (!this.state.showImage) {
|
||||
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
||||
img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
|
||||
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||
}
|
||||
|
||||
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||
if (this.isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
||||
}
|
||||
|
||||
const thumbnail = (
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }} >
|
||||
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||
<div style={{ paddingBottom: forcedHeight ? (forcedHeight + "px") : ((100 * infoHeight / infoWidth) + '%') }} />
|
||||
{ showPlaceholder &&
|
||||
<div className="mx_MImageBody_thumbnail" style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
|
@ -427,14 +430,14 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
wrapImage(contentUrl, children) {
|
||||
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
{children}
|
||||
</a>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder(width, height) {
|
||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
||||
return <div className="mx_MImageBody_thumbnail_spinner">
|
||||
|
@ -443,17 +446,17 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getTooltip() {
|
||||
protected getTooltip(): JSX.Element {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getFileBody() {
|
||||
protected getFileBody(): JSX.Element {
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
|
@ -464,15 +467,15 @@ export default class MImageBody extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const contentUrl = this.getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
if (this.isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
thumbUrl = this.getThumbUrl();
|
||||
}
|
||||
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <span className="mx_MImageBody">
|
||||
|
@ -482,16 +485,18 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export class HiddenImagePlaceholder extends React.PureComponent {
|
||||
static propTypes = {
|
||||
hover: PropTypes.bool,
|
||||
};
|
||||
interface PlaceholderIProps {
|
||||
hover?: boolean;
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
|
||||
render() {
|
||||
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
|
||||
let className = 'mx_HiddenImagePlaceholder';
|
||||
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={className} style={{ maxWidth: maxWidth }}>
|
||||
<div className='mx_HiddenImagePlaceholder_button'>
|
||||
<span className='mx_HiddenImagePlaceholder_eye' />
|
||||
<span>{_t("Show image")}</span>
|
62
src/components/views/messages/MImageReplyBody.tsx
Normal file
62
src/components/views/messages/MImageReplyBody.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2020-2021 Tulir Asokan <tulir@maunium.net>
|
||||
|
||||
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 MImageBody from "./MImageBody";
|
||||
import { presentableTextForFile } from "./MFileBody";
|
||||
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
||||
import SenderProfile from "./SenderProfile";
|
||||
|
||||
const FORCED_IMAGE_HEIGHT = 44;
|
||||
|
||||
export default class MImageReplyBody extends MImageBody {
|
||||
public onClick = (ev: React.MouseEvent): void => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
public wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Don't show "Download this_file.png ..."
|
||||
public getFileBody(): JSX.Element {
|
||||
return presentableTextForFile(this.props.mxEvent.getContent());
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error !== null) {
|
||||
return super.render();
|
||||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||
|
||||
const contentUrl = this.getContentUrl();
|
||||
const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content, FORCED_IMAGE_HEIGHT);
|
||||
const fileBody = this.getFileBody();
|
||||
const sender = <SenderProfile
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={false}
|
||||
/>;
|
||||
|
||||
return <div className="mx_MImageReplyBody">
|
||||
{ thumbnail }
|
||||
<div className="mx_MImageReplyBody_info">
|
||||
<div className="mx_MImageReplyBody_sender">{ sender }</div>
|
||||
<div className="mx_MImageReplyBody_filename">{ fileBody }</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -47,6 +47,10 @@ export default class MessageEvent extends React.Component {
|
|||
/* the maximum image height to use, if the event is an image */
|
||||
maxImageHeight: PropTypes.number,
|
||||
|
||||
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
|
||||
overrideBodyTypes: PropTypes.object,
|
||||
overrideEventTypes: PropTypes.object,
|
||||
|
||||
/* the permalinkCreator */
|
||||
permalinkCreator: PropTypes.object,
|
||||
};
|
||||
|
@ -74,9 +78,12 @@ export default class MessageEvent extends React.Component {
|
|||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
|
||||
...(this.props.overrideBodyTypes || {}),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
...(this.props.overrideEventTypes || {}),
|
||||
};
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -113,7 +120,7 @@ export default class MessageEvent extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
return <BodyType
|
||||
return BodyType ? <BodyType
|
||||
ref={this._body}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
@ -126,6 +133,6 @@ export default class MessageEvent extends React.Component {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
onMessageAllowed={this.onTileUpdate}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>;
|
||||
/> : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -36,7 +38,7 @@ interface IState {
|
|||
@replaceableComponent("views.messages.SenderProfile")
|
||||
export default class SenderProfile extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
private unmounted: boolean;
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -49,8 +51,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
this.updateRelatedGroups();
|
||||
|
||||
if (this.state.userGroups.length === 0) {
|
||||
this.getPublicisedGroups();
|
||||
|
@ -64,35 +65,29 @@ 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 });
|
||||
}
|
||||
private async getPublicisedGroups() {
|
||||
const userGroups = await FlairStore.getPublicisedGroupsCached(this.context, this.props.mxEvent.getSender());
|
||||
if (this.unmounted) return;
|
||||
this.setState({ userGroups });
|
||||
}
|
||||
|
||||
onRoomStateEvents = event => {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
) {
|
||||
this._updateRelatedGroups();
|
||||
private onRoomStateEvents = (event: MatrixEvent) => {
|
||||
if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId()) {
|
||||
this.updateRelatedGroups();
|
||||
}
|
||||
};
|
||||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
private updateRelatedGroups() {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
|
||||
relatedGroups: relatedGroupsEvent?.getContent().groups || [],
|
||||
});
|
||||
}
|
||||
|
||||
_getDisplayedGroups(userGroups, relatedGroups) {
|
||||
private getDisplayedGroups(userGroups?: string[], relatedGroups?: string[]) {
|
||||
let displayedGroups = userGroups || [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
displayedGroups = relatedGroups.filter((groupId) => {
|
||||
|
@ -113,7 +108,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
|
||||
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
if (msgtype === MsgType.Emote) {
|
||||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
|
@ -128,7 +123,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
|
||||
let flair;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
const displayedGroups = this.getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
);
|
||||
|
||||
|
|
|
@ -14,9 +14,10 @@ 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 React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -26,11 +27,11 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.messages.TextualEvent")
|
||||
export default class TextualEvent extends React.Component<IProps> {
|
||||
render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
||||
if (!text || (text as string).length === 0) return null;
|
||||
return (
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
);
|
||||
static contextType = RoomContext;
|
||||
|
||||
public render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
|
||||
if (!text) return null;
|
||||
return <div className="mx_TextualEvent">{ text }</div>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue