Merge branch 'develop' into germain-gg/fix-right-panel-member

This commit is contained in:
Germain 2023-09-01 14:42:42 +01:00 committed by GitHub
commit 4e6148d798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 306 additions and 15 deletions

View file

@ -22,6 +22,7 @@ limitations under the License.
border-radius: 10px; border-radius: 10px;
background-color: $secondary-hairline-color; background-color: $secondary-hairline-color;
user-select: none; user-select: none;
align-items: center;
&:hover { &:hover {
border-color: $quinary-content; border-color: $quinary-content;

View file

@ -18,6 +18,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix"; import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { capitalize } from "lodash";
import { _t, getUserLanguage } from "../../../languageHandler"; import { _t, getUserLanguage } from "../../../languageHandler";
import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils"; import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils";
@ -92,6 +93,10 @@ export default class DateSeparator extends React.Component<IProps, IState> {
}); });
}; };
private get relativeTimeFormat(): Intl.RelativeTimeFormat {
return new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" });
}
private getLabel(): string { private getLabel(): string {
const date = new Date(this.props.ts); const date = new Date(this.props.ts);
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates); const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
@ -104,11 +109,10 @@ export default class DateSeparator extends React.Component<IProps, IState> {
const days = getDaysArray("long"); const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1); yesterday.setDate(today.getDate() - 1);
const relativeTimeFormat = new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" });
if (date.toDateString() === today.toDateString()) { if (date.toDateString() === today.toDateString()) {
return relativeTimeFormat.format(0, "day"); // Today return this.relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) { } else if (date.toDateString() === yesterday.toDateString()) {
return relativeTimeFormat.format(-1, "day"); // Yesterday return this.relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; // Sunday-Saturday return days[date.getDay()]; // Sunday-Saturday
} else { } else {
@ -263,6 +267,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
private renderJumpToDateMenu(): React.ReactElement { private renderJumpToDateMenu(): React.ReactElement {
let contextMenu: JSX.Element | undefined; let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuPosition) { if (this.state.contextMenuPosition) {
const relativeTimeFormat = this.relativeTimeFormat;
contextMenu = ( contextMenu = (
<IconizedContextMenu <IconizedContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)} {...contextMenuBelow(this.state.contextMenuPosition)}
@ -270,12 +275,12 @@ export default class DateSeparator extends React.Component<IProps, IState> {
> >
<IconizedContextMenuOptionList first> <IconizedContextMenuOptionList first>
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("time|last_week")} label={capitalize(relativeTimeFormat.format(-1, "week"))}
onClick={this.onLastWeekClicked} onClick={this.onLastWeekClicked}
data-testid="jump-to-date-last-week" data-testid="jump-to-date-last-week"
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("time|last_month")} label={capitalize(relativeTimeFormat.format(-1, "month"))}
onClick={this.onLastMonthClicked} onClick={this.onLastMonthClicked}
data-testid="jump-to-date-last-month" data-testid="jump-to-date-last-month"
/> />

View file

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

View file

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

View file

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

View file

@ -480,6 +480,7 @@ export interface IProps {
interface IState { interface IState {
contextMenuPosition?: DOMRect; contextMenuPosition?: DOMRect;
rightPanelOpen: boolean; rightPanelOpen: boolean;
featureAskToJoin: boolean;
} }
/** /**
@ -496,6 +497,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>; public context!: React.ContextType<typeof RoomContext>;
private readonly client = this.props.room.client; private readonly client = this.props.room.client;
private readonly featureAskToJoinWatcher: string;
public constructor(props: IProps, context: IState) { public constructor(props: IProps, context: IState) {
super(props, context); super(props, context);
@ -503,7 +505,15 @@ export default class RoomHeader extends React.Component<IProps, IState> {
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate); notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = { this.state = {
rightPanelOpen: RightPanelStore.instance.isOpen, rightPanelOpen: RightPanelStore.instance.isOpen,
featureAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
}; };
this.featureAskToJoinWatcher = SettingsStore.watchSetting(
"feature_ask_to_join",
null,
(_settingName, _roomId, _atLevel, _newValAtLevel, featureAskToJoin) => {
this.setState({ featureAskToJoin });
},
);
} }
public componentDidMount(): void { public componentDidMount(): void {
@ -516,6 +526,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room); const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate); notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
SettingsStore.unwatchSetting(this.featureAskToJoinWatcher);
} }
private onRightPanelStoreUpdate = (): void => { private onRightPanelStoreUpdate = (): void => {
@ -821,7 +832,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
</div> </div>
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />} {!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
<RoomLiveShareWarning roomId={this.props.room.roomId} /> <RoomLiveShareWarning roomId={this.props.room.roomId} />
<RoomKnocksBar room={this.props.room} /> {this.state.featureAskToJoin && <RoomKnocksBar room={this.props.room} />}
</header> </header>
); );
} }

View file

@ -218,9 +218,7 @@
"short_seconds": "%(value)ss", "short_seconds": "%(value)ss",
"short_days_hours_minutes_seconds": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", "short_days_hours_minutes_seconds": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss",
"short_hours_minutes_seconds": "%(hours)sh %(minutes)sm %(seconds)ss", "short_hours_minutes_seconds": "%(hours)sh %(minutes)sm %(seconds)ss",
"short_minutes_seconds": "%(minutes)sm %(seconds)ss", "short_minutes_seconds": "%(minutes)sm %(seconds)ss"
"last_week": "Last week",
"last_month": "Last month"
}, },
"Identity server has no terms of service": "Identity server has no terms of service", "Identity server has no terms of service": "Identity server has no terms of service",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.", "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
@ -948,6 +946,8 @@
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
"Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)", "Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)",
"Font size": "Font size", "Font size": "Font size",
"Render custom images in reactions": "Render custom images in reactions",
"Sometimes referred to as \"custom emojis\".": "Sometimes referred to as \"custom emojis\".",
"Use custom size": "Use custom size", "Use custom size": "Use custom size",
"Show polls button": "Show polls button", "Show polls button": "Show polls button",
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout", "Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
@ -2316,6 +2316,7 @@
"Error processing voice message": "Error processing voice message", "Error processing voice message": "Error processing voice message",
"Add reaction": "Add reaction", "Add reaction": "Add reaction",
"%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s",
"Custom reaction": "Custom reaction",
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>", "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
"Message deleted on %(date)s": "Message deleted on %(date)s", "Message deleted on %(date)s": "Message deleted on %(date)s",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",

View file

@ -472,6 +472,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
default: "", default: "",
controller: new FontSizeController(), controller: new FontSizeController(),
}, },
"feature_render_reaction_images": {
isFeature: true,
labsGroup: LabGroup.Messaging,
displayName: _td("Render custom images in reactions"),
description: _td('Sometimes referred to as "custom emojis".'),
supportedLevels: LEVELS_FEATURE,
default: false,
},
/** /**
* With the transition to Compound we are moving to a base font size * With the transition to Compound we are moving to a base font size
* of 16px. We're taking the opportunity to move away from the `baseFontSize` * of 16px. We're taking the opportunity to move away from the `baseFontSize`

View file

@ -0,0 +1,119 @@
/*
Copyright 2023 Beeper
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 { IContent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render } from "@testing-library/react";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { getMockClientWithEventEmitter } from "../../../test-utils";
import ReactionsRowButton, { IProps } from "../../../../src/components/views/messages/ReactionsRowButton";
describe("ReactionsRowButton", () => {
const userId = "@alice:server";
const roomId = "!randomcharacters:aser.ver";
const mockClient = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn().mockReturnValue("https://not.a.real.url"),
getRoom: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
const createProps = (relationContent: IContent): IProps => ({
mxEvent: new MatrixEvent({
room_id: roomId,
event_id: "$test:example.com",
content: { body: "test" },
}),
content: relationContent["m.relates_to"]?.key || "",
count: 2,
reactionEvents: [
new MatrixEvent({
type: "m.reaction",
sender: "@user1:example.com",
content: relationContent,
}),
new MatrixEvent({
type: "m.reaction",
sender: "@user2:example.com",
content: relationContent,
}),
],
customReactionImagesEnabled: true,
});
beforeEach(function () {
jest.clearAllMocks();
mockClient.credentials = { userId: userId };
mockClient.getRoom.mockImplementation((roomId: string): Room | null => {
return roomId === room.roomId ? room : null;
});
});
it("renders reaction row button emojis correctly", () => {
const props = createProps({
"m.relates_to": {
event_id: "$user2:example.com",
key: "👍",
rel_type: "m.annotation",
},
});
const root = render(
<MatrixClientContext.Provider value={mockClient}>
<ReactionsRowButton {...props} />
</MatrixClientContext.Provider>,
);
expect(root.asFragment()).toMatchSnapshot();
// Try hover and make sure that the ReactionsRowButtonTooltip works
const reactionButton = root.getByRole("button");
const event = new MouseEvent("mouseover", {
bubbles: true,
cancelable: true,
});
reactionButton.dispatchEvent(event);
expect(root.asFragment()).toMatchSnapshot();
});
it("renders reaction row button custom image reactions correctly", () => {
const props = createProps({
"com.beeper.reaction.shortcode": ":test:",
"shortcode": ":test:",
"m.relates_to": {
event_id: "$user1:example.com",
key: "mxc://example.com/123456789",
rel_type: "m.annotation",
},
});
const root = render(
<MatrixClientContext.Provider value={mockClient}>
<ReactionsRowButton {...props} />
</MatrixClientContext.Provider>,
);
expect(root.asFragment()).toMatchSnapshot();
// Try hover and make sure that the ReactionsRowButtonTooltip works
const reactionButton = root.getByRole("button");
const event = new MouseEvent("mouseover", {
bubbles: true,
cancelable: true,
});
reactionButton.dispatchEvent(event);
expect(root.asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactionsRowButton renders reaction row button custom image reactions correctly 1`] = `
<DocumentFragment>
<div
aria-label="@user1:example.com and @user2:example.com reacted with :test:"
class="mx_AccessibleButton mx_ReactionsRowButton"
role="button"
tabindex="0"
>
<img
alt=":test:"
class="mx_ReactionsRowButton_content"
height="16"
src="https://not.a.real.url"
width="16"
/>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_count"
>
2
</span>
</div>
</DocumentFragment>
`;
exports[`ReactionsRowButton renders reaction row button custom image reactions correctly 2`] = `
<DocumentFragment>
<div
aria-label="@user1:example.com and @user2:example.com reacted with :test:"
class="mx_AccessibleButton mx_ReactionsRowButton"
role="button"
tabindex="0"
>
<img
alt=":test:"
class="mx_ReactionsRowButton_content"
height="16"
src="https://not.a.real.url"
width="16"
/>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_count"
>
2
</span>
<div />
</div>
</DocumentFragment>
`;
exports[`ReactionsRowButton renders reaction row button emojis correctly 1`] = `
<DocumentFragment>
<div
aria-label="@user1:example.com and @user2:example.com reacted with 👍"
class="mx_AccessibleButton mx_ReactionsRowButton"
role="button"
tabindex="0"
>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_content"
>
👍
</span>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_count"
>
2
</span>
</div>
</DocumentFragment>
`;
exports[`ReactionsRowButton renders reaction row button emojis correctly 2`] = `
<DocumentFragment>
<div
aria-label="@user1:example.com and @user2:example.com reacted with 👍"
class="mx_AccessibleButton mx_ReactionsRowButton"
role="button"
tabindex="0"
>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_content"
>
👍
</span>
<span
aria-hidden="true"
class="mx_ReactionsRowButton_count"
>
2
</span>
<div />
</div>
</DocumentFragment>
`;