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

Add a dialog showing all reactions to a message #8051

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_PollCreateDialog.scss";
@import "./views/dialogs/_ReactionsDialog.scss";
@import "./views/dialogs/_RegistrationEmailPromptDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/context_menus/_MessageContextMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,8 @@ limitations under the License.
.mx_MessageContextMenu_jumpToEvent::before {
mask-image: url('$(res)/img/element-icons/child-relationship.svg');
}

.mx_MessageContextMenu_iconEmoji::before {
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
}
}
67 changes: 67 additions & 0 deletions res/css/views/dialogs/_ReactionsDialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.mx_ReactionsDialog {
width: 520px;
color: $primary-content;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 80vh;

.mx_ReactionsWrapper {
display: flex;
flex-direction: row;

> .mx_EmojiList {
margin-right: 1em;
padding-left: 1em;
padding-right: 1em;
}

> ul {
list-style-type: none;
}

.mx_SenderList {
padding: 0;
li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
}
}

.mx_EmojiFilterButton {
display: inline-flex;
line-height: $font-20px;
margin-right: 6px;
margin-bottom: 0.5em;
padding: 1px 6px;
border: 1px solid $message-action-bar-border-color;
border-radius: 10px;
background-color: $header-panel-bg-color;
cursor: pointer;
user-select: none;
vertical-align: middle;

&:hover {
border-color: $reaction-row-button-hover-border-color;
}

&.mx_ReactionsRowButton_selected {
background-color: $reaction-row-button-selected-bg-color;
border-color: $accent;
}

&.mx_AccessibleButton_disabled {
cursor: not-allowed;
}

.mx_ReactionsRowButton_content {
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 4px;
}
}
}
27 changes: 26 additions & 1 deletion src/components/views/context_menus/MessageContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import ContextMenu, { toRightOf, IPosition, ChevronFace } from '../../structures
import ReactionPicker from '../emojipicker/ReactionPicker';
import ViewSource from '../../structures/ViewSource';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import ReactionsDialog from '../dialogs/ReactionsDialog';
import ShareDialog from '../dialogs/ShareDialog';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
Expand Down Expand Up @@ -73,7 +74,11 @@ interface IProps extends IPosition {
// A permalink to this event or an href of an anchor element the user has clicked
link?: string;

getRelationsForEvent?: GetRelationsForEvent;
getRelationsForEvent?: (
eventId: string,
relationType: string,
eventType: string
) => Relations;
}

interface IState {
Expand Down Expand Up @@ -179,6 +184,17 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
this.closeMenu();
};

private onReactionsClick = (): void => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());

Modal.createTrackedDialog('Reactions', '', ReactionsDialog, {
room,
reactions: this.props.reactions,
}, 'mx_Dialog_viewsource');
this.closeMenu();
};

private onRedactClick = (): void => {
const { mxEvent, onCloseDialog } = this.props;
createRedactEventDialog({
Expand Down Expand Up @@ -420,6 +436,14 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
);
}

const reactionsButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconEmoji"
label={_t("Reactions")}
onClick={this.onReactionsClick}
/>
);

let permalinkButton: JSX.Element;
if (permalink) {
permalinkButton = (
Expand Down Expand Up @@ -632,6 +656,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
{ jumpToRelatedEventButton }
{ unhidePreviewButton }
{ viewSourceButton }
{ reactionsButton }
{ resendReactionsButton }
{ collapseReplyChainButton }
</IconizedContextMenuOptionList>
Expand Down
112 changes: 112 additions & 0 deletions src/components/views/dialogs/ReactionsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { Room } from 'matrix-js-sdk/src/matrix';

import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import AccessibleButton from '../elements/AccessibleButton';

interface IProps extends IDialogProps {
room: Room;
reactions: Relations;
}

interface IState {
filteredEmoji: string|null;
allAnnotations: any[];
}

@replaceableComponent("views.dialogs.ReactionsDialog")
export default class ReactionsDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
}

state = {
filteredEmoji: null,
allAnnotations: [],
};

componentDidMount() {
this.setState({ allAnnotations: this.getAllAnnotations() });
}

private sortAnnotations(arr1, arr2) {
return arr2[1].size - arr1[1].size;
}

private getAllAnnotations() {
const room = this.props.room;
const reactions = this.props.reactions;
const sortedAnnotations = reactions.getSortedAnnotationsByKey()
.sort(this.sortAnnotations);
const senders = [];
for (const reaction of sortedAnnotations) {
const emoji = reaction[0];
const reactionEvents = Array.from(reaction[1]);
const sortedSenderNames = reactionEvents
.map(reactionEvent => {
const member = room.getMember(reactionEvent.getSender());
return member ? member.name : reactionEvent.getSender();
})
.sort((a, b) => a.localeCompare(b));
sortedSenderNames.forEach((name) => senders.push([emoji, name]));
}
return senders;
}

private getFilteredAnnotations() {
const filterEmoji = this.state.filteredEmoji;
return this.state.allAnnotations
.filter(ann => filterEmoji === null || ann[0] == filterEmoji);
}

private setFilterEmoji(emoji) {
const matchingEmoji = this.state.allAnnotations
.filter(([existingEmoji, _]) => emoji === existingEmoji);
if (matchingEmoji.length == 0) {
emoji = null;
}
this.setState({ filteredEmoji: emoji });
}

private emojiFilterButton(emoji, size) {
return (<AccessibleButton
className="mx_EmojiFilterButton"
onClick={() => this.setFilterEmoji(emoji)}
>
{ emoji } { size }
</AccessibleButton>);
}

render() {
const reactions = this.props.reactions.getSortedAnnotationsByKey()
.map(([emoji, senders]): [string, number] => [emoji, senders.size]);
const totalReactions = reactions.reduce((acc, [_, size]) => size + acc, 0);
reactions.unshift([_t('All'), totalReactions]);

const emojiList = reactions.map(([emoji, size]) =>
(<li key={emoji}>{ this.emojiFilterButton(emoji, size) }</li>));
const senderList = this.getFilteredAnnotations()
.map(([emoji, sender]) => (<li key={emoji + sender}>{ emoji }&nbsp;{ sender }</li>));
return (
<BaseDialog
className="mx_ReactionsDialog"
onFinished={this.props.onFinished}
title={_t('Reactions')}
contentId='mx_ReactionsDialog'
>
<div className="mx_ReactionsWrapper">
<ul className="mx_EmojiList">
{ emojiList }
</ul>
<ul className="mx_SenderList">
{ senderList }
</ul>
</div>
</BaseDialog>
);
}
}
4 changes: 4 additions & 0 deletions src/components/views/messages/MessageActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ interface IOptionsButtonProps {
relationType: string,
eventType: string
) => Relations;
reactions: Relations;
}

const OptionsButton: React.FC<IOptionsButtonProps> = ({
Expand All @@ -70,6 +71,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
permalinkCreator,
onFocusChange,
getRelationsForEvent,
reactions,
}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
Expand Down Expand Up @@ -102,6 +104,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
onFinished={closeMenu}
getRelationsForEvent={getRelationsForEvent}
reactions={reactions}
/>;
}

Expand Down Expand Up @@ -495,6 +498,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
onFocusChange={this.onFocusChange}
key="menu"
getRelationsForEvent={this.props.getRelationsForEvent}
reactions={this.props.reactions}
/>);
}

Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2679,6 +2679,7 @@
"Message edits": "Message edits",
"Modal Widget": "Modal Widget",
"Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s",
"All": "All",
"Continuing without email": "Continuing without email",
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
"Email (optional)": "Email (optional)",
Expand Down
Loading