mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): size the chat skeleton from the remembered viewport height (#1843)
This commit is contained in:
1 parent
bd381ccc74
commit
01a29d629b
9 files changed
+88
-49
No files matched your search
@@ -182,7 +182,7 @@
|
||||
overflow: hidden;
|
||||
padding-left: var(--chat-mobile-horizontal-padding);
|
||||
padding-right: var(--chat-mobile-horizontal-padding);
|
||||
padding-bottom: var(--messages-bottom-clearance);
|
||||
padding-bottom: calc(var(--messages-bottom-clearance) - var(--message-filler-padding-bottom));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import styles from '@app/features/app/components/skeleton/ChatSkeleton.module.css';
|
||||
import {usePlaceholderSpecs} from '@app/features/app/components/skeleton/PlaceholderSpecs';
|
||||
import {useMessageListPlaceholderSpecs} from '@app/features/app/components/skeleton/PlaceholderSpecs';
|
||||
import {
|
||||
ChatSkeletonChannelKind,
|
||||
type ChatSkeletonPresentation,
|
||||
@@ -34,8 +34,6 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {Fragment, useState} from 'react';
|
||||
|
||||
const MESSAGE_LIST_SEED_KEY = 'chat-skeleton-message-list';
|
||||
|
||||
const HEADER_BACK_ICON_SIZE = skeletonSurfaceVar('--channel-header-back-icon-size');
|
||||
const HEADER_ICON_SIZE = skeletonSurfaceVar('--channel-header-icon-size');
|
||||
const HEADER_DM_AVATAR_SIZE = remFromPx(CHANNEL_HEADER_DM_AVATAR_SIZE_PX);
|
||||
@@ -176,12 +174,11 @@ export const ChatSkeleton = observer(function ChatSkeleton({presentation}: ChatS
|
||||
composerActionCount = composerLayout.mobileActionCount;
|
||||
composerDividerVisible = false;
|
||||
}
|
||||
const messageSpecs = usePlaceholderSpecs({
|
||||
const messageSpecs = useMessageListPlaceholderSpecs({
|
||||
channelId: presentation.channelId,
|
||||
compact: messagePresentation.compact,
|
||||
compactAvatarsVisible: messagePresentation.compactAvatarsVisible,
|
||||
groupSpacing: messagePresentation.groupSpacingPx,
|
||||
viewportHeightPx: messagePresentation.viewportHeightPx,
|
||||
seedKey: MESSAGE_LIST_SEED_KEY,
|
||||
});
|
||||
const rootStyle: ChatSkeletonRootStyle = {
|
||||
'--chat-horizontal-padding': remFromPx(messagePresentation.messageGutterPx),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
getRememberedSkeletonMessagePresentation,
|
||||
resolveDefaultSkeletonChatViewportHeightPx,
|
||||
} from '@app/features/app/components/skeleton/SkeletonLayoutMemory';
|
||||
import {createSkeletonRandomFromKey} from '@app/features/app/components/skeleton/SkeletonSeed';
|
||||
import {MESSAGE_LAYOUT_SPEC} from '@app/features/theme/layout/MessageLayoutSpec';
|
||||
import {REM_BASE_PX} from '@app/features/theme/layout/RemFromPx';
|
||||
import {useMemo} from 'react';
|
||||
import {useMemo, useState} from 'react';
|
||||
|
||||
function pxFromRemLength(value: `${number}rem`): number {
|
||||
return Math.round(Number.parseFloat(value) * REM_BASE_PX);
|
||||
@@ -35,29 +39,35 @@ export interface PlaceholderSpecs {
|
||||
readonly groupSpacing: number;
|
||||
}
|
||||
|
||||
export const FILLER_WRAPPER_PADDING_TOP = '1rem' as const;
|
||||
export const FILLER_WRAPPER_PADDING_BOTTOM = '0.75rem' as const;
|
||||
export const FILLER_WRAPPER_VERTICAL_PADDING =
|
||||
pxFromRemLength(FILLER_WRAPPER_PADDING_TOP) + pxFromRemLength(FILLER_WRAPPER_PADDING_BOTTOM);
|
||||
const MESSAGE_LINE_HEIGHT = pxFromRemLength(MESSAGE_LAYOUT_SPEC.lineHeight);
|
||||
const MESSAGE_ROW_VERTICAL_PADDING = pxFromRemLength(MESSAGE_LAYOUT_SPEC.spacingY) * 2;
|
||||
const MESSAGE_HEIGHT_COMPACT = MESSAGE_LINE_HEIGHT + MESSAGE_ROW_VERTICAL_PADDING;
|
||||
const COZY_LEAD_MESSAGE_HEIGHT = MESSAGE_LINE_HEIGHT * 2 + MESSAGE_ROW_VERTICAL_PADDING;
|
||||
const COZY_GROUPED_MESSAGE_HEIGHT = MESSAGE_LINE_HEIGHT + MESSAGE_ROW_VERTICAL_PADDING;
|
||||
const SKELETON_PADDING_TOP = pxFromRemLength('1rem');
|
||||
const SKELETON_PADDING_BOTTOM = pxFromRemLength('0.75rem');
|
||||
const COZY_SKELETON_VERTICAL_PADDING = SKELETON_PADDING_TOP + SKELETON_PADDING_BOTTOM;
|
||||
const ATTACHMENT_MARGIN = 8;
|
||||
const ATTACHMENT_WIDTH_MIN = 140;
|
||||
const ATTACHMENT_WIDTH_MAX = 400;
|
||||
const ATTACHMENT_HEIGHT_MIN = 100;
|
||||
const ATTACHMENT_HEIGHT_MAX = 250;
|
||||
const ATTACHMENT_HEIGHT_MAX = 320;
|
||||
const USERNAME_WIDTH_MIN = 48;
|
||||
const USERNAME_WIDTH_RANGE = 36;
|
||||
const TIMESTAMP_WIDTH_MIN = 8;
|
||||
const TIMESTAMP_WIDTH_RANGE = 12;
|
||||
const LINE_WIDTH_MIN = 75;
|
||||
const LINE_WIDTH_RANGE = 18;
|
||||
const MIN_MESSAGE_GROUPS = 8;
|
||||
const MAX_MESSAGE_GROUPS = 64;
|
||||
const MIN_MESSAGE_GROUPS = 10;
|
||||
const MAX_MESSAGE_GROUPS = 30;
|
||||
const GROUP_LINE_RANGE = 4;
|
||||
const ATTACHMENT_GROUPS = 8;
|
||||
const GROUP_COUNT_OVERSHOOT = 1.05;
|
||||
const COZY_GROUP_COUNT_FACTOR = 0.87;
|
||||
const MIN_ESTIMATED_VIEWPORT_HEIGHT = 400;
|
||||
const MEAN_GROUP_LINE_COUNT = (1 + GROUP_LINE_RANGE) / 2;
|
||||
const MESSAGE_LIST_FALLBACK_SEED_KEY = 'message-list-placeholder';
|
||||
|
||||
interface PlaceholderGenerationOptions {
|
||||
readonly compact: boolean;
|
||||
@@ -72,17 +82,22 @@ function randomInRange(random: () => number, min: number, max: number): number {
|
||||
return Math.floor(random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function resolveShortestGroupHeight(compact: boolean): number {
|
||||
export function resolveAverageGroupHeight(compact: boolean): number {
|
||||
if (compact) {
|
||||
return MESSAGE_HEIGHT_COMPACT;
|
||||
return MESSAGE_HEIGHT_COMPACT * MEAN_GROUP_LINE_COUNT;
|
||||
}
|
||||
return COZY_LEAD_MESSAGE_HEIGHT;
|
||||
return COZY_LEAD_MESSAGE_HEIGHT + COZY_GROUPED_MESSAGE_HEIGHT * (MEAN_GROUP_LINE_COUNT - 1);
|
||||
}
|
||||
|
||||
function resolvePlaceholderMessageGroups(compact: boolean, viewportHeightPx: number): number {
|
||||
const shortestGroupHeight = resolveShortestGroupHeight(compact);
|
||||
const groups = Math.ceil(Math.max(0, viewportHeightPx) / shortestGroupHeight);
|
||||
return Math.min(MAX_MESSAGE_GROUPS, Math.max(MIN_MESSAGE_GROUPS, groups));
|
||||
export function resolvePlaceholderMessageGroups(compact: boolean, viewportHeightPx: number): number {
|
||||
const usableHeight = Math.max(viewportHeightPx, MIN_ESTIMATED_VIEWPORT_HEIGHT);
|
||||
const rows = Math.ceil(usableHeight / resolveAverageGroupHeight(compact));
|
||||
const clamped = Math.min(MAX_MESSAGE_GROUPS, Math.max(MIN_MESSAGE_GROUPS, Math.ceil(GROUP_COUNT_OVERSHOOT * rows)));
|
||||
return compact ? clamped : Math.round(COZY_GROUP_COUNT_FACTOR * clamped);
|
||||
}
|
||||
|
||||
export function resolvePlaceholderAttachmentCount(messageGroups: number): number {
|
||||
return Math.max(1, Math.round((messageGroups / MAX_MESSAGE_GROUPS) * ATTACHMENT_GROUPS));
|
||||
}
|
||||
|
||||
function resolvePlaceholderDensityKey(compact: boolean): string {
|
||||
@@ -95,7 +110,7 @@ function resolvePlaceholderDensityKey(compact: boolean): string {
|
||||
function generatePlaceholderSpecs(options: PlaceholderGenerationOptions): PlaceholderSpecs {
|
||||
const {compact, compactAvatarsVisible, messageGroups, attachments, groupSpacing, random} = options;
|
||||
const groups: Array<MutablePlaceholderMessageGroup> = [];
|
||||
let totalHeight = compact ? 0 : COZY_SKELETON_VERTICAL_PADDING;
|
||||
let totalHeight = FILLER_WRAPPER_VERTICAL_PADDING;
|
||||
for (let index = 0; index < messageGroups; index++) {
|
||||
const lineCount = Math.floor(random() * GROUP_LINE_RANGE) + 1;
|
||||
const lineWidths: Array<number> = [];
|
||||
@@ -131,7 +146,7 @@ function generatePlaceholderSpecs(options: PlaceholderGenerationOptions): Placeh
|
||||
return {compact, compactAvatarsVisible, groups, totalHeight, groupSpacing};
|
||||
}
|
||||
|
||||
export interface PlaceholderSpecsOptions {
|
||||
interface PlaceholderSpecsOptions {
|
||||
readonly compact: boolean;
|
||||
readonly compactAvatarsVisible: boolean;
|
||||
readonly groupSpacing: number;
|
||||
@@ -139,7 +154,7 @@ export interface PlaceholderSpecsOptions {
|
||||
readonly seedKey: string;
|
||||
}
|
||||
|
||||
export function usePlaceholderSpecs({
|
||||
function usePlaceholderSpecs({
|
||||
compact,
|
||||
compactAvatarsVisible,
|
||||
groupSpacing,
|
||||
@@ -153,10 +168,35 @@ export function usePlaceholderSpecs({
|
||||
compact,
|
||||
compactAvatarsVisible,
|
||||
messageGroups,
|
||||
attachments: Math.min(ATTACHMENT_GROUPS, messageGroups),
|
||||
attachments: Math.min(messageGroups, resolvePlaceholderAttachmentCount(messageGroups)),
|
||||
groupSpacing,
|
||||
random: createSkeletonRandomFromKey([seedKey, resolvePlaceholderDensityKey(compact), groupSpacing].join('|')),
|
||||
}),
|
||||
[compact, compactAvatarsVisible, groupSpacing, messageGroups, seedKey],
|
||||
);
|
||||
}
|
||||
|
||||
export interface MessageListPlaceholderSpecsOptions {
|
||||
readonly channelId: string | null;
|
||||
readonly compact: boolean;
|
||||
readonly compactAvatarsVisible: boolean;
|
||||
readonly groupSpacing: number;
|
||||
}
|
||||
|
||||
export function useMessageListPlaceholderSpecs({
|
||||
channelId,
|
||||
compact,
|
||||
compactAvatarsVisible,
|
||||
groupSpacing,
|
||||
}: MessageListPlaceholderSpecsOptions): PlaceholderSpecs {
|
||||
const [viewportHeightPx] = useState(
|
||||
() => getRememberedSkeletonMessagePresentation()?.viewportHeightPx ?? resolveDefaultSkeletonChatViewportHeightPx(),
|
||||
);
|
||||
return usePlaceholderSpecs({
|
||||
compact,
|
||||
compactAvatarsVisible,
|
||||
groupSpacing,
|
||||
viewportHeightPx,
|
||||
seedKey: channelId ?? MESSAGE_LIST_FALLBACK_SEED_KEY,
|
||||
});
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export const ChatSkeletonChannelKind = SkeletonChannelProjectionKind;
|
||||
export type ChatSkeletonChannelKind = SkeletonChannelProjectionKind;
|
||||
|
||||
export interface ChatSkeletonPresentation {
|
||||
readonly channelId: string | null;
|
||||
readonly channelKind: ChatSkeletonChannelKind;
|
||||
readonly showMemberList: boolean;
|
||||
readonly showTopic: boolean;
|
||||
@@ -114,6 +115,7 @@ export function resolveChatSkeletonPresentation(pathname: string, canFitMemberLi
|
||||
headerTopicWidthPx = rememberedProjection.topicWidthPx;
|
||||
}
|
||||
return {
|
||||
channelId,
|
||||
channelKind,
|
||||
showMemberList: resolveMemberListVisibility(
|
||||
channelId,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-bottom: var(--message-filler-padding-bottom);
|
||||
margin-left: calc(-1 * var(--message-mobile-margin));
|
||||
margin-right: calc(-1 * var(--message-mobile-margin));
|
||||
padding-left: var(--chat-horizontal-padding, var(--chat-horizontal-padding-default));
|
||||
@@ -44,7 +47,6 @@
|
||||
.messageGroup {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.group {
|
||||
|
||||
@@ -52,10 +52,7 @@ function CozyScrollFillerSkeletonGroup({
|
||||
return (
|
||||
<flx-message-list-skeleton-group
|
||||
className={flxElementClassName(styles.messageGroup)}
|
||||
style={{
|
||||
marginBottom: resolveSkeletonGroupMarginBottom(groupIndex, groupCount, groupSpacing),
|
||||
containIntrinsicSize: `auto ${remFromPx(group.height)}`,
|
||||
}}
|
||||
style={{marginBottom: resolveSkeletonGroupMarginBottom(groupIndex, groupCount, groupSpacing)}}
|
||||
data-flx="app.skeleton.scroll-filler-skeleton.cozy-scroll-filler-skeleton-group.message-group"
|
||||
>
|
||||
<flx-message-list-skeleton-group-row
|
||||
@@ -173,10 +170,7 @@ function CompactScrollFillerSkeletonGroup({
|
||||
return (
|
||||
<flx-message-list-skeleton-group
|
||||
className={flxElementClassName(styles.messageGroup)}
|
||||
style={{
|
||||
marginBottom: resolveSkeletonGroupMarginBottom(groupIndex, groupCount, groupSpacing),
|
||||
containIntrinsicSize: `auto ${remFromPx(group.height)}`,
|
||||
}}
|
||||
style={{marginBottom: resolveSkeletonGroupMarginBottom(groupIndex, groupCount, groupSpacing)}}
|
||||
data-flx="app.skeleton.scroll-filler-skeleton.compact-scroll-filler-skeleton-group.message-group"
|
||||
>
|
||||
<flx-message-list-skeleton-compact-messages
|
||||
@@ -242,7 +236,7 @@ export const ScrollFillerSkeleton = ({
|
||||
}: PlaceholderSpecs) => (
|
||||
<flx-message-list-skeleton
|
||||
className={flxElementClassName(styles.wrapper, compact && styles.wrapperCompact)}
|
||||
style={{minHeight: remFromPx(totalHeight)}}
|
||||
style={{height: remFromPx(totalHeight)}}
|
||||
data-flx="app.skeleton.scroll-filler-skeleton.wrapper"
|
||||
>
|
||||
{groups.map((group, groupIndex) => {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import {usePlaceholderSpecs} from '@app/features/app/components/skeleton/PlaceholderSpecs';
|
||||
import {useMessageListPlaceholderSpecs} from '@app/features/app/components/skeleton/PlaceholderSpecs';
|
||||
import {ScrollFillerSkeleton} from '@app/features/app/components/skeleton/ScrollFillerSkeleton';
|
||||
import {
|
||||
reportSkeletonMessagePresentation,
|
||||
resolveDefaultSkeletonChatViewportHeightPx,
|
||||
} from '@app/features/app/components/skeleton/SkeletonLayoutMemory';
|
||||
import {reportSkeletonMessagePresentation} from '@app/features/app/components/skeleton/SkeletonLayoutMemory';
|
||||
import {measureSkeletonHeightPx, useSkeletonLayoutReport} from '@app/features/app/hooks/useSkeletonLayoutMemoryCapture';
|
||||
import {renderChannelStream} from '@app/features/channel/components/ChannelMessageStream';
|
||||
import styles from '@app/features/channel/components/ChannelMessages.module.css';
|
||||
@@ -176,12 +173,11 @@ export const Messages = observer(function Messages({
|
||||
const isModalOpen = Modal.hasModalOpen();
|
||||
const isGatewayConnected = GatewayConnection.isConnected;
|
||||
const selectedChannelId = SelectedChannel.currentChannelId;
|
||||
const placeholderSpecs = usePlaceholderSpecs({
|
||||
const placeholderSpecs = useMessageListPlaceholderSpecs({
|
||||
channelId: channel.id,
|
||||
compact: state.messageDisplayCompact,
|
||||
compactAvatarsVisible: Accessibility.showUserAvatarsInCompactMode,
|
||||
groupSpacing: state.messageGroupSpacing,
|
||||
viewportHeightPx: resolveDefaultSkeletonChatViewportHeightPx(),
|
||||
seedKey: channel.id,
|
||||
});
|
||||
const safeMessages = state.messages ?? MessagesState.getMessages(channel.id);
|
||||
const canAutoAck = shouldAutoAck({
|
||||
|
||||
@@ -309,6 +309,7 @@ export type ThemeVariableName =
|
||||
| "--message-failed-indicator-font-size"
|
||||
| "--message-failed-indicator-gap"
|
||||
| "--message-failed-opacity"
|
||||
| "--message-filler-padding-bottom"
|
||||
| "--message-group-spacing"
|
||||
| "--message-gutter"
|
||||
| "--message-highlight-bar-width"
|
||||
@@ -768,6 +769,7 @@ export const THEME_VARIABLES: ReadonlyArray<ThemeVariableDefinition> = [
|
||||
{name: "--message-failed-indicator-font-size", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "message-layout"},
|
||||
{name: "--message-failed-indicator-gap", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "message-layout"},
|
||||
{name: "--message-failed-opacity", kind: "number", groupId: "messages", groupLabel: "Messages", source: "features/theme/styles/Message.module.css"},
|
||||
{name: "--message-filler-padding-bottom", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "globals"},
|
||||
{name: "--message-group-spacing", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "runtime-accessibility"},
|
||||
{name: "--message-gutter", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "message-layout"},
|
||||
{name: "--message-highlight-bar-width", kind: "dimension", groupId: "messages", groupLabel: "Messages", source: "features/theme/styles/Message.module.css"},
|
||||
@@ -1228,6 +1230,7 @@ export const THEME_VARIABLE_NAMES: ReadonlyArray<string> = [
|
||||
"--message-failed-indicator-font-size",
|
||||
"--message-failed-indicator-gap",
|
||||
"--message-failed-opacity",
|
||||
"--message-filler-padding-bottom",
|
||||
"--message-group-spacing",
|
||||
"--message-gutter",
|
||||
"--message-highlight-bar-width",
|
||||
@@ -1879,6 +1882,7 @@ export const THEME_STUDIO_DARK_DEFAULT_VARIABLE_VALUES: Readonly<Record<string,
|
||||
"--message-failed-indicator-font-size": "0.75rem",
|
||||
"--message-failed-indicator-gap": "0.375rem",
|
||||
"--message-failed-opacity": "0.5",
|
||||
"--message-filler-padding-bottom": "0.75rem",
|
||||
"--message-group-spacing": "1rem",
|
||||
"--message-gutter": "0.75rem",
|
||||
"--message-highlight-bar-width": "0.125rem",
|
||||
@@ -2339,6 +2343,7 @@ export const THEME_STUDIO_LIGHT_DEFAULT_VARIABLE_VALUES: Readonly<Record<string,
|
||||
"--message-failed-indicator-font-size": "0.75rem",
|
||||
"--message-failed-indicator-gap": "0.375rem",
|
||||
"--message-failed-opacity": "0.5",
|
||||
"--message-filler-padding-bottom": "0.75rem",
|
||||
"--message-group-spacing": "1rem",
|
||||
"--message-gutter": "0.75rem",
|
||||
"--message-highlight-bar-width": "0.125rem",
|
||||
|
||||
Reference in new issue
Block a user