show reactions per user, allow only one reaction per message per user

This commit is contained in:
marlon-erler
2026-04-11 17:47:24 +00:00
parent a29cd023d5
commit cad4ebfdd4
8 changed files with 130 additions and 176 deletions
+2 -12
View File
@@ -416,11 +416,7 @@ export default class ChatModel {
const fileContent = FileModel.createFileContent(v4(), "reaction");
const reaction: ChatMessageReaction = {
...fileContent,
fileId: ChatModel.createMessageReactionId(
messageId,
content,
sender,
),
fileId: ChatModel.createMessageReactionId(messageId, sender),
messageId,
sender,
@@ -432,10 +428,9 @@ export default class ChatModel {
static createMessageReactionId = (
messageId: string,
content: ReactionSymbols,
sender: string,
): string => {
return messageId + sender + reactionStringMap[content];
return messageId + sender;
};
}
@@ -449,11 +444,6 @@ export enum ReactionSymbols {
Question = "❓",
}
const reactionStringMap: { [key: ReactionSymbols | string]: string } = {};
Object.entries(ReactionSymbols).forEach((value) => {
reactionStringMap[value[1]] = value[0];
});
export interface ChatInfo extends ValidObject {
primaryChannel: string;
secondaryChannels: string[];
+2 -2
View File
@@ -181,7 +181,7 @@
overflow: hidden;
}
.message-bubble > .message-reaction-row:has(button:not([zero])) {
.message-bubble > .message-reaction-row:has(button:not([count="0"])) {
display: grid !important;
}
@@ -195,7 +195,7 @@
animation: slide-up var(--animation-duration);
}
.message-bubble > .message-reaction-row button[zero] {
.message-bubble > .message-reaction-row button[count="0"] {
display: none;
}
+2 -24
View File
@@ -13,50 +13,35 @@ export function MessageReactionButton(
) {
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);
let isActive = React.createProxyState([chatMessageViewModel.userReaction], (() => chatMessageViewModel.userReaction.value == content))
function sendReaction() {
chatMessageViewModel.sendReaction(content, isActive.value);
}
function getUsername(): string {
return chatMessageViewModel.messagePageViewModel.chatViewModel
.settingsViewModel.username.value;
}
function checkIsHighlighted(): boolean {
return reactionState.value.has(getUsername());
}
switch (content) {
case ReactionSymbols.ThumbsUp: {
audioLabel =
coreViewModel.translations.chatPage.message.thumbsUpReaction;
count = chatMessageViewModel.reactionsThumbsUpCount;
reactionState = chatMessageViewModel.reactionsThumbsUp;
break;
}
case ReactionSymbols.Check: {
audioLabel =
coreViewModel.translations.chatPage.message.checkReaction;
count = chatMessageViewModel.reactionsCheckCount;
reactionState = chatMessageViewModel.reactionsCheck;
break;
}
case ReactionSymbols.Stop: {
audioLabel =
coreViewModel.translations.chatPage.message.stopReaction;
count = chatMessageViewModel.reactionsStopCount;
reactionState = chatMessageViewModel.reactionsStop;
break;
}
case ReactionSymbols.Attention: {
audioLabel =
coreViewModel.translations.chatPage.message.attentionReaction;
count = chatMessageViewModel.reactionsAttentionCount;
reactionState = chatMessageViewModel.reactionsAttention;
break;
}
case ReactionSymbols.DoubleAttention: {
@@ -64,30 +49,23 @@ export function MessageReactionButton(
coreViewModel.translations.chatPage.message
.doubleAttentionReaction;
count = chatMessageViewModel.reactionsDoubleAttentionCount;
reactionState = chatMessageViewModel.reactionsDoubleAttention;
break;
}
case ReactionSymbols.Question: {
audioLabel =
coreViewModel.translations.chatPage.message.questionReaction;
count = chatMessageViewModel.reactionsQuestionCount;
reactionState = chatMessageViewModel.reactionsQuestion;
break;
}
}
reactionState.subscribe(() => {
isActive.value = checkIsHighlighted();
isZero.value = reactionState.value.size == 0;
});
return (
<button
class="flex"
on:click={sendReaction}
aria-label={audioLabel}
toggle:selected={isActive}
toggle:zero={isZero}
set:count={count}
>
{content}
<span subscribe:innerText={count}></span>
@@ -0,0 +1,13 @@
import * as React from "bloatless-react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ChatMessageReaction } from "../../Model/Chat/chatModel";
export function MessageReactionEntry(reaction: ChatMessageReaction) {
return (
<div class="tile flex-row">
<span class="flex width-100">{reaction.sender}</span>
<span>{reaction.content}</span>
</div>
);
}
+15 -4
View File
@@ -4,6 +4,7 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { MessageReactionButtonRow } from "../Components/messageReactionButtonRow";
import { InfoTile } from "../Components/infoTile";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { MessageReactionEntry } from "../Components/messageReactionEntry";
export function ChatMessageInfoModal(
coreViewModel: CoreViewModel,
@@ -76,10 +77,20 @@ export function ChatMessageInfoModal(
<hr></hr>
{MessageReactionButtonRow(
coreViewModel,
chatMessageViewModel,
)}
<div class="flex-column gap">
{MessageReactionButtonRow(
coreViewModel,
chatMessageViewModel,
)}
<div
class="flex-column gap"
children:append={[
chatMessageViewModel.allReactions,
MessageReactionEntry,
]}
></div>
</div>
</main>
<button on:click={chatMessageViewModel.hideInfoModal}>
{coreViewModel.translations.general.closeButton}
+38 -45
View File
@@ -3,7 +3,6 @@ import * as React from "bloatless-react";
import {
ChatMessage,
ChatMessageReaction,
ChatMessageReactionReference,
ChatMessageStatuses,
ReactionSymbols,
} from "../../Model/Chat/chatModel";
@@ -23,36 +22,44 @@ export default class ChatMessageViewModel {
);
sentByUser: boolean;
reactionsThumbsUp = new React.MapState<ChatMessageReaction>();
reactionsCheck = new React.MapState<ChatMessageReaction>();
reactionsStop = new React.MapState<ChatMessageReaction>();
reactionsAttention = new React.MapState<ChatMessageReaction>();
reactionsDoubleAttention = new React.MapState<ChatMessageReaction>();
reactionsQuestion = new React.MapState<ChatMessageReaction>();
allReactions = new React.MapState<ChatMessageReaction>();
userReaction = new React.State<ReactionSymbols | string | undefined>(undefined);
reactionsThumbsUp = React.MapState<ChatMessageReaction>;
reactionsCheck = React.MapState<ChatMessageReaction>;
reactionsStop = React.MapState<ChatMessageReaction>;
reactionsAttention = React.MapState<ChatMessageReaction>;
reactionsDoubleAttention = React.MapState<ChatMessageReaction>;
reactionsQuestion = React.MapState<ChatMessageReaction>;
reactionsThumbsUpCount = React.createProxyState(
[this.reactionsThumbsUp],
() => this.reactionsThumbsUp.value.size,
generateReactionCountProxyState = (
content: ReactionSymbols,
): React.State<number> => {
return React.createProxyState(
[this.allReactions],
() =>
[...this.allReactions.value.values()].filter(
(x) => x.content == content,
).length,
);
};
reactionsThumbsUpCount = this.generateReactionCountProxyState(
ReactionSymbols.ThumbsUp,
);
reactionsCheckCount = React.createProxyState(
[this.reactionsCheck],
() => this.reactionsCheck.value.size,
reactionsCheckCount = this.generateReactionCountProxyState(
ReactionSymbols.Check,
);
reactionsStopCount = React.createProxyState(
[this.reactionsStop],
() => this.reactionsStop.value.size,
reactionsStopCount = this.generateReactionCountProxyState(
ReactionSymbols.Stop,
);
reactionsAttentionCount = React.createProxyState(
[this.reactionsAttention],
() => this.reactionsAttention.value.size,
reactionsAttentionCount = this.generateReactionCountProxyState(
ReactionSymbols.Attention,
);
reactionsDoubleAttentionCount = React.createProxyState(
[this.reactionsDoubleAttention],
() => this.reactionsDoubleAttention.value.size,
reactionsDoubleAttentionCount = this.generateReactionCountProxyState(
ReactionSymbols.DoubleAttention,
);
reactionsQuestionCount = React.createProxyState(
[this.reactionsQuestion],
() => this.reactionsQuestion.value.size,
reactionsQuestionCount = this.generateReactionCountProxyState(
ReactionSymbols.Question,
);
// state
@@ -80,28 +87,14 @@ export default class ChatMessageViewModel {
// reactions
handleReaction = (reaction: ChatMessageReaction): void => {
function setReaction(mapState: React.MapState<any>) {
if (reaction.isDeleting) {
mapState.remove(reaction.sender);
} else {
mapState.set(reaction.sender, reaction);
}
if (reaction.isDeleting) {
this.allReactions.remove(reaction.sender);
} else {
this.allReactions.set(reaction.sender, reaction);
}
switch (reaction.content) {
case ReactionSymbols.ThumbsUp:
return setReaction(this.reactionsThumbsUp);
case ReactionSymbols.Check:
return setReaction(this.reactionsCheck);
case ReactionSymbols.Stop:
return setReaction(this.reactionsStop);
case ReactionSymbols.Attention:
return setReaction(this.reactionsAttention);
case ReactionSymbols.DoubleAttention:
return setReaction(this.reactionsDoubleAttention);
case ReactionSymbols.Question:
return setReaction(this.reactionsQuestion);
}
if (reaction.sender != this.coreViewModel.settingsModel.username) return;
this.userReaction.value = reaction.isDeleting ? undefined : reaction.content;
};
sendReaction = (content: ReactionSymbols, isDeleting: boolean): void => {