|
|
@@ -124,6 +124,15 @@ import { MatrixClientService } from '../../../core/matrix-client.service';
|
|
|
} @else if (messages().length === 0) {
|
|
|
<ui-empty title="工作组还没有消息" description="在下方 @ 一名或多名数字生命开始协作。" />
|
|
|
} @else {
|
|
|
+ <div class="history-hold">
|
|
|
+ @if (paginating()) {
|
|
|
+ <ui-skeleton variant="rect" height="32px" />
|
|
|
+ } @else if (hasMore()) {
|
|
|
+ <ui-button variant="ghost" size="sm" (click)="loadEarlier()">加载更早</ui-button>
|
|
|
+ } @else {
|
|
|
+ <span class="history-end">已到最早</span>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
@for (m of messages(); track m.id) {
|
|
|
<app-message-bubble
|
|
|
[message]="m"
|
|
|
@@ -381,6 +390,15 @@ import { MatrixClientService } from '../../../core/matrix-client.service';
|
|
|
gap: var(--sp-3);
|
|
|
align-content: start;
|
|
|
}
|
|
|
+ .history-hold {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ padding: var(--sp-1) 0;
|
|
|
+ }
|
|
|
+ .history-end {
|
|
|
+ font-size: var(--fs-12);
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
.skeleton-hold {
|
|
|
display: grid;
|
|
|
gap: var(--sp-2);
|
|
|
@@ -540,6 +558,10 @@ export class DkGroupPage {
|
|
|
readonly membersOpen = signal(true);
|
|
|
readonly mentionOpen = signal(false);
|
|
|
readonly mentionTargets = signal<Set<string>>(new Set());
|
|
|
+ /** 上滚加载历史中(防重入) */
|
|
|
+ readonly paginating = signal(false);
|
|
|
+ /** 还有更早的历史 */
|
|
|
+ readonly hasMore = signal(false);
|
|
|
|
|
|
private readonly messagesEl = viewChild<ElementRef<HTMLElement>>('msgList');
|
|
|
|
|
|
@@ -590,6 +612,7 @@ export class DkGroupPage {
|
|
|
replaceUrl: this.route.snapshot.paramMap.get('id') === roomId,
|
|
|
});
|
|
|
this.loadMessages(roomId);
|
|
|
+ this.hasMore.set(this.mx.paginationToken(roomId) != null);
|
|
|
this.loading.set(false);
|
|
|
this.scrollToBottom();
|
|
|
}
|
|
|
@@ -691,8 +714,45 @@ export class DkGroupPage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 滚到顶部附近 → 往前翻历史,并**保持视觉位置不动**。
|
|
|
+ *
|
|
|
+ * 与 DM 页同一套做法(那边踩过坑):
|
|
|
+ * 补偿必须在**渲染完成后**量 `scrollHeight`,`queueMicrotask` 时新内容
|
|
|
+ * 还没进 DOM,量到的是旧高度,补偿量算成 0 → 视口被顶飞。
|
|
|
+ */
|
|
|
onScroll(): void {
|
|
|
- // 预留:上滚加载历史(群聊消息量通常小于 DM,暂不做分页)
|
|
|
+ const el = this.messagesEl()?.nativeElement;
|
|
|
+ if (!el || el.scrollTop > 80) return;
|
|
|
+ if (this.paginating() || !this.hasMore()) return;
|
|
|
+ void this.loadEarlier();
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadEarlier(): Promise<void> {
|
|
|
+ const el = this.messagesEl()?.nativeElement;
|
|
|
+ const rid = this.roomId();
|
|
|
+ if (!el || !rid) return;
|
|
|
+ const beforeHeight = el.scrollHeight;
|
|
|
+ const beforeTop = el.scrollTop;
|
|
|
+ this.paginating.set(true);
|
|
|
+ try {
|
|
|
+ const grew = await this.mx.paginateBack(rid, 30);
|
|
|
+ if (!grew) {
|
|
|
+ this.hasMore.set(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.loadMessages(rid);
|
|
|
+ // 两帧后再量:一帧可能还没完成 markdown 渲染
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ const added = el.scrollHeight - beforeHeight;
|
|
|
+ if (added > 0) el.scrollTop = beforeTop + added;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ this.hasMore.set(this.mx.paginationToken(rid) != null);
|
|
|
+ } finally {
|
|
|
+ this.paginating.set(false);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private scrollToBottom(): void {
|