implement message filter by reactions

This commit is contained in:
marlon-erler
2026-04-11 21:01:23 +00:00
parent 45e3ed31ee
commit 9c0a20d22f
9 changed files with 261 additions and 7 deletions
+3
View File
@@ -763,6 +763,9 @@ body[showing-chat] #chat-page-wrapper {
grid-template-columns: repeat(3, minmax(80px, 1fr)); grid-template-columns: repeat(3, minmax(80px, 1fr));
} }
} }
.grid.message-reaction-filter {
grid-template-columns: repeat(3, 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 {
+94 -2
View File
@@ -2021,6 +2021,7 @@
this.status = new State( this.status = new State(
void 0 void 0
); );
this.isHidden = new State(false);
this.allReactions = new MapState(); this.allReactions = new MapState();
this.userReaction = new State( this.userReaction = new State(
void 0 void 0
@@ -2105,6 +2106,41 @@
this.chatMessage = chatMessage; this.chatMessage = chatMessage;
this.sentByUser = sentByUser; this.sentByUser = sentByUser;
this.loadData(); this.loadData();
this.messagePageViewModel.reactionFilter.subscribe((content) => {
console.log(content, content == void 0);
if (content == void 0) {
this.isHidden.value = false;
return;
}
let count = 0;
switch (content) {
case "\u{1F44D}" /* ThumbsUp */: {
count = this.reactionsThumbsUpCount.value;
break;
}
case "\u2705" /* Check */: {
count = this.reactionsCheckCount.value;
break;
}
case "\u{1F6D1}" /* Stop */: {
count = this.reactionsStopCount.value;
break;
}
case "\u2757\uFE0F" /* Attention */: {
count = this.reactionsAttentionCount.value;
break;
}
case "\u203C\uFE0F" /* DoubleAttention */: {
count = this.reactionsDoubleAttentionCount.value;
break;
}
case "\u2753" /* Question */: {
count = this.reactionsQuestionCount.value;
break;
}
}
this.isHidden.value = count == 0;
});
} }
}; };
@@ -2171,6 +2207,7 @@
this.chatMessageViewModels = new MapState(); this.chatMessageViewModels = new MapState();
this.filteredMessageViewModels = new ListState(); this.filteredMessageViewModels = new ListState();
this.isFilterModalOpen = new State(false); this.isFilterModalOpen = new State(false);
this.reactionFilter = new State(void 0);
this.composingMessage = new State(""); this.composingMessage = new State("");
// methods // methods
this.sendMessage = () => { this.sendMessage = () => {
@@ -2224,6 +2261,12 @@
this.closeFilterModal = () => { this.closeFilterModal = () => {
this.isFilterModalOpen.value = false; this.isFilterModalOpen.value = false;
}; };
this.revokeReactionFilter = () => {
this.reactionFilter.value = void 0;
};
this.setReactionFilter = (content) => {
this.reactionFilter.value = content;
};
// load // load
this.loadData = () => { this.loadData = () => {
this.chatMessageViewModels.clear(); this.chatMessageViewModels.clear();
@@ -3583,7 +3626,8 @@
"div", "div",
{ {
class: "message-bubble", class: "message-bubble",
"toggle:sentbyuser": chatMessageViewModel.sentByUser "toggle:sentbyuser": chatMessageViewModel.sentByUser,
"toggle:hidden": chatMessageViewModel.isHidden
}, },
/* @__PURE__ */ createElement("div", { class: "main tile" }, /* @__PURE__ */ createElement("div", { class: "text-container" }, /* @__PURE__ */ createElement("span", { class: "sender-name ellipsis" }, chatMessageViewModel.sender), /* @__PURE__ */ createElement( /* @__PURE__ */ createElement("div", { class: "main tile" }, /* @__PURE__ */ createElement("div", { class: "text-container" }, /* @__PURE__ */ createElement("span", { class: "sender-name ellipsis" }, chatMessageViewModel.sender), /* @__PURE__ */ createElement(
"span", "span",
@@ -3610,8 +3654,32 @@
); );
} }
// src/View/Components/messageReactionFilterButton.tsx
function MessageReactionFilterButton(messagePageViewModel, content) {
const select = () => {
messagePageViewModel.setReactionFilter(content);
};
const isSelected = createProxyState(
[messagePageViewModel.reactionFilter],
() => messagePageViewModel.reactionFilter.value == content
);
return /* @__PURE__ */ createElement(
"button",
{
class: "flex justify-center",
"on:click": select,
"toggle:selected": isSelected
},
content
);
}
// src/View/Modals/messageFilterModal.tsx // src/View/Modals/messageFilterModal.tsx
function MessageFilterModal(coreViewModel2, messagePageViewModel, converter) { function MessageFilterModal(coreViewModel2, messagePageViewModel, converter) {
const noFilter = createProxyState(
[messagePageViewModel.reactionFilter],
() => messagePageViewModel.reactionFilter.value == void 0
);
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": messagePageViewModel.isFilterModalOpen }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.chatPage.message.messageFilterHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": messagePageViewModel.isFilterModalOpen }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, coreViewModel2.translations.chatPage.message.messageFilterHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input", "input",
{ {
@@ -3628,7 +3696,25 @@
"toggle:disabled": messagePageViewModel.searchViewModel.cannotApplySearch "toggle:disabled": messagePageViewModel.searchViewModel.cannotApplySearch
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "search") /* @__PURE__ */ createElement("span", { class: "icon" }, "search")
)), /* @__PURE__ */ createElement("hr", null)), /* @__PURE__ */ createElement("button", { "on:click": messagePageViewModel.closeFilterModal }, coreViewModel2.translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("h3", null, coreViewModel2.translations.chatPage.message.messageFilterReactionsHadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "toggle:selected": noFilter, "on:click": messagePageViewModel.revokeReactionFilter }, coreViewModel2.translations.chatPage.message.messageFilterAllReactionsButton), /* @__PURE__ */ createElement("div", { class: "grid gap message-reaction-filter" }, MessageReactionFilterButton(
messagePageViewModel,
"\u{1F44D}" /* ThumbsUp */
), MessageReactionFilterButton(
messagePageViewModel,
"\u2705" /* Check */
), MessageReactionFilterButton(
messagePageViewModel,
"\u{1F6D1}" /* Stop */
), MessageReactionFilterButton(
messagePageViewModel,
"\u2757\uFE0F" /* Attention */
), MessageReactionFilterButton(
messagePageViewModel,
"\u203C\uFE0F" /* DoubleAttention */
), MessageReactionFilterButton(
messagePageViewModel,
"\u2753" /* Question */
)))), /* @__PURE__ */ createElement("button", { "on:click": messagePageViewModel.closeFilterModal }, coreViewModel2.translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
} }
// src/View/ChatPages/messagePage.tsx // src/View/ChatPages/messagePage.tsx
@@ -5164,6 +5250,8 @@
message: { message: {
messagesHeadline: "Messages", messagesHeadline: "Messages",
messageFilterHeadline: "Filter messages", messageFilterHeadline: "Filter messages",
messageFilterReactionsHadline: "Reactions",
messageFilterAllReactionsButton: "Show all",
/// ///
composerInputPlaceholder: "Type a message...", composerInputPlaceholder: "Type a message...",
sendMessageButtonAudioLabel: "send message", sendMessageButtonAudioLabel: "send message",
@@ -5372,6 +5460,8 @@
message: { message: {
messagesHeadline: "Nachrichten", messagesHeadline: "Nachrichten",
messageFilterHeadline: "Nachrichten filtern", messageFilterHeadline: "Nachrichten filtern",
messageFilterReactionsHadline: "Reaktionen",
messageFilterAllReactionsButton: "Alle anzeigen",
composerInputPlaceholder: "Schreib eine Nachricht...", composerInputPlaceholder: "Schreib eine Nachricht...",
sendMessageButtonAudioLabel: "nachricht senden", sendMessageButtonAudioLabel: "nachricht senden",
filterMessagesButtonAudioLabel: "nachrichten filtern", filterMessagesButtonAudioLabel: "nachrichten filtern",
@@ -5568,6 +5658,8 @@
message: { message: {
messagesHeadline: "Mensajes", messagesHeadline: "Mensajes",
messageFilterHeadline: "Filtrar mensajes", messageFilterHeadline: "Filtrar mensajes",
messageFilterReactionsHadline: "Reacciones",
messageFilterAllReactionsButton: "Mostrar todas",
composerInputPlaceholder: "Escribe un mensaje...", composerInputPlaceholder: "Escribe un mensaje...",
sendMessageButtonAudioLabel: "enviar mensaje", sendMessageButtonAudioLabel: "enviar mensaje",
filterMessagesButtonAudioLabel: "filtrar mensajes", filterMessagesButtonAudioLabel: "filtrar mensajes",
+4
View File
@@ -216,3 +216,7 @@
grid-template-columns: repeat(3, minmax(80px, 1fr)); grid-template-columns: repeat(3, minmax(80px, 1fr));
} }
} }
.grid.message-reaction-filter {
grid-template-columns: repeat(3, 1fr);
}
+1
View File
@@ -30,6 +30,7 @@ export function ChatMessage(
<div <div
class="message-bubble" class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser} toggle:sentbyuser={chatMessageViewModel.sentByUser}
toggle:hidden={chatMessageViewModel.isHidden}
> >
<div class="main tile"> <div class="main tile">
<div class="text-container"> <div class="text-container">
@@ -0,0 +1,32 @@
import * as React from "bloatless-react";
import {
ChatMessageReaction,
ReactionSymbols,
} from "../../Model/Chat/chatModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
export function MessageReactionFilterButton(
messagePageViewModel: MessagePageViewModel,
content: ReactionSymbols,
) {
const select = () => {
messagePageViewModel.setReactionFilter(content);
};
const isSelected = React.createProxyState(
[messagePageViewModel.reactionFilter],
() => messagePageViewModel.reactionFilter.value == content,
);
return (
<button
class="flex justify-center"
on:click={select}
toggle:selected={isSelected}
>
{content}
</button>
);
}
+68 -5
View File
@@ -5,24 +5,40 @@ import { StringToOption } from "../Components/option";
import { v4 } from "uuid"; import { v4 } from "uuid";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel"; import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import { MessageReactionFilterButton } from "../Components/messageReactionFilterButton";
import { ReactionSymbols } from "../../Model/Chat/chatModel";
export function MessageFilterModal<T>( export function MessageFilterModal<T>(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
messagePageViewModel: MessagePageViewModel, messagePageViewModel: MessagePageViewModel,
converter: React.StateItemConverter<T>, converter: React.StateItemConverter<T>,
) { ) {
const noFilter = React.createProxyState(
[messagePageViewModel.reactionFilter],
() => messagePageViewModel.reactionFilter.value == undefined,
);
return ( return (
<div class="modal" toggle:open={messagePageViewModel.isFilterModalOpen}> <div class="modal" toggle:open={messagePageViewModel.isFilterModalOpen}>
<div> <div>
<main> <main>
<h2>{coreViewModel.translations.chatPage.message.messageFilterHeadline}</h2> <h2>
{
coreViewModel.translations.chatPage.message
.messageFilterHeadline
}
</h2>
<div class="flex-row width-input"> <div class="flex-row width-input">
<input <input
placeholder={ placeholder={
coreViewModel.translations.general.searchLabel coreViewModel.translations.general.searchLabel
} }
bind:value={messagePageViewModel.searchViewModel.searchInput} bind:value={
on:enter={messagePageViewModel.searchViewModel.applySearch} messagePageViewModel.searchViewModel.searchInput
}
on:enter={
messagePageViewModel.searchViewModel.applySearch
}
></input> ></input>
<button <button
class="primary" class="primary"
@@ -30,14 +46,61 @@ export function MessageFilterModal<T>(
coreViewModel.translations.general coreViewModel.translations.general
.searchButtonAudioLabel .searchButtonAudioLabel
} }
on:click={messagePageViewModel.searchViewModel.applySearch} on:click={
toggle:disabled={messagePageViewModel.searchViewModel.cannotApplySearch} messagePageViewModel.searchViewModel.applySearch
}
toggle:disabled={
messagePageViewModel.searchViewModel
.cannotApplySearch
}
> >
<span class="icon">search</span> <span class="icon">search</span>
</button> </button>
</div> </div>
<hr></hr> <hr></hr>
<h3>
{
coreViewModel.translations.chatPage.message
.messageFilterReactionsHadline
}
</h3>
<div class="flex-column gap">
<button toggle:selected={noFilter} on:click={messagePageViewModel.revokeReactionFilter}>
{
coreViewModel.translations.chatPage.message
.messageFilterAllReactionsButton
}
</button>
<div class="grid gap message-reaction-filter">
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.ThumbsUp,
)}
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.Check,
)}
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.Stop,
)}
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.Attention,
)}
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.DoubleAttention,
)}
{MessageReactionFilterButton(
messagePageViewModel,
ReactionSymbols.Question,
)}
</div>
</div>
</main> </main>
<button on:click={messagePageViewModel.closeFilterModal}> <button on:click={messagePageViewModel.closeFilterModal}>
{coreViewModel.translations.general.closeButton} {coreViewModel.translations.general.closeButton}
+9
View File
@@ -184,7 +184,10 @@ const englishTranslations = {
message: { message: {
messagesHeadline: "Messages", messagesHeadline: "Messages",
messageFilterHeadline: "Filter messages", messageFilterHeadline: "Filter messages",
messageFilterReactionsHadline: "Reactions",
messageFilterAllReactionsButton: "Show all",
/// ///
@@ -482,7 +485,10 @@ export const allTranslations: {
message: { message: {
messagesHeadline: "Nachrichten", messagesHeadline: "Nachrichten",
messageFilterHeadline: "Nachrichten filtern", messageFilterHeadline: "Nachrichten filtern",
messageFilterReactionsHadline: "Reaktionen",
messageFilterAllReactionsButton: "Alle anzeigen",
composerInputPlaceholder: "Schreib eine Nachricht...", composerInputPlaceholder: "Schreib eine Nachricht...",
sendMessageButtonAudioLabel: "nachricht senden", sendMessageButtonAudioLabel: "nachricht senden",
@@ -754,7 +760,10 @@ export const allTranslations: {
message: { message: {
messagesHeadline: "Mensajes", messagesHeadline: "Mensajes",
messageFilterHeadline: "Filtrar mensajes", messageFilterHeadline: "Filtrar mensajes",
messageFilterReactionsHadline: "Reacciones",
messageFilterAllReactionsButton: "Mostrar todas",
composerInputPlaceholder: "Escribe un mensaje...", composerInputPlaceholder: "Escribe un mensaje...",
sendMessageButtonAudioLabel: "enviar mensaje", sendMessageButtonAudioLabel: "enviar mensaje",
@@ -22,6 +22,8 @@ export default class ChatMessageViewModel {
); );
sentByUser: boolean; sentByUser: boolean;
isHidden = new React.State<boolean>(false);
allReactions = new React.MapState<ChatMessageReaction>(); allReactions = new React.MapState<ChatMessageReaction>();
userReaction = new React.State<ReactionSymbols | string | undefined>( userReaction = new React.State<ReactionSymbols | string | undefined>(
undefined, undefined,
@@ -129,5 +131,43 @@ export default class ChatMessageViewModel {
this.chatMessage = chatMessage; this.chatMessage = chatMessage;
this.sentByUser = sentByUser; this.sentByUser = sentByUser;
this.loadData(); this.loadData();
this.messagePageViewModel.reactionFilter.subscribe((content) => {
console.log(content, content==undefined)
if (content == undefined) {
this.isHidden.value = false;
return;
}
let count = 0;
switch (content) {
case ReactionSymbols.ThumbsUp: {
count = this.reactionsThumbsUpCount.value;
break;
}
case ReactionSymbols.Check: {
count = this.reactionsCheckCount.value;
break;
}
case ReactionSymbols.Stop: {
count = this.reactionsStopCount.value;
break;
}
case ReactionSymbols.Attention: {
count = this.reactionsAttentionCount.value;
break;
}
case ReactionSymbols.DoubleAttention: {
count = this.reactionsDoubleAttentionCount.value;
break;
}
case ReactionSymbols.Question: {
count = this.reactionsQuestionCount.value;
break;
}
}
this.isHidden.value = count == 0;
})
} }
} }
@@ -18,6 +18,8 @@ export default class MessagePageViewModel {
new React.ListState(); new React.ListState();
searchViewModel: SearchViewModel<ChatMessageViewModel>; searchViewModel: SearchViewModel<ChatMessageViewModel>;
isFilterModalOpen = new React.State<boolean>(false); isFilterModalOpen = new React.State<boolean>(false);
reactionFilter = new React.State<ReactionSymbols | undefined>(undefined);
composingMessage: React.State<string> = new React.State(""); composingMessage: React.State<string> = new React.State("");
// guards // guards
@@ -94,6 +96,14 @@ export default class MessagePageViewModel {
this.isFilterModalOpen.value = false; this.isFilterModalOpen.value = false;
}; };
revokeReactionFilter = (): void => {
this.reactionFilter.value = undefined;
}
setReactionFilter = (content: ReactionSymbols): void => {
this.reactionFilter.value = content;
};
// load // load
loadData = (): void => { loadData = (): void => {
this.chatMessageViewModels.clear(); this.chatMessageViewModels.clear();