improve reaction interface on message bubbles

This commit is contained in:
marlon-erler
2026-04-07 20:06:00 +00:00
parent 528650c66b
commit 01d57038e9
7 changed files with 92 additions and 67 deletions
+21
View File
@@ -659,6 +659,7 @@ body[showing-chat] #chat-page-wrapper {
} }
.message-bubble { .message-bubble {
display: flex; display: flex;
flex-direction: column;
width: 100%; width: 100%;
max-width: var(--max-content-width); max-width: var(--max-content-width);
} }
@@ -728,6 +729,26 @@ body[showing-chat] #chat-page-wrapper {
.message-bubble[sentbyuser] .tile.main span { .message-bubble[sentbyuser] .tile.main span {
text-align: right; text-align: right;
} }
.message-bubble > .message-reaction-row {
max-width: var(--max-input-width);
width: 100%;
padding: var(--gap-between-tiles);
background: var(--surface);
}
.message-bubble > .message-reaction-row button {
min-height: unset;
height: 100%;
width: 100%;
padding: var(--control-padding-compact);
gap: 0px;
flex: 1 !important;
}
.message-bubble > .message-reaction-row button[zero] {
display: none;
}
.message-reaction-row {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
/* src/View/ChatPages/taskPage.css */ /* src/View/ChatPages/taskPage.css */
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content { #chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
+21 -30
View File
@@ -3943,74 +3943,64 @@
// src/View/Components/messageReactionButton.tsx // src/View/Components/messageReactionButton.tsx
function MessageReactionButton(chatMessageViewModel, content) { function MessageReactionButton(chatMessageViewModel, content) {
let audioLabel;
let count;
let reactionState;
let isActive = new State(false);
let isZero = new State(true);
function sendReaction() { function sendReaction() {
chatMessageViewModel.sendReaction(content, isActive.value); chatMessageViewModel.sendReaction(content, isActive.value);
} }
function getUsername() { function getUsername() {
return chatMessageViewModel.messagePageViewModel.chatViewModel.settingsViewModel.username.value; return chatMessageViewModel.messagePageViewModel.chatViewModel.settingsViewModel.username.value;
} }
function checkIsHighlighted(mapState) { function checkIsHighlighted() {
return mapState.value.has(getUsername()); return reactionState.value.has(getUsername());
} }
let audioLabel;
let count;
let isActive;
switch (content) { switch (content) {
case "\u{1F44D}" /* ThumbsUp */: { case "\u{1F44D}" /* ThumbsUp */: {
audioLabel = translations.chatPage.message.thumbsUpReaction; audioLabel = translations.chatPage.message.thumbsUpReaction;
count = chatMessageViewModel.reactionsThumbsUpCount; count = chatMessageViewModel.reactionsThumbsUpCount;
isActive = createProxyState( reactionState = chatMessageViewModel.reactionsThumbsUp;
[chatMessageViewModel.reactionsThumbsUp],
() => checkIsHighlighted(chatMessageViewModel.reactionsThumbsUp)
);
break; break;
} }
case "\u2705" /* Check */: { case "\u2705" /* Check */: {
audioLabel = translations.chatPage.message.checkReaction; audioLabel = translations.chatPage.message.checkReaction;
count = chatMessageViewModel.reactionsCheckCount; count = chatMessageViewModel.reactionsCheckCount;
isActive = createProxyState( reactionState = chatMessageViewModel.reactionsCheck;
[chatMessageViewModel.reactionsCheck],
() => checkIsHighlighted(chatMessageViewModel.reactionsCheck)
);
break; break;
} }
case "\u2757\uFE0F" /* Attention */: { case "\u2757\uFE0F" /* Attention */: {
audioLabel = translations.chatPage.message.attentionReaction; audioLabel = translations.chatPage.message.attentionReaction;
count = chatMessageViewModel.reactionsAttentionCount; count = chatMessageViewModel.reactionsAttentionCount;
isActive = createProxyState( reactionState = chatMessageViewModel.reactionsAttention;
[chatMessageViewModel.reactionsAttention],
() => checkIsHighlighted(chatMessageViewModel.reactionsAttention)
);
break; break;
} }
case "\u203C\uFE0F" /* DoubleAttention */: { case "\u203C\uFE0F" /* DoubleAttention */: {
audioLabel = translations.chatPage.message.doubleAttentionReaction; audioLabel = translations.chatPage.message.doubleAttentionReaction;
count = chatMessageViewModel.reactionsDoubleAttentionCount; count = chatMessageViewModel.reactionsDoubleAttentionCount;
isActive = createProxyState( reactionState = chatMessageViewModel.reactionsDoubleAttention;
[chatMessageViewModel.reactionsDoubleAttention],
() => checkIsHighlighted(
chatMessageViewModel.reactionsDoubleAttention
)
);
break; break;
} }
case "\u2753" /* Question */: { case "\u2753" /* Question */: {
audioLabel = translations.chatPage.message.questionReaction; audioLabel = translations.chatPage.message.questionReaction;
count = chatMessageViewModel.reactionsQuestionCount; count = chatMessageViewModel.reactionsQuestionCount;
isActive = createProxyState( reactionState = chatMessageViewModel.reactionsQuestion;
[chatMessageViewModel.reactionsQuestion],
() => checkIsHighlighted(chatMessageViewModel.reactionsQuestion)
);
break; break;
} }
} }
reactionState.subscribe(() => {
isActive.value = checkIsHighlighted();
isZero.value = reactionState.value.size == 0;
});
return /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "flex", class: "flex",
"on:click": sendReaction, "on:click": sendReaction,
"aria-label": audioLabel, "aria-label": audioLabel,
"toggle:selected": isActive "toggle:selected": isActive,
"toggle:zero": isZero
}, },
content, content,
/* @__PURE__ */ createElement("span", { "subscribe:innerText": count }) /* @__PURE__ */ createElement("span", { "subscribe:innerText": count })
@@ -4019,7 +4009,7 @@
// src/View/Components/messageReactionButtonRow.tsx // src/View/Components/messageReactionButtonRow.tsx
function MessageReactionButtonRow(chatMessageViewModel) { function MessageReactionButtonRow(chatMessageViewModel) {
return /* @__PURE__ */ createElement("div", { class: "flex-row gap" }, MessageReactionButton( return /* @__PURE__ */ createElement("div", { class: "grid gap width-100 message-reaction-row" }, MessageReactionButton(
chatMessageViewModel, chatMessageViewModel,
"\u{1F44D}" /* ThumbsUp */ "\u{1F44D}" /* ThumbsUp */
), MessageReactionButton( ), MessageReactionButton(
@@ -4090,7 +4080,7 @@
class: "icon", class: "icon",
"subscribe:innerText": statusIcon "subscribe:innerText": statusIcon
} }
), chatMessageViewModel.dateSent), MessageReactionButtonRow(chatMessageViewModel)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement( ), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement(
"button", "button",
{ {
"on:click": chatMessageViewModel.showInfoModal, "on:click": chatMessageViewModel.showInfoModal,
@@ -4098,6 +4088,7 @@
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "info") /* @__PURE__ */ createElement("span", { class: "icon" }, "info")
))), ))),
MessageReactionButtonRow(chatMessageViewModel),
ChatMessageInfoModal(chatMessageViewModel) ChatMessageInfoModal(chatMessageViewModel)
); );
} }
+1
View File
@@ -288,6 +288,7 @@ SIZES
/* padding */ /* padding */
--control-padding: 1rem; --control-padding: 1rem;
--control-padding-compact: .4rem .8rem;
--control-marker-padding: 0.7rem; --control-marker-padding: 0.7rem;
--content-padding: 1rem; --content-padding: 1rem;
--modal-padding: 1.25rem; --modal-padding: 1.25rem;
+26
View File
@@ -75,6 +75,7 @@
.message-bubble { .message-bubble {
display: flex; display: flex;
flex-direction: column;
width: 100%; width: 100%;
max-width: var(--max-content-width); max-width: var(--max-content-width);
} }
@@ -166,3 +167,28 @@
.message-bubble[sentbyuser] .tile.main span { .message-bubble[sentbyuser] .tile.main span {
text-align: right; text-align: right;
} }
.message-bubble > .message-reaction-row {
max-width: var(--max-input-width);
width: 100%;
padding: var(--gap-between-tiles);
background: var(--surface);
}
.message-bubble > .message-reaction-row button {
min-height: unset;
height: 100%;
width: 100%;
padding: var(--control-padding-compact);
gap: 0px;
flex: 1 !important;
}
.message-bubble > .message-reaction-row button[zero] {
display: none;
}
.message-reaction-row {
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
}
+2 -3
View File
@@ -44,8 +44,6 @@ export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
></span> ></span>
{chatMessageViewModel.dateSent} {chatMessageViewModel.dateSent}
</span> </span>
{MessageReactionButtonRow(chatMessageViewModel)}
</div> </div>
<div class="button-container"> <div class="button-container">
@@ -59,8 +57,9 @@ export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
<span class="icon">info</span> <span class="icon">info</span>
</button> </button>
</div> </div>
</div>
</div>
{MessageReactionButtonRow(chatMessageViewModel)}
{ChatMessageInfoModal(chatMessageViewModel)} {ChatMessageInfoModal(chatMessageViewModel)}
</div> </div>
); );
+20 -33
View File
@@ -1,5 +1,5 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { ReactionSymbols } from "../../Model/Chat/chatModel"; import { ChatMessageReaction, ReactionSymbols } from "../../Model/Chat/chatModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations"; import { translations } from "../translations";
@@ -7,6 +7,12 @@ export function MessageReactionButton(
chatMessageViewModel: ChatMessageViewModel, chatMessageViewModel: ChatMessageViewModel,
content: ReactionSymbols, content: ReactionSymbols,
) { ) {
let audioLabel: string;
let count: React.State<number>;
let reactionState: React.MapState<ChatMessageReaction>;
let isActive = new React.State(false);
let isZero = new React.State(true);
function sendReaction() { function sendReaction() {
chatMessageViewModel.sendReaction(content, isActive.value); chatMessageViewModel.sendReaction(content, isActive.value);
} }
@@ -16,74 +22,55 @@ export function MessageReactionButton(
.settingsViewModel.username.value; .settingsViewModel.username.value;
} }
function checkIsHighlighted(mapState: React.MapState<any>): boolean { function checkIsHighlighted(): boolean {
return mapState.value.has(getUsername()); return reactionState.value.has(getUsername());
} }
let audioLabel: string;
let count: React.State<number>;
let isActive: React.State<boolean>;
switch (content) { switch (content) {
case ReactionSymbols.ThumbsUp: { case ReactionSymbols.ThumbsUp: {
audioLabel = translations.chatPage.message.thumbsUpReaction; audioLabel = translations.chatPage.message.thumbsUpReaction;
count = chatMessageViewModel.reactionsThumbsUpCount; count = chatMessageViewModel.reactionsThumbsUpCount;
isActive = React.createProxyState( reactionState = chatMessageViewModel.reactionsThumbsUp;
[chatMessageViewModel.reactionsThumbsUp],
() =>
checkIsHighlighted(chatMessageViewModel.reactionsThumbsUp),
);
break; break;
} }
case ReactionSymbols.Check: { case ReactionSymbols.Check: {
audioLabel = translations.chatPage.message.checkReaction; audioLabel = translations.chatPage.message.checkReaction;
count = chatMessageViewModel.reactionsCheckCount; count = chatMessageViewModel.reactionsCheckCount;
isActive = React.createProxyState( reactionState = chatMessageViewModel.reactionsCheck;
[chatMessageViewModel.reactionsCheck],
() => checkIsHighlighted(chatMessageViewModel.reactionsCheck),
);
break; break;
} }
case ReactionSymbols.Attention: { case ReactionSymbols.Attention: {
audioLabel = translations.chatPage.message.attentionReaction; audioLabel = translations.chatPage.message.attentionReaction;
count = chatMessageViewModel.reactionsAttentionCount; count = chatMessageViewModel.reactionsAttentionCount;
isActive = React.createProxyState( reactionState = chatMessageViewModel.reactionsAttention;
[chatMessageViewModel.reactionsAttention],
() =>
checkIsHighlighted(chatMessageViewModel.reactionsAttention),
);
break; break;
} }
case ReactionSymbols.DoubleAttention: { case ReactionSymbols.DoubleAttention: {
audioLabel = translations.chatPage.message.doubleAttentionReaction; audioLabel = translations.chatPage.message.doubleAttentionReaction;
count = chatMessageViewModel.reactionsDoubleAttentionCount; count = chatMessageViewModel.reactionsDoubleAttentionCount;
isActive = React.createProxyState( reactionState = chatMessageViewModel.reactionsDoubleAttention;
[chatMessageViewModel.reactionsDoubleAttention],
() =>
checkIsHighlighted(
chatMessageViewModel.reactionsDoubleAttention,
),
);
break; break;
} }
case ReactionSymbols.Question: { case ReactionSymbols.Question: {
audioLabel = translations.chatPage.message.questionReaction; audioLabel = translations.chatPage.message.questionReaction;
count = chatMessageViewModel.reactionsQuestionCount; count = chatMessageViewModel.reactionsQuestionCount;
isActive = React.createProxyState( reactionState = chatMessageViewModel.reactionsQuestion;
[chatMessageViewModel.reactionsQuestion],
() =>
checkIsHighlighted(chatMessageViewModel.reactionsQuestion),
);
break; break;
} }
} }
reactionState.subscribe(() => {
isActive.value = checkIsHighlighted();
isZero.value = reactionState.value.size == 0;
})
return ( return (
<button <button
class="flex" class="flex"
on:click={sendReaction} on:click={sendReaction}
aria-label={audioLabel} aria-label={audioLabel}
toggle:selected={isActive} toggle:selected={isActive}
toggle:zero={isZero}
> >
{content} {content}
<span subscribe:innerText={count}></span> <span subscribe:innerText={count}></span>
@@ -8,7 +8,7 @@ export function MessageReactionButtonRow(
chatMessageViewModel: ChatMessageViewModel, chatMessageViewModel: ChatMessageViewModel,
) { ) {
return ( return (
<div class="flex-row gap"> <div class="grid gap width-100 message-reaction-row">
{MessageReactionButton( {MessageReactionButton(
chatMessageViewModel, chatMessageViewModel,
ReactionSymbols.ThumbsUp, ReactionSymbols.ThumbsUp,