merge reply view header with message page title bar

This commit is contained in:
marlon-erler
2026-09-24 15:16:13 +02:00
parent 5ea256a2fa
commit 4deedf781a
5 changed files with 940 additions and 1096 deletions
+785 -952
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1,4 +1,8 @@
/* layout */ /* layout */
#chat-page #main #message-page[reply-view] .toolbar .title {
padding-left: 0px;
}
#chat-page #main #message-page .content { #chat-page #main #message-page .content {
padding: 0px; padding: 0px;
overflow: hidden; overflow: hidden;
+147 -141
View File
@@ -7,7 +7,6 @@ import * as React from "../../react";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel"; import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel"; import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import {ReplyViewHeader} from "../Components/replyViewHeader";
export function MessagePage( export function MessagePage(
coreViewModel: CoreViewModel, coreViewModel: CoreViewModel,
@@ -16,148 +15,155 @@ export function MessagePage(
messagePageViewModel.loadData(); messagePageViewModel.loadData();
const ChatMessageViewModelToView: React.StateItemConverter< const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => { > = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel); return ChatMessage(coreViewModel, chatMessageViewModel);
}; };
const messageContainer = ( const messageContainer = (
<div <div
id="message-container" id="message-container"
children:append={[ children:append={[
messagePageViewModel.filteredMessageViewModels, messagePageViewModel.filteredMessageViewModels,
ChatMessageViewModelToView, ChatMessageViewModelToView,
]} ]}
></div> ></div>
); );
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id; const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState( const replyPreview = ViewController.inlineReplies.setState(
persistenceId, persistenceId,
() => () =>
React.createProxyState( React.createProxyState(
[messagePageViewModel.replyingMessage], [messagePageViewModel.replyingMessage],
() => { () => {
if (messagePageViewModel.replyingMessage.value == undefined) if (messagePageViewModel.replyingMessage.value == undefined)
return <div></div>; return <div></div>;
return ReplyPreview( return ReplyPreview(
coreViewModel, coreViewModel,
messagePageViewModel.replyingMessage.value, messagePageViewModel.replyingMessage.value,
); );
}, },
), ),
); );
function scrollDown(hard: boolean = false) { function scrollDown(hard: boolean = false) {
if (hard) { if (hard) {
messageContainer.setAttribute("scroll-hard", ""); messageContainer.setAttribute("scroll-hard", "");
}
messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard");
} }
function scrollDownIfApplicable() { messageContainer.scrollTop = messageContainer.scrollHeight;
const scrollFromBottom = messageContainer.removeAttribute("scroll-hard");
messageContainer.scrollHeight - }
(messageContainer.scrollTop + messageContainer.offsetHeight); function scrollDownIfApplicable() {
if (scrollFromBottom > 400) return; const scrollFromBottom =
messageContainer.scrollHeight -
scrollDown(); (messageContainer.scrollTop + messageContainer.offsetHeight);
} if (scrollFromBottom > 400) return;
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable, scrollDown();
); }
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => { messagePageViewModel.filteredMessageViewModels.subscribeSilent(
if (selectedMessage != undefined) return; scrollDownIfApplicable,
setTimeout(scrollDown, 100); );
}) messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => {
setTimeout(() => scrollDown(true), 100); if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100);
messagePageViewModel.focusSetter.subscribeSilent(() => { })
ViewController.setFocusWithDelay(); setTimeout(() => scrollDown(true), 100);
});
messagePageViewModel.focusSetter.subscribeSilent(() => {
const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined); ViewController.setFocusWithDelay();
});
return (
<div id="message-page" toggle:reply-view={isInReplyView}> const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined);
<div class="pane-wrapper">
<div class="pane"> const title = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>messagePageViewModel.isReplyViewActive.value ? coreViewModel.translations.chatPage.message.replyHeaderLabel(messagePageViewModel.replyViewSelectedMessage.value.body.value) : coreViewModel.translations.chatPage.message.messagesHeadline);
<div class="toolbar">
<span class="title"> return (
{ <div id="message-page" toggle:reply-view={isInReplyView}>
coreViewModel.translations.chatPage.message <div class="pane-wrapper">
.messagesHeadline <div class="pane">
} <div class="toolbar">
</span> <button
<span> class="ghost"
<button on:click={messagePageViewModel.resetReplyView}
class="ghost inset-outline" toggle:hidden={messagePageViewModel.isReplyViewInactive}
on:click={messagePageViewModel.showFilterModal} >
aria-label={ <span class="icon">arrow_back</span>
coreViewModel.translations.chatPage.message </button>
.filterMessagesButtonAudioLabel <span
} class="title ellipsis width-100 flex"
toggle:selected={ subscribe:innerText={title}
messagePageViewModel.isFilterActive >
} </span>
> <span>
<span class="icon">filter_alt</span> <button
</button> class="ghost inset-outline"
</span> on:click={messagePageViewModel.showFilterModal}
</div> aria-label={
<div class="content"> coreViewModel.translations.chatPage.message
{ReplyViewHeader(coreViewModel, messagePageViewModel)} .filterMessagesButtonAudioLabel
{messageContainer} }
<div id="composer"> toggle:selected={
<div class="content-width-constraint"> messagePageViewModel.isFilterActive
<div }
class="reply-preview-wrapper" >
children:set={replyPreview} <span class="icon">filter_alt</span>
></div> </button>
<div class="input-width-constraint"> </span>
<input </div>
id="focused" <div class="content">
class="blur" {messageContainer}
bind:value={ <div id="composer">
messagePageViewModel.composingMessage <div class="content-width-constraint">
} <div
on:enter={ class="reply-preview-wrapper"
messagePageViewModel.sendMessage children:set={replyPreview}
} ></div>
placeholder={ <div class="input-width-constraint">
coreViewModel.translations.chatPage <input
.message id="focused"
.composerInputPlaceholder class="blur"
} bind:value={
></input> messagePageViewModel.composingMessage
<button }
class="primary blur" on:enter={
aria-label={ messagePageViewModel.sendMessage
coreViewModel.translations.chatPage }
.message placeholder={
.sendMessageButtonAudioLabel coreViewModel.translations.chatPage
} .message
on:click={ .composerInputPlaceholder
messagePageViewModel.sendMessage }
} ></input>
toggle:disabled={ <button
messagePageViewModel.cannotSendMessage class="primary blur"
} aria-label={
> coreViewModel.translations.chatPage
<span class="icon">send</span> .message
</button> .sendMessageButtonAudioLabel
</div> }
</div> on:click={
</div> messagePageViewModel.sendMessage
</div> }
</div> toggle:disabled={
</div> messagePageViewModel.cannotSendMessage
}
{MessageFilterModal( >
coreViewModel, <span class="icon">send</span>
messagePageViewModel, </button>
ChatMessageViewModelToView, </div>
)} </div>
</div> </div>
); </div>
</div>
</div>
{MessageFilterModal(
coreViewModel,
messagePageViewModel,
ChatMessageViewModelToView,
)}
</div>
);
} }
+3 -3
View File
@@ -252,7 +252,7 @@ const englishTranslations = {
// //
replyPrefixLabel: "Replies: ", replyPrefixLabel: "Replies: ",
replyHeaderLabel: (sender: string) => `Replies to ${sender}`, replyHeaderLabel: (message: string) => `Replies to "${message}"`,
}, },
task: { task: {
@@ -576,7 +576,7 @@ export const allTranslations: {
questionReaction: "Reaktion: Fragezeichen", questionReaction: "Reaktion: Fragezeichen",
replyPrefixLabel: "Antworten: ", replyPrefixLabel: "Antworten: ",
replyHeaderLabel: (sender: string) => `Antworten an ${sender}`, replyHeaderLabel: (message: string) => `Antworten an "${message}"`,
}, },
task: { task: {
@@ -881,7 +881,7 @@ export const allTranslations: {
questionReaction: "Reaccion: signo de interrogación", questionReaction: "Reaccion: signo de interrogación",
replyPrefixLabel: "Respuestas: ", replyPrefixLabel: "Respuestas: ",
replyHeaderLabel: (sender: string) => `Respuestas a ${sender}`, replyHeaderLabel: (message: string) => `Respuestas a "${message}"`,
}, },
task: { task: {
@@ -18,6 +18,7 @@ export default class MessagePageViewModel extends Context {
new React.ListState(); new React.ListState();
replyViewSelectedMessage: React.State<ChatMessageViewModel|undefined> = new React.State(undefined); replyViewSelectedMessage: React.State<ChatMessageViewModel|undefined> = new React.State(undefined);
isReplyViewActive: React.State<boolean> = React.createProxyState([this.replyViewSelectedMessage], ()=>this.replyViewSelectedMessage.value != undefined); isReplyViewActive: React.State<boolean> = React.createProxyState([this.replyViewSelectedMessage], ()=>this.replyViewSelectedMessage.value != undefined);
isReplyViewInactive = React.createProxyState([this.isReplyViewActive], ()=>!this.isReplyViewActive.value);
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); reactionFilter = new React.State<ReactionSymbols | undefined>(undefined);