This commit is contained in:
marlon-erler
2026-09-26 23:32:38 +02:00
parent ce9f6d89f0
commit 9622c196cd
40 changed files with 2866 additions and 2706 deletions
+169 -148
View File
@@ -15,156 +15,177 @@ export function MessagePage(
messagePageViewModel.loadData();
const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel);
};
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel);
};
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.filteredMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.filteredMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState(
persistenceId,
() =>
React.createProxyState(
[messagePageViewModel.replyingMessage],
() => {
if (messagePageViewModel.replyingMessage.value == undefined)
return <div></div>;
return ReplyPreview(
coreViewModel,
messagePageViewModel.replyingMessage.value,
);
},
),
);
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState(
persistenceId,
() =>
React.createProxyState(
[messagePageViewModel.replyingMessage],
() => {
if (messagePageViewModel.replyingMessage.value == undefined)
return <div></div>;
return ReplyPreview(
coreViewModel,
messagePageViewModel.replyingMessage.value,
);
},
),
);
function scrollDown(hard: boolean = false) {
if (hard) {
messageContainer.setAttribute("scroll-hard", "");
function scrollDown(hard: boolean = false) {
if (hard) {
messageContainer.setAttribute("scroll-hard", "");
}
messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard");
}
messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard");
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
React.bulkSubscribe([messagePageViewModel.reactionFilter, messagePageViewModel.searchViewModel.appliedQuery], scrollDown)
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => {
if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100);
})
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay();
});
const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined);
const title = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>messagePageViewModel.isReplyViewActive.value ? coreViewModel.translations.chatPage.message.replyHeaderLabel((messagePageViewModel.replyViewSelectedMessage.value!).body.value) : coreViewModel.translations.chatPage.message.messagesHeadline);
return (
<div id="message-page" toggle:reply-view={isInReplyView}>
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<button
class="ghost"
on:click={messagePageViewModel.resetReplyView}
toggle:hidden={messagePageViewModel.isReplyViewInactive}
>
<span class="icon">arrow_back</span>
</button>
<span
class="title ellipsis width-100 flex"
subscribe:innerText={title}
>
</span>
<span>
<button
class="ghost inset-outline"
on:click={messagePageViewModel.showFilterModal}
aria-label={
coreViewModel.translations.chatPage.message
.filterMessagesButtonAudioLabel
}
toggle:selected={
messagePageViewModel.isFilterActive
}
>
<span class="icon">filter_alt</span>
</button>
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div
class="reply-preview-wrapper"
children:set={replyPreview}
></div>
<div class="input-width-constraint">
<input
id="focused"
class="blur"
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
coreViewModel.translations.chatPage
.message
.composerInputPlaceholder
}
></input>
<button
class="primary blur"
aria-label={
coreViewModel.translations.chatPage
.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{MessageFilterModal(
coreViewModel,
messagePageViewModel,
ChatMessageViewModelToView,
)}
</div>
);
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
React.bulkSubscribe(
[
messagePageViewModel.reactionFilter,
messagePageViewModel.searchViewModel.appliedQuery,
],
scrollDown,
);
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(
(selectedMessage) => {
if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100);
},
);
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay();
});
const isInReplyView = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() => messagePageViewModel.replyViewSelectedMessage.value != undefined,
);
const title = React.createProxyState(
[messagePageViewModel.isReplyViewActive],
() =>
messagePageViewModel.isReplyViewActive.value
? coreViewModel.translations.chatPage.message.replyHeaderLabel(
messagePageViewModel.replyViewSelectedMessage.value!.body
.value,
)
: coreViewModel.translations.chatPage.message.messagesHeadline,
);
return (
<div id="message-page" toggle:reply-view={isInReplyView}>
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<button
class="ghost"
on:click={messagePageViewModel.resetReplyView}
toggle:hidden={
messagePageViewModel.isReplyViewInactive
}
>
<span class="icon">arrow_back</span>
</button>
<span
class="title ellipsis width-100 flex"
subscribe:innerText={title}
></span>
<span>
<button
class="ghost inset-outline"
on:click={messagePageViewModel.showFilterModal}
aria-label={
coreViewModel.translations.chatPage.message
.filterMessagesButtonAudioLabel
}
toggle:selected={
messagePageViewModel.isFilterActive
}
>
<span class="icon">filter_alt</span>
</button>
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div
class="reply-preview-wrapper"
children:set={replyPreview}
></div>
<div class="input-width-constraint">
<input
id="focused"
class="blur"
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
coreViewModel.translations.chatPage
.message
.composerInputPlaceholder
}
></input>
<button
class="primary blur"
aria-label={
coreViewModel.translations.chatPage
.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{MessageFilterModal(
coreViewModel,
messagePageViewModel,
ChatMessageViewModelToView,
)}
</div>
);
}