From fe674e894880967d2f7308beacf9caf9722df2cf Mon Sep 17 00:00:00 2001 From: Andrew Ryan Date: Mon, 21 Feb 2022 14:51:58 -0800 Subject: [PATCH] Render image data in reactions Many messaging services such as Slack and Discord allow for custom images for reacts, rather than only standard emoji. Currently Element will only show the plain text of the reaction key. This PR updates element to render the media content from reaction events. This does not add a way for users to add custom reactions yet. This can be useful in the case of bots and bridges, which will be able to immediately use this functinality. A picker for Element users will be added in the future. --- src/HtmlUtils.tsx | 2 +- src/components/views/messages/MImageBody.tsx | 2 +- .../views/messages/ReactionImage.tsx | 64 +++++++++++++++++++ .../views/messages/ReactionsRowButton.tsx | 31 ++++++++- src/utils/MediaEventHelper.ts | 2 +- 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/components/views/messages/ReactionImage.tsx diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 2f574351c94..cdf08232de2 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -102,7 +102,7 @@ function mightContainEmoji(str: string): boolean { */ export function unicodeToShortcode(char: string): string { const shortcodes = getEmojiFromUnicode(char)?.shortcodes; - return shortcodes?.length ? `:${shortcodes[0]}:` : ''; + return shortcodes?.length ? `:${shortcodes[0]}:` : char; } export function processHtmlForSending(html: string): string { diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 2d62ce6ce7d..a894dbdd282 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -139,7 +139,7 @@ export default class MImageBody extends React.Component { } }; - private isGif = (): boolean => { + protected isGif = (): boolean => { const content = this.props.mxEvent.getContent(); return content.info?.mimetype === "image/gif"; }; diff --git a/src/components/views/messages/ReactionImage.tsx b/src/components/views/messages/ReactionImage.tsx new file mode 100644 index 00000000000..48a74578fb4 --- /dev/null +++ b/src/components/views/messages/ReactionImage.tsx @@ -0,0 +1,64 @@ +/* +Copyright 2018 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 MImageBody from './MImageBody'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { BLURHASH_FIELD } from "../../../ContentMessages"; +import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent'; +import SettingsStore from '../../../settings/SettingsStore'; + +const FORCED_IMAGE_HEIGHT = 20; + +@replaceableComponent("views.messages.ReactionImage") +export default class ReactionImage extends MImageBody { + public onClick = (ev: React.MouseEvent): void => { + ev.preventDefault(); + }; + + protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element { + return children; + } + + protected getPlaceholder(width: number, height: number): JSX.Element { + if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height); + return ; + } + + // Tooltip to show on mouse over + protected getTooltip(): JSX.Element { + return null; + } + + // Don't show "Download this_file.png ..." + protected getFileBody() { + return null; + } + + render() { + const contentUrl = this.getContentUrl(); + const content = this.props.mxEvent.getContent(); + let thumbUrl; + if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) { + thumbUrl = contentUrl; + } else { + thumbUrl = this.getThumbUrl(); + } + const thumbnail = this.messageContent(contentUrl, thumbUrl, content, FORCED_IMAGE_HEIGHT); + return thumbnail; + } +} diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 242428ed367..3c65a4b1800 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -25,6 +25,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; import AccessibleButton from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import ReactionImage from "./ReactionImage"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; interface IProps { // The event we're displaying reactions for @@ -49,12 +51,26 @@ interface IState { @replaceableComponent("views.messages.ReactionsRowButton") export default class ReactionsRowButton extends React.PureComponent { static contextType = MatrixClientContext; + private mediaHelper: MediaEventHelper; + private mediaEligible: boolean; + private mediaEvent: MatrixEvent; state = { tooltipRendered: false, tooltipVisible: false, }; + public constructor(props: IProps, context: React.ContextType) { + super(props); + const mediaEvents = [...props.reactionEvents].filter(event => MediaEventHelper.isEligible(event)); + if (mediaEvents.length > 0) { + this.mediaEligible = true; + // assume that reactors aren't sending different contents with the same key + this.mediaEvent = mediaEvents[0]; + this.mediaHelper = new MediaEventHelper(this.mediaEvent); + } + } + onClick = () => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { @@ -64,6 +80,7 @@ export default class ReactionsRowButton extends React.PureComponent{ content }; + if (this.mediaEligible) { + actualContent = { }} + onMessageAllowed={undefined} + permalinkCreator={undefined} + mediaEventHelper={this.mediaHelper} />; + } + const classes = classNames({ mx_ReactionsRowButton: true, mx_ReactionsRowButton_selected: !!myReactionEvent, @@ -133,7 +162,7 @@ export default class ReactionsRowButton extends React.PureComponent