fix: Improve plugin ui entry points, Fix chat scroll, fix notifications, fix user rights

This commit is contained in:
2026-05-17 16:09:16 +02:00
parent 8e3ccf4157
commit 8631290c01
35 changed files with 1560 additions and 619 deletions

View File

@@ -52,6 +52,7 @@ import {
})
export class ChatMessagesComponent {
@ViewChild(ChatMessageComposerComponent) composer?: ChatMessageComposerComponent;
@ViewChild(ChatMessageListComponent) messageList?: ChatMessageListComponent;
private readonly electronBridge = inject(ElectronBridgeService);
private readonly store = inject(Store);
@@ -98,6 +99,8 @@ export class ChatMessagesComponent {
}
handleMessageSubmitted(event: ChatMessageComposerSubmitEvent): void {
this.messageList?.scrollToBottomAfterLocalSend();
this.store.dispatch(
MessagesActions.sendMessage({
content: event.content,

View File

@@ -141,9 +141,11 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
return lookup;
});
private initialScrollObserver: MutationObserver | null = null;
private initialScrollTimer: ReturnType<typeof setTimeout> | null = null;
private bottomScrollObserver: MutationObserver | null = null;
private bottomScrollTimer: ReturnType<typeof setTimeout> | null = null;
private boundOnImageLoad: (() => void) | null = null;
private localSendScrollPending = false;
private localSendScrollTimer: ReturnType<typeof setTimeout> | null = null;
private isAutoScrolling = false;
private lastMessageCount = 0;
private initialScrollPending = true;
@@ -170,10 +172,17 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
const newMessages = currentCount > this.lastMessageCount;
const forceLocalSendScroll = this.shouldForceLocalSendScroll();
if (newMessages) {
if (distanceFromBottom <= 300) {
this.scheduleScrollToBottomSmooth();
if (forceLocalSendScroll || distanceFromBottom <= 300) {
if (forceLocalSendScroll) {
this.clearLocalSendScrollPending();
this.scheduleScrollToBottomAfterRender(true);
} else {
this.scheduleScrollToBottomSmooth();
}
this.showNewMessagesBar.set(false);
} else {
queueMicrotask(() => this.showNewMessagesBar.set(true));
@@ -198,7 +207,8 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
this.isAutoScrolling = false;
});
this.startInitialScrollWatch();
this.clearLocalSendScrollPending();
this.startBottomScrollWatch();
this.showNewMessagesBar.set(false);
this.lastMessageCount = this.messages().length;
this.scheduleCodeHighlight();
@@ -214,7 +224,8 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
}
ngOnDestroy(): void {
this.stopInitialScrollWatch();
this.stopBottomScrollWatch();
this.clearLocalSendScrollPending();
}
findRepliedMessage(messageId?: string | null): Message | undefined {
@@ -237,8 +248,8 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
this.showNewMessagesBar.set(false);
}
if (this.initialScrollObserver) {
this.stopInitialScrollWatch();
if (this.bottomScrollObserver) {
this.stopBottomScrollWatch();
}
if (element.scrollTop < 150 && this.hasMoreMessages() && !this.loadingMore()) {
@@ -275,6 +286,13 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
this.showNewMessagesBar.set(false);
}
scrollToBottomAfterLocalSend(): void {
this.localSendScrollPending = true;
this.showNewMessagesBar.set(false);
this.scheduleScrollToBottomAfterRender(true);
this.armLocalSendScrollTimeout();
}
scrollToMessage(messageId: string): void {
const container = this.messagesContainer?.nativeElement;
@@ -336,54 +354,42 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
private resetScrollingState(): void {
this.initialScrollPending = true;
this.stopInitialScrollWatch();
this.stopBottomScrollWatch();
this.clearLocalSendScrollPending();
this.showNewMessagesBar.set(false);
this.lastMessageCount = 0;
this.displayLimit.set(this.PAGE_SIZE);
}
private startInitialScrollWatch(): void {
this.stopInitialScrollWatch();
private startBottomScrollWatch(): void {
this.stopBottomScrollWatch();
const element = this.messagesContainer?.nativeElement;
if (!element)
return;
const snapToBottom = () => {
const container = this.messagesContainer?.nativeElement;
if (!container)
return;
this.isAutoScrolling = true;
container.scrollTop = container.scrollHeight;
requestAnimationFrame(() => {
this.isAutoScrolling = false;
});
};
this.initialScrollObserver = new MutationObserver(() => {
requestAnimationFrame(snapToBottom);
this.bottomScrollObserver = new MutationObserver(() => {
requestAnimationFrame(() => this.scrollToBottomInstant());
});
this.initialScrollObserver.observe(element, {
this.bottomScrollObserver.observe(element, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src']
});
this.boundOnImageLoad = () => requestAnimationFrame(snapToBottom);
this.boundOnImageLoad = () => requestAnimationFrame(() => this.scrollToBottomInstant());
element.addEventListener('load', this.boundOnImageLoad, true);
this.initialScrollTimer = setTimeout(() => this.stopInitialScrollWatch(), 5000);
this.bottomScrollTimer = setTimeout(() => this.stopBottomScrollWatch(), 5000);
}
private stopInitialScrollWatch(): void {
if (this.initialScrollObserver) {
this.initialScrollObserver.disconnect();
this.initialScrollObserver = null;
private stopBottomScrollWatch(): void {
if (this.bottomScrollObserver) {
this.bottomScrollObserver.disconnect();
this.bottomScrollObserver = null;
}
if (this.boundOnImageLoad && this.messagesContainer) {
@@ -392,12 +398,41 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
this.boundOnImageLoad = null;
}
if (this.initialScrollTimer) {
clearTimeout(this.initialScrollTimer);
this.initialScrollTimer = null;
if (this.bottomScrollTimer) {
clearTimeout(this.bottomScrollTimer);
this.bottomScrollTimer = null;
}
}
private armLocalSendScrollTimeout(): void {
if (this.localSendScrollTimer) {
clearTimeout(this.localSendScrollTimer);
}
this.localSendScrollTimer = setTimeout(() => {
this.localSendScrollPending = false;
this.localSendScrollTimer = null;
}, 1000);
}
private clearLocalSendScrollPending(): void {
this.localSendScrollPending = false;
if (this.localSendScrollTimer) {
clearTimeout(this.localSendScrollTimer);
this.localSendScrollTimer = null;
}
}
private shouldForceLocalSendScroll(): boolean {
if (!this.localSendScrollPending)
return false;
const latestMessage = this.channelMessages().at(-1);
return !!latestMessage && latestMessage.senderId === this.currentUserId();
}
private getMessageDateTimestamp(message: Message): number {
return message.timestamp || getMessageTimestamp(message);
}
@@ -424,6 +459,31 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
}
}
private scrollToBottomInstant(): void {
const element = this.messagesContainer?.nativeElement;
if (!element)
return;
this.isAutoScrolling = true;
element.scrollTop = element.scrollHeight;
requestAnimationFrame(() => {
this.isAutoScrolling = false;
});
}
private scheduleScrollToBottomAfterRender(watchForLayoutChanges = false): void {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.scrollToBottomInstant();
if (watchForLayoutChanges) {
this.startBottomScrollWatch();
}
});
});
}
private scheduleScrollToBottomSmooth(): void {
requestAnimationFrame(() => {
requestAnimationFrame(() => this.scrollToBottomSmooth());