Merge branch 'thread' into 'develop'

Add sticky column header, improve design of threads

See merge request soapbox-pub/soapbox!2423
This commit is contained in:
Alex Gleason
2023-04-10 15:10:08 +00:00
8 changed files with 52 additions and 21 deletions

View File

@ -16,7 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Posts: truncate Nostr pubkeys in reply mentions.
- Posts: upgraded emoji picker component.
- Posts: improved design of threads.
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
- UI: added sticky column header.
### Fixed
- Posts: fixed emojis being cut off in reactions modal.

View File

@ -16,11 +16,13 @@ const messages = defineMessages({
back: { id: 'card.back.label', defaultMessage: 'Back' },
});
export type CardSizes = keyof typeof sizes
interface ICard {
/** The type of card. */
variant?: 'default' | 'rounded'
/** Card size preset. */
size?: keyof typeof sizes
size?: CardSizes
/** Extra classnames for the <div> element. */
className?: string
/** Elements inside the card. */
@ -33,7 +35,7 @@ const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'def
ref={ref}
{...filteredProps}
className={clsx({
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none': variant === 'rounded',
[sizes[size]]: variant === 'rounded',
}, className)}
>
@ -72,7 +74,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
};
return (
<HStack alignItems='center' space={2} className={clsx('mb-4', className)}>
<HStack alignItems='center' space={2} className={className}>
{renderBackButton()}
{children}

View File

@ -1,11 +1,12 @@
import clsx from 'clsx';
import React from 'react';
import throttle from 'lodash/throttle';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import Helmet from 'soapbox/components/helmet';
import { useSoapboxConfig } from 'soapbox/hooks';
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
import { Card, CardBody, CardHeader, CardTitle, type CardSizes } from '../card/card';
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' | 'className' | 'action'>;
@ -54,13 +55,29 @@ export interface IColumn {
ref?: React.Ref<HTMLDivElement>
/** Children to display in the column. */
children?: React.ReactNode
/** Action for the ColumnHeader, displayed at the end. */
action?: React.ReactNode
/** Column size, inherited from Card. */
size?: CardSizes
}
/** A backdrop for the main section of the UI. */
const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element => {
const { backHref, children, label, transparent = false, withHeader = true, className, action } = props;
const { backHref, children, label, transparent = false, withHeader = true, className, action, size } = props;
const soapboxConfig = useSoapboxConfig();
const [isScrolled, setIsScrolled] = useState(false);
const handleScroll = useCallback(throttle(() => {
setIsScrolled(window.pageYOffset > 32);
}, 50), []);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div role='region' className='relative' ref={ref} aria-label={label} column-type={transparent ? 'transparent' : 'filled'}>
@ -76,12 +93,18 @@ const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedR
)}
</Helmet>
<Card variant={transparent ? undefined : 'rounded'} className={className}>
<Card size={size} variant={transparent ? undefined : 'rounded'} className={className}>
{withHeader && (
<ColumnHeader
label={label}
backHref={backHref}
className={clsx({ 'px-4 pt-4 sm:p-0': transparent })}
className={clsx({
'rounded-t-3xl': !isScrolled && !transparent,
'sticky top-12 z-10 bg-white/90 dark:bg-primary-900/90 backdrop-blur lg:top-16': !transparent,
'p-4 sm:p-0 sm:pb-4': transparent,
'-mt-4 -mx-4 p-4': size !== 'lg' && !transparent,
'-mt-4 -mx-4 p-4 sm:-mt-6 sm:-mx-6 sm:p-6': size === 'lg' && !transparent,
})}
action={action}
/>
)}

View File

@ -50,8 +50,9 @@ import type {
} from 'soapbox/types/entities';
const messages = defineMessages({
title: { id: 'status.title', defaultMessage: '@{username}\'s Post' },
title: { id: 'status.title', defaultMessage: 'Post Details' },
titleDirect: { id: 'status.title_direct', defaultMessage: 'Direct message' },
titleGroup: { id: 'status.title_group', defaultMessage: 'Group Post Details' },
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' },
@ -462,9 +463,6 @@ const Thread: React.FC<IThread> = (props) => {
react: handleHotkeyReact,
};
const username = String(status.getIn(['account', 'acct']));
const titleMessage = status.visibility === 'direct' ? messages.titleDirect : messages.title;
const focusedStatus = (
<div className={clsx({ 'pb-4': hasDescendants })} key={status.id}>
<HotKeys handlers={handlers}>
@ -488,7 +486,7 @@ const Thread: React.FC<IThread> = (props) => {
{!isUnderReview ? (
<>
<hr className='mb-2 border-t-2 dark:border-primary-800' />
<hr className='-mx-4 mb-2 max-w-[100vw] border-t-2 dark:border-primary-800' />
<StatusActionBar
status={status}
@ -502,7 +500,7 @@ const Thread: React.FC<IThread> = (props) => {
</HotKeys>
{hasDescendants && (
<hr className='mt-2 border-t-2 dark:border-primary-800' />
<hr className='-mx-4 mt-2 max-w-[100vw] border-t-2 dark:border-primary-800' />
)}
</div>
);
@ -523,10 +521,15 @@ const Thread: React.FC<IThread> = (props) => {
return <Redirect to={`/groups/${status.group.id}/posts/${props.params.statusId}`} />;
}
const titleMessage = () => {
if (status.visibility === 'direct') return messages.titleDirect;
return status.group ? messages.titleGroup : messages.title;
};
return (
<Column label={intl.formatMessage(titleMessage, { username })} transparent>
<Column label={intl.formatMessage(titleMessage())}>
<PullToRefresh onRefresh={handleRefresh}>
<Stack space={2}>
<Stack space={2} className='mt-2'>
<div ref={node} className='thread'>
<ScrollableList
id='thread'

View File

@ -141,7 +141,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
<Stack space={2}>
<Stack>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
@ -153,7 +153,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
</HStack>
<HStack alignItems='center' space={0.5}>
<Text size='sm' theme='muted' direction='ltr'>
<Text size='sm' theme='muted' direction='ltr' truncate>
@{displayFqn ? account.fqn : account.acct}
</Text>

View File

@ -1462,8 +1462,9 @@
"status.show_less_all": "Show less for all",
"status.show_more_all": "Show more for all",
"status.show_original": "Show original",
"status.title": "@{username}'s post",
"status.title": "Post Details",
"status.title_direct": "Direct message",
"status.title_group": "Group Post Details",
"status.translate": "Translate",
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.unbookmark": "Remove bookmark",

View File

@ -1,5 +1,5 @@
.thread {
@apply bg-white dark:bg-primary-900 p-4 shadow-xl dark:shadow-none sm:p-6 sm:rounded-xl;
@apply bg-white dark:bg-primary-900 sm:rounded-xl;
&__status {
@apply relative pb-4;

View File

@ -17,7 +17,7 @@
[column-type='filled'] .status__wrapper,
[column-type='filled'] .status-placeholder {
@apply bg-transparent dark:bg-transparent rounded-none shadow-none p-4;
@apply bg-transparent dark:bg-transparent rounded-none shadow-none;
}
.status-check-box {