Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit fe674e8

Browse files
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.
1 parent 4ab5968 commit fe674e8

File tree

5 files changed

+97
-4
lines changed

5 files changed

+97
-4
lines changed

src/HtmlUtils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function mightContainEmoji(str: string): boolean {
102102
*/
103103
export function unicodeToShortcode(char: string): string {
104104
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
105-
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
105+
return shortcodes?.length ? `:${shortcodes[0]}:` : char;
106106
}
107107

108108
export function processHtmlForSending(html: string): string {

src/components/views/messages/MImageBody.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
139139
}
140140
};
141141

142-
private isGif = (): boolean => {
142+
protected isGif = (): boolean => {
143143
const content = this.props.mxEvent.getContent();
144144
return content.info?.mimetype === "image/gif";
145145
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2018 New Vector Ltd
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
import MImageBody from './MImageBody';
20+
import { replaceableComponent } from "../../../utils/replaceableComponent";
21+
import { BLURHASH_FIELD } from "../../../ContentMessages";
22+
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
23+
import SettingsStore from '../../../settings/SettingsStore';
24+
25+
const FORCED_IMAGE_HEIGHT = 20;
26+
27+
@replaceableComponent("views.messages.ReactionImage")
28+
export default class ReactionImage extends MImageBody {
29+
public onClick = (ev: React.MouseEvent): void => {
30+
ev.preventDefault();
31+
};
32+
33+
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
34+
return children;
35+
}
36+
37+
protected getPlaceholder(width: number, height: number): JSX.Element {
38+
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
39+
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="14" height="20" />;
40+
}
41+
42+
// Tooltip to show on mouse over
43+
protected getTooltip(): JSX.Element {
44+
return null;
45+
}
46+
47+
// Don't show "Download this_file.png ..."
48+
protected getFileBody() {
49+
return null;
50+
}
51+
52+
render() {
53+
const contentUrl = this.getContentUrl();
54+
const content = this.props.mxEvent.getContent<IMediaEventContent>();
55+
let thumbUrl;
56+
if (this.props.forExport || (this.isGif() && SettingsStore.getValue("autoplayGifs"))) {
57+
thumbUrl = contentUrl;
58+
} else {
59+
thumbUrl = this.getThumbUrl();
60+
}
61+
const thumbnail = this.messageContent(contentUrl, thumbUrl, content, FORCED_IMAGE_HEIGHT);
62+
return thumbnail;
63+
}
64+
}

src/components/views/messages/ReactionsRowButton.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
2525
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
2626
import AccessibleButton from "../elements/AccessibleButton";
2727
import MatrixClientContext from "../../../contexts/MatrixClientContext";
28+
import ReactionImage from "./ReactionImage";
29+
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
2830

2931
interface IProps {
3032
// The event we're displaying reactions for
@@ -49,12 +51,26 @@ interface IState {
4951
@replaceableComponent("views.messages.ReactionsRowButton")
5052
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
5153
static contextType = MatrixClientContext;
54+
private mediaHelper: MediaEventHelper;
55+
private mediaEligible: boolean;
56+
private mediaEvent: MatrixEvent;
5257

5358
state = {
5459
tooltipRendered: false,
5560
tooltipVisible: false,
5661
};
5762

63+
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
64+
super(props);
65+
const mediaEvents = [...props.reactionEvents].filter(event => MediaEventHelper.isEligible(event));
66+
if (mediaEvents.length > 0) {
67+
this.mediaEligible = true;
68+
// assume that reactors aren't sending different contents with the same key
69+
this.mediaEvent = mediaEvents[0];
70+
this.mediaHelper = new MediaEventHelper(this.mediaEvent);
71+
}
72+
}
73+
5874
onClick = () => {
5975
const { mxEvent, myReactionEvent, content } = this.props;
6076
if (myReactionEvent) {
@@ -64,6 +80,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
6480
);
6581
} else {
6682
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
83+
...(this.mediaEligible ? this.mediaEvent.getContent() : {}),
6784
"m.relates_to": {
6885
"rel_type": "m.annotation",
6986
"event_id": mxEvent.getId(),
@@ -92,6 +109,18 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
92109
render() {
93110
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
94111

112+
let actualContent: JSX.Element = <>{ content }</>;
113+
if (this.mediaEligible) {
114+
actualContent = <ReactionImage
115+
mxEvent={this.mediaEvent}
116+
highlights={undefined}
117+
highlightLink={undefined}
118+
onHeightChanged={() => { }}
119+
onMessageAllowed={undefined}
120+
permalinkCreator={undefined}
121+
mediaEventHelper={this.mediaHelper} />;
122+
}
123+
95124
const classes = classNames({
96125
mx_ReactionsRowButton: true,
97126
mx_ReactionsRowButton_selected: !!myReactionEvent,
@@ -133,7 +162,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
133162
onMouseLeave={this.onMouseLeave}
134163
>
135164
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
136-
{ content }
165+
{ actualContent }
137166
</span>
138167
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
139168
{ count }

src/utils/MediaEventHelper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class MediaEventHelper implements IDestroyable {
105105
if (!event) return false;
106106
if (event.isRedacted()) return false;
107107
if (event.getType() === EventType.Sticker) return true;
108-
if (event.getType() !== EventType.RoomMessage) return false;
108+
if (event.getType() !== EventType.RoomMessage && event.getType() !== EventType.Reaction) return false;
109109

110110
const content = event.getContent();
111111
const mediaMsgTypes: string[] = [

0 commit comments

Comments
 (0)