Render custom images in reactions (#11087)

* Add support for rendering custom emojis in reactions

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Include custom reaction short names in tooltips

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Use custom reaction shortcode for accessibility

This uses the shortcode in the following places:

* The aria-label of the reaction buttons
* The alt text for the reaction image

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Remove explicit instantiation of `customReactionName` variable and add types

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Put custom reaction images behind a labs flag

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Use UnstableValue for finding the shortcode

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Signed-off-by: Sumner Evans <sumner@beeper.com>

* Move calculation of whether to render custom reaction images up to ReactionRow

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Make alt text more friendly when custom reaction doesn't have shortcode

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Add test for ReactionsRowButton

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Apply suggestions from code review

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Don't use Optional

Signed-off-by: Sumner Evans <sumner@beeper.com>

* Fix ReactionsRowButton test

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Signed-off-by: Sumner Evans <sumner@beeper.com>

---------

Signed-off-by: Sumner Evans <sumner@beeper.com>
Co-authored-by: Tulir Asokan <tulir@maunium.net>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Sumner Evans 2023-09-01 04:16:24 -06:00 committed by GitHub
parent d551469543
commit a54f2ff878
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 283 additions and 6 deletions

View file

@ -18,6 +18,7 @@ import React, { SyntheticEvent } from "react";
import classNames from "classnames";
import { MatrixEvent, MatrixEventEvent, Relations, RelationsEvent } from "matrix-js-sdk/src/matrix";
import { uniqBy } from "lodash";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import { _t } from "../../../languageHandler";
import { isContentActionable } from "../../../utils/EventUtils";
@ -27,10 +28,13 @@ import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton";
import RoomContext from "../../../contexts/RoomContext";
import AccessibleButton from "../elements/AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8;
export const REACTION_SHORTCODE_KEY = new UnstableValue("shortcode", "com.beeper.reaction.shortcode");
const ReactButton: React.FC<IProps> = ({ mxEvent, reactions }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -169,6 +173,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
if (!reactions || !isContentActionable(mxEvent)) {
return null;
}
const customReactionImagesEnabled = SettingsStore.getValue("feature_render_reaction_images");
let items = reactions
.getSortedAnnotationsByKey()
@ -195,6 +200,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
mxEvent={mxEvent}
reactionEvents={deduplicatedEvents}
myReactionEvent={myReactionEvent}
customReactionImagesEnabled={customReactionImagesEnabled}
disabled={
!this.context.canReact ||
(myReactionEvent && !myReactionEvent.isRedacted() && !this.context.canSelfRedact)

View file

@ -18,13 +18,15 @@ import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { mediaFromMxc } from "../../../customisations/Media";
import { _t } from "../../../languageHandler";
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import dis from "../../../dispatcher/dispatcher";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
import { REACTION_SHORTCODE_KEY } from "./ReactionsRow";
export interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
@ -37,6 +39,8 @@ interface IProps {
myReactionEvent?: MatrixEvent;
// Whether to prevent quick-reactions by clicking on this reaction
disabled?: boolean;
// Whether to render custom image reactions
customReactionImagesEnabled?: boolean;
}
interface IState {
@ -100,27 +104,56 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
content={content}
reactionEvents={reactionEvents}
visible={this.state.tooltipVisible}
customReactionImagesEnabled={this.props.customReactionImagesEnabled}
/>
);
}
const room = this.context.getRoom(mxEvent.getRoomId());
let label: string | undefined;
let customReactionName: string | undefined;
if (room) {
const senders: string[] = [];
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender()!);
senders.push(member?.name || reactionEvent.getSender()!);
customReactionName =
(this.props.customReactionImagesEnabled &&
REACTION_SHORTCODE_KEY.findIn(reactionEvent.getContent())) ||
undefined;
}
const reactors = formatCommaSeparatedList(senders, 6);
if (content) {
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
label = _t("%(reactors)s reacted with %(content)s", {
reactors,
content: customReactionName || content,
});
} else {
label = reactors;
}
}
let reactionContent = (
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}
</span>
);
if (this.props.customReactionImagesEnabled && content.startsWith("mxc://")) {
const imageSrc = mediaFromMxc(content).srcHttp;
if (imageSrc) {
reactionContent = (
<img
className="mx_ReactionsRowButton_content"
alt={customReactionName || _t("Custom reaction")}
src={imageSrc}
width="16"
height="16"
/>
);
}
}
return (
<AccessibleButton
className={classes}
@ -130,9 +163,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}
</span>
{reactionContent}
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
</span>

View file

@ -22,6 +22,7 @@ import { _t } from "../../../languageHandler";
import { formatCommaSeparatedList } from "../../../utils/FormattingUtils";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { REACTION_SHORTCODE_KEY } from "./ReactionsRow";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
@ -30,6 +31,8 @@ interface IProps {
// A list of Matrix reaction events for this key
reactionEvents: MatrixEvent[];
visible: boolean;
// Whether to render custom image reactions
customReactionImagesEnabled?: boolean;
}
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
@ -43,12 +46,17 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
let tooltipLabel: JSX.Element | undefined;
if (room) {
const senders: string[] = [];
let customReactionName: string | undefined;
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender()!);
const name = member?.name ?? reactionEvent.getSender()!;
senders.push(name);
customReactionName =
(this.props.customReactionImagesEnabled &&
REACTION_SHORTCODE_KEY.findIn(reactionEvent.getContent())) ||
undefined;
}
const shortName = unicodeToShortcode(content);
const shortName = unicodeToShortcode(content) || customReactionName;
tooltipLabel = (
<div>
{_t(