Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication: Refactor consecutive message view #9456

Open
wants to merge 34 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b09c320
consecutive message view with new reaction bar added
asliayk Oct 10, 2024
fedc900
renamed file
asliayk Oct 10, 2024
1113a4b
tests updated
asliayk Oct 10, 2024
df6a1fc
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 10, 2024
8a48723
tests updated
asliayk Oct 11, 2024
613e8d8
tests updated & refactor
asliayk Oct 11, 2024
72fefe2
minor fix
asliayk Oct 11, 2024
313dfef
refactored reaction bar style
asliayk Oct 12, 2024
252cedf
refactored reaction bar style
asliayk Oct 12, 2024
8f264b4
updated resolve button
asliayk Oct 12, 2024
a53c7ba
updated reaction bar style
asliayk Oct 12, 2024
f152661
Merge branch 'develop' into feature/communication/consecutive-message…
yassinsws Oct 13, 2024
e3c5384
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 17, 2024
9725c07
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 21, 2024
6794ca1
added dropdown
asliayk Oct 21, 2024
c9e7e6e
Merge remote-tracking branch 'origin/feature/communication/consecutiv…
asliayk Oct 21, 2024
8726cee
reply view added
asliayk Oct 21, 2024
f6d149c
adjusted timeframe to 5 minutes
asliayk Oct 21, 2024
bfec85d
added dropdown to posting directive
asliayk Oct 21, 2024
0cc49d9
updated strings
asliayk Oct 21, 2024
29a25c5
refactor code, fix pin/unpin string
asliayk Oct 22, 2024
2f73014
opened dropdown disables scrolling
asliayk Oct 23, 2024
c9d276a
fix pin styling, updated tests
asliayk Oct 23, 2024
7792212
updated tests
asliayk Oct 23, 2024
21e97e1
fixed styling
asliayk Oct 23, 2024
67ce6f0
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 23, 2024
eec93d9
directive test added
asliayk Oct 24, 2024
9287c3a
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 24, 2024
d355ce0
minor fix
asliayk Oct 24, 2024
64bae5f
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 26, 2024
f81ba93
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 27, 2024
c34b89f
adjusted dropdown/reaction bar position, added tests
asliayk Oct 27, 2024
daa405a
Merge branch 'develop' into feature/communication/consecutive-message…
asliayk Oct 29, 2024
47c5b04
conflicts resolved & adjusted dropdown position
asliayk Oct 29, 2024
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 src/main/webapp/app/entities/metis/answer-post.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Posting } from 'app/entities/metis/posting.model';
export class AnswerPost extends Posting {
public resolvesPost?: boolean;
public post?: Post;
public isConsecutive?: boolean = false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM! Consider adding a comment for clarity.

The addition of the isConsecutive property aligns well with the PR objectives for enhancing message grouping. The implementation follows the coding guidelines, using camelCase for property naming.

Consider adding a brief comment explaining the purpose of the isConsecutive property for better code documentation. For example:

// Indicates whether this post is part of a consecutive message group
public isConsecutive?: boolean = false;


constructor() {
super();
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/app/entities/metis/post.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Post extends Posting {
public conversation?: Conversation;
public displayPriority?: DisplayPriority;
public resolved?: boolean;
public isConsecutive?: boolean = false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM! Consider making the property non-optional for type safety.

The addition of the isConsecutive property aligns well with the PR objectives for enhancing message grouping. The naming and default value are appropriate.

For improved type safety, consider making the property non-optional:

public isConsecutive: boolean = false;

This change would eliminate the need for null checks when using this property elsewhere in the codebase.


constructor() {
super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,24 @@
>
<!-- list of all top level posts -->
<!-- answers are opened in the thread sidebar -->
@for (post of posts; track postsTrackByFn($index, post)) {
<div>
<jhi-posting-thread
#postingThread
[lastReadDate]="_activeConversation?.lastReadDate"
[hasChannelModerationRights]="!!getAsChannel(_activeConversation)?.hasChannelModerationRights"
[id]="'item-' + post.id"
[post]="post"
[showAnswers]="false"
[readOnlyMode]="!!getAsChannel(_activeConversation)?.isArchived"
[isCommunicationPage]="true"
(openThread)="setPostForThread($event)"
/>
@for (group of groupedPosts; track postsTrackByFn($index, group)) {
<div class="message-group">
@for (post of group.posts; track postsTrackByFn($index, post)) {
<div class="post-item">
<jhi-posting-thread
#postingThread
[lastReadDate]="_activeConversation?.lastReadDate"
[hasChannelModerationRights]="!!getAsChannel(_activeConversation)?.hasChannelModerationRights"
[id]="'item-' + post.id"
[post]="post"
[showAnswers]="false"
[readOnlyMode]="!!getAsChannel(_activeConversation)?.isArchived"
[isCommunicationPage]="true"
(openThread)="setPostForThread($event)"
[isConsecutive]="post.isConsecutive || false"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider using the nullish coalescing operator for default values

Instead of using the logical OR || to assign a default value to [isConsecutive], consider using the nullish coalescing operator ?? for clarity and to handle falsy values correctly:

-[isConsecutive]="post.isConsecutive || false"
+[isConsecutive]="post.isConsecutive ?? false"

This ensures that if post.isConsecutive is explicitly false, it remains false, and only null or undefined values are replaced with false.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[isConsecutive]="post.isConsecutive || false"
[isConsecutive]="post.isConsecutive ?? false"

></jhi-posting-thread>
</div>
}
</div>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,25 @@
display: none;
}
}

.message-group {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}

.grouped-posts {
margin-left: 30px;
padding-left: 10px;
}
Comment on lines +84 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM: Good indentation for grouped posts

The .grouped-posts class creates an effective indentation for grouped messages, which helps distinguish between the main message and subsequent grouped messages. This aligns well with the PR objective of grouping messages from the same user sent within a one-hour timeframe.

Consider using CSS variables for the margin and padding values to maintain consistency and ease future adjustments. For example:

:root {
  --grouped-posts-left-margin: 30px;
  --grouped-posts-left-padding: 10px;
}

.grouped-posts {
  margin-left: var(--grouped-posts-left-margin);
  padding-left: var(--grouped-posts-left-padding);
}


.grouped-posts,
.grouped-post {
margin-top: 0;
margin-bottom: 0;
padding: 0;
}

jhi-posting-thread {
margin-bottom: 5px;
}
Comment on lines +96 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM: Appropriate spacing for posting threads

The addition of a 5px bottom margin to the jhi-posting-thread element provides a subtle separation between posting threads, which enhances the overall visual hierarchy and readability of the conversation.

Consider using a class selector instead of an element selector to be more consistent with BEM naming conventions and to improve the modularity of your styles. For example:

.posting-thread {
  margin-bottom: 5px;
}

Then, apply this class to your jhi-posting-thread element in the HTML template.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import { canCreateNewMessageInConversation } from 'app/shared/metis/conversation
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { LayoutService } from 'app/shared/breakpoints/layout.service';
import { CustomBreakpointNames } from 'app/shared/breakpoints/breakpoints.service';
import dayjs from 'dayjs/esm';
import { User } from 'app/core/user/user.model';

interface PostGroup {
author: User | undefined;
posts: Post[];
}

@Component({
selector: 'jhi-conversation-messages',
Expand Down Expand Up @@ -72,6 +79,7 @@ export class ConversationMessagesComponent implements OnInit, AfterViewInit, OnD

newPost?: Post;
posts: Post[] = [];
groupedPosts: PostGroup[] = [];
totalNumberOfPosts = 0;
page = 1;
public isFetchingPosts = true;
Expand Down Expand Up @@ -178,11 +186,66 @@ export class ConversationMessagesComponent implements OnInit, AfterViewInit, OnD
};
}

private groupPosts(): void {
if (!this.posts || this.posts.length === 0) {
this.groupedPosts = [];
return;
}

const sortedPosts = this.posts.sort((a, b) => {
const aDate = (a as any).creationDateDayjs;
const bDate = (b as any).creationDateDayjs;
return aDate?.valueOf() - bDate?.valueOf();
});

const groups: PostGroup[] = [];
let currentGroup: PostGroup = {
author: sortedPosts[0].author,
posts: [{ ...sortedPosts[0], isConsecutive: false }],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid adding isConsecutive property directly to Post objects

Adding isConsecutive directly to Post instances may cause unintended side effects if Post objects are used elsewhere. Consider defining an interface or class that extends Post to include isConsecutive, or use a separate data structure to store this information.

Also applies to: 214-214

};

for (let i = 1; i < sortedPosts.length; i++) {
const currentPost = sortedPosts[i];
const lastPostInGroup = currentGroup.posts[currentGroup.posts.length - 1];

const currentDate = (currentPost as any).creationDateDayjs;
const lastDate = (lastPostInGroup as any).creationDateDayjs;

let timeDiff = Number.MAX_SAFE_INTEGER;
if (currentDate && lastDate) {
timeDiff = currentDate.diff(lastDate, 'minute');
}

if (currentPost.author?.id === currentGroup.author?.id && timeDiff < 5 && timeDiff >= 0) {
currentGroup.posts.push({ ...currentPost, isConsecutive: true }); // consecutive post
} else {
groups.push(currentGroup);
currentGroup = {
author: currentPost.author,
posts: [{ ...currentPost, isConsecutive: false }],
};
}
}

groups.push(currentGroup);
this.groupedPosts = groups;
this.cdr.detectChanges();
}

setPosts(posts: Post[]): void {
if (this.content) {
this.previousScrollDistanceFromTop = this.content.nativeElement.scrollHeight - this.content.nativeElement.scrollTop;
}
this.posts = posts.slice().reverse();

this.posts = posts
.slice()
.reverse()
.map((post) => {
(post as any).creationDateDayjs = post.creationDate ? dayjs(post.creationDate) : undefined;
return post;
});

this.groupPosts();
}

fetchNextPage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h5 class="mb-0 fw-medium" jhiTranslate="artemisApp.conversationsLayout.threadSi
</div>
</div>
<!-- body -->
<div #scrollBody class="pt-0 scrollbar wrapper-scroll-y mx-1">
<div #scrollBody class="pt-0 scrollbar wrapper-scroll-y mx-1 thread-answer-post">
@if (post !== undefined) {
<div class="my-2">
<div class="card-body">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@if (!showConfirm) {
<fa-icon [icon]="initialIcon" [ngbTooltip]="initialTooltip" [size]="iconSize" (click)="toggle()" />
<fa-icon style="display: inline-flex; justify-content: center" [icon]="initialIcon" [ngbTooltip]="initialTooltip" (click)="toggle()" />
}
@if (showConfirm) {
<fa-icon [ngClass]="'text-danger'" [icon]="confirmIcon" [ngbTooltip]="confirmTooltip" [size]="iconSize" (mouseleave)="toggle()" (click)="confirmAction()" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,80 @@
<div [id]="'item-' + posting.id" class="row" [ngClass]="isCommunicationPage ? 'module-bg mt-2 rounded-2' : 'answer-post m-1'">
<jhi-answer-post-header
[posting]="posting"
[isReadOnlyMode]="isReadOnlyMode"
(openPostingCreateEditModal)="createAnswerPostModal.open()"
[isCommunicationPage]="isCommunicationPage"
[lastReadDate]="lastReadDate"
[hasChannelModerationRights]="hasChannelModerationRights"
/>
@if (!isConsecutive()) {
<jhi-answer-post-header
[posting]="posting"
[isReadOnlyMode]="isReadOnlyMode"
[isCommunicationPage]="isCommunicationPage"
[lastReadDate]="lastReadDate"
[hasChannelModerationRights]="hasChannelModerationRights"
/>
}
@if (!createAnswerPostModal.isInputOpen) {
<div class="answer-post-content-margin">
<jhi-posting-content
[content]="posting.content"
[isEdited]="!!posting.updatedDate"
[author]="posting.author"
[posting]="posting"
[isReply]="true"
(userReferenceClicked)="userReferenceClicked.emit($event)"
(channelReferenceClicked)="channelReferenceClicked.emit($event)"
/>
<div class="answer-post-content-margin message-container" (contextmenu)="onRightClick($event)">
<div class="message-content" [class.force-hover]="showDropdown">
<jhi-posting-content
[content]="posting.content"
[isEdited]="!!posting.updatedDate"
[author]="posting.author"
[posting]="posting"
[isReply]="true"
(userReferenceClicked)="userReferenceClicked.emit($event)"
(channelReferenceClicked)="channelReferenceClicked.emit($event)"
/>
<div class="answer-post-content-margin hover-actions">
<jhi-answer-post-reactions-bar
[isReadOnlyMode]="isReadOnlyMode"
[posting]="posting"
[isLastAnswer]="isLastAnswer"
[isThreadSidebar]="isThreadSidebar"
(openPostingCreateEditModal)="createAnswerPostModal.open()"
(reactionsUpdated)="onReactionsUpdated($event)"
/>
</div>
</div>
</div>
}
<div class="answer-post-content-margin">
<ng-container #createEditAnswerPostContainer />
</div>
<div class="answer-post-content-margin">
<jhi-answer-post-footer
<jhi-answer-post-reactions-bar
[isReadOnlyMode]="isReadOnlyMode"
[posting]="posting"
[isLastAnswer]="isLastAnswer"
[isThreadSidebar]="isThreadSidebar"
(openPostingCreateEditModal)="openPostingCreateEditModal.emit()"
(openPostingCreateEditModal)="createAnswerPostModal.open()"
(reactionsUpdated)="onReactionsUpdated($event)"
[isEmojiCount]="true"
/>
Comment on lines +40 to 48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM: Standalone reactions bar for emoji counts

The addition of a standalone jhi-answer-post-reactions-bar with [isEmojiCount]="true" aligns with the PR objective of adding an emoji add button next to existing emoji reactions. This implementation allows for a clear separation between the hover actions and the persistent emoji count display.

For consistency, consider moving the [isEmojiCount]="true" property to be with the other input properties:

<jhi-answer-post-reactions-bar
    [isReadOnlyMode]="isReadOnlyMode"
    [posting]="posting"
    [isLastAnswer]="isLastAnswer"
    [isThreadSidebar]="isThreadSidebar"
+   [isEmojiCount]="true"
    (openPostingCreateEditModal)="createAnswerPostModal.open()"
    (reactionsUpdated)="onReactionsUpdated($event)"
-   [isEmojiCount]="true"
/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<jhi-answer-post-reactions-bar
[isReadOnlyMode]="isReadOnlyMode"
[posting]="posting"
[isLastAnswer]="isLastAnswer"
[isThreadSidebar]="isThreadSidebar"
(openPostingCreateEditModal)="openPostingCreateEditModal.emit()"
(openPostingCreateEditModal)="createAnswerPostModal.open()"
(reactionsUpdated)="onReactionsUpdated($event)"
[isEmojiCount]="true"
/>
<jhi-answer-post-reactions-bar
[isReadOnlyMode]="isReadOnlyMode"
[posting]="posting"
[isLastAnswer]="isLastAnswer"
[isThreadSidebar]="isThreadSidebar"
[isEmojiCount]="true"
(openPostingCreateEditModal)="createAnswerPostModal.open()"
(reactionsUpdated)="onReactionsUpdated($event)"
/>

</div>
</div>
<jhi-answer-post-create-edit-modal #createAnswerPostModal [posting]="posting" [createEditAnswerPostContainerRef]="containerRef" />
<jhi-answer-post-create-edit-modal #createAnswerPostModal [posting]="posting" (postingUpdated)="onPostingUpdated($event)" [createEditAnswerPostContainerRef]="containerRef" />

<!-- Right-Click Dropdown -->
<div *ngIf="showDropdown" [ngStyle]="{ position: 'fixed', 'top.px': dropdownPosition.y, 'left.px': dropdownPosition.x }" class="dropdown-menu show">
<button class="dropdown-item d-flex" (click)="addReaction($event)">
<fa-icon [icon]="faSmile" class="item-icon"></fa-icon>
<span jhiTranslate="artemisApp.metis.post.addReaction"></span>
</button>
<button class="dropdown-item d-flex" (click)="editPosting()">
<fa-icon [icon]="faPencilAlt" class="item-icon"></fa-icon>
<span jhiTranslate="artemisApp.metis.post.editMessage"></span>
</button>
<button class="dropdown-item d-flex" (click)="deletePost()">
<fa-icon [icon]="faTrash" class="item-icon"></fa-icon>
<span jhiTranslate="artemisApp.metis.post.deleteMessage"></span>
</button>
</div>

<div #emojiPickerTrigger="cdkOverlayOrigin" cdkOverlayOrigin [ngStyle]="{ position: 'fixed', 'top.px': clickPosition.y, 'left.px': clickPosition.x }"></div>

<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
[cdkConnectedOverlayOrigin]="emojiPickerTrigger"
[cdkConnectedOverlayOpen]="showReactionSelector"
(backdropClick)="toggleEmojiSelect()"
>
<jhi-emoji-picker (emojiSelect)="selectReaction($event)"></jhi-emoji-picker>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,57 @@
padding-left: 0.5rem;
}
}

.message-container {
position: relative;
border-radius: 5px;
transition: background-color 0.3s ease;
}
Comment on lines +20 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM! Consider adding box-shadow for depth.

The .message-container class is well-structured and aligns with the PR objectives. The relative positioning and border-radius enhance the visual appeal, while the transition prepares for smooth hover effects.

To further improve the visual hierarchy, consider adding a subtle box-shadow:

 .message-container {
     position: relative;
     border-radius: 5px;
     transition: background-color 0.3s ease;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.message-container {
position: relative;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.message-container {
position: relative;
border-radius: 5px;
transition: background-color 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}


.message-content {
padding-left: 0.3rem;

&.force-hover {
background: var(--metis-selection-option-hover-background);

.hover-actions {
opacity: 1;
visibility: visible;
}
}
}

.message-content:hover {
background: var(--metis-selection-option-hover-background);
}
Comment on lines +39 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM! Consider adding keyboard focus styles for accessibility.

The hover effect for .message-content is well-implemented and aligns with the PR objectives. To improve accessibility, consider adding styles for keyboard focus:

-.message-content:hover {
+.message-content:hover,
+.message-content:focus-within {
     background: var(--metis-selection-option-hover-background);
 }

This ensures that keyboard users can also access the hover state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.message-content:hover {
background: var(--metis-selection-option-hover-background);
}
.message-content:hover,
.message-content:focus-within {
background: var(--metis-selection-option-hover-background);
}


.hover-actions {

Check warning on line 43 in src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/shared/metis/answer-post/answer-post.component.scss#L43

Expected selector ".hover-actions" to come before selector ".message-content.force-hover .hover-actions" (no-descending-specificity)
position: absolute;
top: -1.8rem;
right: 3%;
display: flex;
gap: 10px;
visibility: hidden;
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out;
background: var(--metis-selection-option-background);
padding: 5px;
border-radius: 5px;
border: 0.01rem solid var(--metis-gray);
}

.message-container:hover .hover-actions {
opacity: 1;
visibility: visible;
}
Comment on lines +59 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LGTM! Consider adding keyboard focus styles for accessibility.

The implementation for showing hover actions when the message container is hovered over is well done. To improve accessibility, consider adding styles for keyboard focus:

-.message-container:hover .hover-actions {
+.message-container:hover .hover-actions,
+.message-container:focus-within .hover-actions {
     opacity: 1;
     visibility: visible;
 }

This ensures that keyboard users can also access the hover actions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.message-container:hover .hover-actions {
opacity: 1;
visibility: visible;
}
.message-container:hover .hover-actions,
.message-container:focus-within .hover-actions {
opacity: 1;
visibility: visible;
}


.clickable {
cursor: pointer;
}

.item-icon {
width: 20px;
height: 20px;
margin-right: 0.2rem;
}
Loading
Loading