kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'black' into 'main'
Redesign desktop layout See merge request soapbox-pub/soapbox!3312merge-requests/3313/head
commit
31c461b43f
|
@ -22,7 +22,7 @@ const AnnouncementsPanel = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
|
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
|
||||||
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md' variant='rounded'>
|
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md'>
|
||||||
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
|
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
|
||||||
{announcements!.map((announcement) => (
|
{announcements!.map((announcement) => (
|
||||||
<Announcement
|
<Announcement
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface IBigCard {
|
||||||
|
|
||||||
const BigCard: React.FC<IBigCard> = ({ title, subtitle, children, onClose }) => {
|
const BigCard: React.FC<IBigCard> = ({ title, subtitle, children, onClose }) => {
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='xl'>
|
<Card size='xl' rounded>
|
||||||
<CardBody className='relative'>
|
<CardBody className='relative'>
|
||||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
/** Fullscreen gradient used as a backdrop to public pages. */
|
|
||||||
const LandingGradient: React.FC = () => (
|
|
||||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-gradient-end/10 black:hidden dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
|
|
||||||
);
|
|
||||||
|
|
||||||
export default LandingGradient;
|
|
|
@ -1,12 +1,9 @@
|
||||||
import LandingGradient from 'soapbox/components/landing-gradient.tsx';
|
|
||||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
|
|
||||||
/** Fullscreen loading indicator. */
|
/** Fullscreen loading indicator. */
|
||||||
const LoadingScreen: React.FC = () => {
|
const LoadingScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='fixed h-screen w-screen'>
|
<div className='fixed h-screen w-screen'>
|
||||||
<LandingGradient />
|
|
||||||
|
|
||||||
<div className='d-screen fixed z-10 flex w-screen items-center justify-center'>
|
<div className='d-screen fixed z-10 flex w-screen items-center justify-center'>
|
||||||
<div className='p-4'>
|
<div className='p-4'>
|
||||||
<Spinner size={40} withText={false} />
|
<Spinner size={40} withText={false} />
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface MissingIndicatorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MissingIndicator = ({ nested = false }: MissingIndicatorProps): JSX.Element => (
|
const MissingIndicator = ({ nested = false }: MissingIndicatorProps): JSX.Element => (
|
||||||
<Card variant={nested ? undefined : 'rounded'} size='lg'>
|
<Card rounded={!nested} size='lg'>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Text weight='medium' align='center' size='lg'>
|
<Text weight='medium' align='center' size='lg'>
|
||||||
|
|
|
@ -3,8 +3,7 @@ import calendarIcon from '@tabler/icons/outline/calendar.svg';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useIntl, FormattedMessage } from 'react-intl';
|
import { useIntl, FormattedMessage } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
import { fetchRelationships } from 'soapbox/actions/accounts.ts';
|
import { fetchRelationships } from 'soapbox/actions/accounts.ts';
|
||||||
import {
|
import {
|
||||||
|
@ -14,15 +13,20 @@ import {
|
||||||
import { useAccount, usePatronUser } from 'soapbox/api/hooks/index.ts';
|
import { useAccount, usePatronUser } from 'soapbox/api/hooks/index.ts';
|
||||||
import Badge from 'soapbox/components/badge.tsx';
|
import Badge from 'soapbox/components/badge.tsx';
|
||||||
import Markup from 'soapbox/components/markup.tsx';
|
import Markup from 'soapbox/components/markup.tsx';
|
||||||
|
import StillImage from 'soapbox/components/still-image.tsx';
|
||||||
|
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||||
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import Icon from 'soapbox/components/ui/icon.tsx';
|
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification-badge.tsx';
|
||||||
import ActionButton from 'soapbox/features/ui/components/action-button.tsx';
|
import ActionButton from 'soapbox/features/ui/components/action-button.tsx';
|
||||||
import { UserPanel } from 'soapbox/features/ui/util/async-components.ts';
|
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
||||||
|
import { shortNumberFormat } from 'soapbox/utils/numbers.tsx';
|
||||||
|
|
||||||
import { showProfileHoverCard } from './hover-ref-wrapper.tsx';
|
import { showProfileHoverCard } from './hover-ref-wrapper.tsx';
|
||||||
import { dateFormatOptions } from './relative-timestamp.tsx';
|
import { dateFormatOptions } from './relative-timestamp.tsx';
|
||||||
|
@ -77,6 +81,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||||
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined);
|
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined);
|
||||||
const { account } = useAccount(accountId, { withRelationship: true });
|
const { account } = useAccount(accountId, { withRelationship: true });
|
||||||
const { patronUser } = usePatronUser(account?.url);
|
const { patronUser } = usePatronUser(account?.url);
|
||||||
|
const { displayFqn } = useSoapboxConfig();
|
||||||
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
|
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
|
||||||
const badges = getBadges(account, patronUser);
|
const badges = getBadges(account, patronUser);
|
||||||
|
|
||||||
|
@ -118,14 +123,75 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||||
onMouseEnter={handleMouseEnter(dispatch)}
|
onMouseEnter={handleMouseEnter(dispatch)}
|
||||||
onMouseLeave={handleMouseLeave(dispatch)}
|
onMouseLeave={handleMouseLeave(dispatch)}
|
||||||
>
|
>
|
||||||
<Card variant='rounded' className='relative isolate overflow-hidden'>
|
<Card className='relative isolate overflow-hidden' rounded slim>
|
||||||
<CardBody>
|
<CardBody className='relative'>
|
||||||
<Stack space={2}>
|
<div className='relative h-24 overflow-hidden bg-gray-200'>
|
||||||
<UserPanel
|
<StillImage src={account.header} />
|
||||||
accountId={account.id}
|
</div>
|
||||||
action={<ActionButton account={account} small />}
|
|
||||||
badges={badges}
|
<Stack space={2} className='-mt-12 px-3 pb-3'>
|
||||||
/>
|
<HStack justifyContent='between'>
|
||||||
|
<Link to={`/@${account.acct}`} title={account.acct}>
|
||||||
|
<Avatar src={account.avatar} size={80} className='size-20 overflow-hidden bg-gray-50 ring-2 ring-white' />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className='mt-2'>
|
||||||
|
<ActionButton account={account} small />
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<Link to={`/@${account.acct}`}>
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
<Text size='lg' weight='bold' truncate>
|
||||||
|
{emojifyText(account.display_name, account.emojis)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{account.verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
{badges && badges.length > 0 && (
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
{badges}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<Text size='sm' theme='muted' direction='ltr' truncate>
|
||||||
|
@{displayFqn ? account.fqn : account.acct}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<HStack alignItems='center' space={3}>
|
||||||
|
{account.followers_count >= 0 && (
|
||||||
|
<Link to={`/@${account.acct}/followers`} title={intl.formatNumber(account.followers_count)}>
|
||||||
|
<HStack alignItems='center' space={1}>
|
||||||
|
<Text theme='primary' weight='bold' size='sm'>
|
||||||
|
{shortNumberFormat(account.followers_count)}
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='sm'>
|
||||||
|
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{account.following_count >= 0 && (
|
||||||
|
<Link to={`/@${account.acct}/following`} title={intl.formatNumber(account.following_count)}>
|
||||||
|
<HStack alignItems='center' space={1}>
|
||||||
|
<Text theme='primary' weight='bold' size='sm'>
|
||||||
|
{shortNumberFormat(account.following_count)}
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='sm'>
|
||||||
|
<FormattedMessage id='account.follows' defaultMessage='Following' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
{account.local ? (
|
{account.local ? (
|
||||||
<HStack alignItems='center' space={0.5}>
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
|
|
@ -36,8 +36,6 @@ interface IPureStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children
|
||||||
emptyMessage: React.ReactNode;
|
emptyMessage: React.ReactNode;
|
||||||
/** ID of the timeline in Redux. */
|
/** ID of the timeline in Redux. */
|
||||||
timelineId?: string;
|
timelineId?: string;
|
||||||
/** Whether to display a gap or border between statuses in the list. */
|
|
||||||
divideType?: 'space' | 'border';
|
|
||||||
/** Whether to display ads. */
|
/** Whether to display ads. */
|
||||||
showAds?: boolean;
|
showAds?: boolean;
|
||||||
/** Whether to show group information. */
|
/** Whether to show group information. */
|
||||||
|
@ -51,7 +49,6 @@ const PureStatusList: React.FC<IPureStatusList> = ({
|
||||||
statuses,
|
statuses,
|
||||||
lastStatusId,
|
lastStatusId,
|
||||||
featuredStatuses,
|
featuredStatuses,
|
||||||
divideType = 'border',
|
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
timelineId,
|
timelineId,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -133,7 +130,6 @@ const PureStatusList: React.FC<IPureStatusList> = ({
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
showGroup={showGroup}
|
showGroup={showGroup}
|
||||||
variant={divideType === 'border' ? 'slim' : 'rounded'}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -161,7 +157,6 @@ const PureStatusList: React.FC<IPureStatusList> = ({
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
showGroup={showGroup}
|
showGroup={showGroup}
|
||||||
variant={divideType === 'border' ? 'slim' : 'default'} // shouldn't "default" be changed to "rounded" ?
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
@ -236,15 +231,10 @@ const PureStatusList: React.FC<IPureStatusList> = ({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={isLoading && statuses.length === 0}
|
showLoading={isLoading && statuses.length === 0}
|
||||||
onLoadMore={handleLoadOlder}
|
onLoadMore={handleLoadOlder}
|
||||||
placeholderComponent={() => <PlaceholderStatus variant={divideType === 'border' ? 'slim' : 'rounded'} />}
|
placeholderComponent={() => <PlaceholderStatus />}
|
||||||
placeholderCount={20}
|
placeholderCount={20}
|
||||||
ref={node}
|
ref={node}
|
||||||
listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
|
listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', className)}
|
||||||
'divide-none': divideType !== 'border',
|
|
||||||
}, className)}
|
|
||||||
itemClassName={clsx({
|
|
||||||
'pb-3': divideType !== 'border',
|
|
||||||
})}
|
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
{renderScrollableContent()}
|
{renderScrollableContent()}
|
||||||
|
|
|
@ -57,7 +57,6 @@ export interface IPureStatus {
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
hideActionBar?: boolean;
|
hideActionBar?: boolean;
|
||||||
hoverable?: boolean;
|
hoverable?: boolean;
|
||||||
variant?: 'default' | 'rounded' | 'slim';
|
|
||||||
showGroup?: boolean;
|
showGroup?: boolean;
|
||||||
accountAction?: React.ReactElement;
|
accountAction?: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +79,6 @@ const PureStatus: React.FC<IPureStatus> = (props) => {
|
||||||
featured,
|
featured,
|
||||||
unread,
|
unread,
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
variant = 'rounded',
|
|
||||||
showGroup = true,
|
showGroup = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -420,9 +418,9 @@ const PureStatus: React.FC<IPureStatus> = (props) => {
|
||||||
role='link'
|
role='link'
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
variant={variant}
|
|
||||||
className={clsx('status--wrapper space-y-4', {
|
className={clsx('status--wrapper space-y-4', {
|
||||||
'py-6 sm:p-5': variant === 'rounded', muted, read: unread === false,
|
muted,
|
||||||
|
read: unread === false,
|
||||||
})}
|
})}
|
||||||
data-id={status.id}
|
data-id={status.id}
|
||||||
>
|
>
|
||||||
|
|
|
@ -87,7 +87,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed left-1/2 top-20 z-50 -translate-x-1/2'>
|
<div className='fixed left-1/2 top-28 z-50 -translate-x-1/2'>
|
||||||
<button
|
<button
|
||||||
className='flex cursor-pointer items-center space-x-1.5 whitespace-nowrap rounded-full bg-primary-600 px-4 py-2 text-white transition-transform hover:scale-105 hover:bg-primary-700 active:scale-100'
|
className='flex cursor-pointer items-center space-x-1.5 whitespace-nowrap rounded-full bg-primary-600 px-4 py-2 text-white transition-transform hover:scale-105 hover:bg-primary-700 active:scale-100'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|
|
@ -169,7 +169,7 @@ const ScrollableList = forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{emptyMessageCard ? (
|
{emptyMessageCard ? (
|
||||||
<Card variant='rounded' size='lg'>
|
<Card size='lg'>
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</Card>
|
</Card>
|
||||||
) : emptyMessage}
|
) : emptyMessage}
|
||||||
|
|
|
@ -381,7 +381,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{switcher && (
|
{switcher && (
|
||||||
<div className='border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800'>
|
<div className='border-t border-solid border-gray-100 dark:border-gray-800'>
|
||||||
{otherAccounts.map(account => renderAccount(account))}
|
{otherAccounts.map(account => renderAccount(account))}
|
||||||
|
|
||||||
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
||||||
|
|
|
@ -44,8 +44,9 @@ const SidebarNavigationLink = forwardRef((props: ISidebarNavigationLink, ref: Re
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rtl:space-x-reverse rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true,
|
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rtl:space-x-reverse rounded-full group hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-primary-800': true,
|
||||||
'dark:text-gray-100 text-gray-900': isActive,
|
'text-gray-600 dark:text-gray-500': !isActive,
|
||||||
|
'text-gray-900 dark:text-gray-50': isActive,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className='relative'>
|
<span className='relative'>
|
||||||
|
@ -54,8 +55,8 @@ const SidebarNavigationLink = forwardRef((props: ISidebarNavigationLink, ref: Re
|
||||||
count={count}
|
count={count}
|
||||||
countMax={countMax}
|
countMax={countMax}
|
||||||
className={clsx('size-5', {
|
className={clsx('size-5', {
|
||||||
'text-gray-600 black:text-white dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
|
'text-gray-600 black:text-white dark:text-gray-500 group-hover:text-gray-900 dark:group-hover:text-gray-50': !isActive,
|
||||||
'text-primary-500 dark:text-primary-400': isActive,
|
'text-gray-900 dark:text-gray-50': isActive,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -21,10 +21,15 @@ import userPlusIcon from '@tabler/icons/outline/user-plus.svg';
|
||||||
import userIcon from '@tabler/icons/outline/user.svg';
|
import userIcon from '@tabler/icons/outline/user.svg';
|
||||||
import worldIcon from '@tabler/icons/outline/world.svg';
|
import worldIcon from '@tabler/icons/outline/world.svg';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Account from 'soapbox/components/account.tsx';
|
||||||
|
import SiteLogo from 'soapbox/components/site-logo.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context.tsx';
|
import { useStatContext } from 'soapbox/contexts/stat-context.tsx';
|
||||||
|
import Search from 'soapbox/features/compose/components/search.tsx';
|
||||||
import ComposeButton from 'soapbox/features/ui/components/compose-button.tsx';
|
import ComposeButton from 'soapbox/features/ui/components/compose-button.tsx';
|
||||||
|
import ProfileDropdown from 'soapbox/features/ui/components/profile-dropdown.tsx';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
|
@ -139,100 +144,119 @@ const SidebarNavigation = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={4}>
|
<Stack justifyContent='between' className='min-h-screen py-6'>
|
||||||
<Stack space={2}>
|
<Stack space={6}>
|
||||||
<SidebarNavigationLink
|
<Link key='logo' to='/' data-preview-title-id='column.home' className='ml-4 flex shrink-0 items-center'>
|
||||||
to='/'
|
<SiteLogo alt='Logo' className='h-10 w-auto cursor-pointer' />
|
||||||
icon={homeIcon}
|
<span className='hidden'><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span>
|
||||||
activeIcon={homeFilledIcon}
|
</Link>
|
||||||
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarNavigationLink
|
<Search openInRoute autosuggest />
|
||||||
to='/search'
|
|
||||||
icon={searchIcon}
|
<Stack space={2}>
|
||||||
text={<FormattedMessage id='tabs_bar.search' defaultMessage='Discover' />}
|
<SidebarNavigationLink
|
||||||
/>
|
to='/'
|
||||||
|
icon={homeIcon}
|
||||||
|
activeIcon={homeFilledIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/search'
|
||||||
|
icon={searchIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.search' defaultMessage='Discover' />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{account && (
|
||||||
|
<>
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/notifications'
|
||||||
|
icon={bellIcon}
|
||||||
|
activeIcon={bellFilledIcon}
|
||||||
|
count={notificationCount}
|
||||||
|
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{renderMessagesLink()}
|
||||||
|
|
||||||
|
{features.groups && (
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/groups'
|
||||||
|
icon={circlesIcon}
|
||||||
|
activeIcon={circlesFilledIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
icon={userIcon}
|
||||||
|
activeIcon={userFilledIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/settings'
|
||||||
|
icon={settingsIcon}
|
||||||
|
activeIcon={settingsFilledIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
|
||||||
|
count={settingsNotifications.size}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{account.staff && (
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/soapbox/admin'
|
||||||
|
icon={dashboardIcon}
|
||||||
|
count={dashboardCount}
|
||||||
|
text={<FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(features.publicTimeline) && (
|
||||||
|
<>
|
||||||
|
{(account || !restrictUnauth.timelines.local) && (
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/timeline/local'
|
||||||
|
icon={features.federating ? atIcon : worldIcon}
|
||||||
|
text={features.federating ? instance.domain : <FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(features.federating && (account || !restrictUnauth.timelines.federated)) && (
|
||||||
|
<SidebarNavigationLink
|
||||||
|
to='/timeline/global'
|
||||||
|
icon={worldIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{menu.length > 0 && (
|
||||||
|
<DropdownMenu items={menu} placement='top'>
|
||||||
|
<SidebarNavigationLink
|
||||||
|
icon={dotsCircleHorizontalIcon}
|
||||||
|
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
|
||||||
|
/>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{account && (
|
{account && (
|
||||||
<>
|
<ComposeButton />
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/notifications'
|
|
||||||
icon={bellIcon}
|
|
||||||
activeIcon={bellFilledIcon}
|
|
||||||
count={notificationCount}
|
|
||||||
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{renderMessagesLink()}
|
|
||||||
|
|
||||||
{features.groups && (
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/groups'
|
|
||||||
icon={circlesIcon}
|
|
||||||
activeIcon={circlesFilledIcon}
|
|
||||||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to={`/@${account.acct}`}
|
|
||||||
icon={userIcon}
|
|
||||||
activeIcon={userFilledIcon}
|
|
||||||
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/settings'
|
|
||||||
icon={settingsIcon}
|
|
||||||
activeIcon={settingsFilledIcon}
|
|
||||||
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
|
|
||||||
count={settingsNotifications.size}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{account.staff && (
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/soapbox/admin'
|
|
||||||
icon={dashboardIcon}
|
|
||||||
count={dashboardCount}
|
|
||||||
text={<FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(features.publicTimeline) && (
|
|
||||||
<>
|
|
||||||
{(account || !restrictUnauth.timelines.local) && (
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/timeline/local'
|
|
||||||
icon={features.federating ? atIcon : worldIcon}
|
|
||||||
text={features.federating ? instance.domain : <FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(features.federating && (account || !restrictUnauth.timelines.federated)) && (
|
|
||||||
<SidebarNavigationLink
|
|
||||||
to='/timeline/global'
|
|
||||||
icon={worldIcon}
|
|
||||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{menu.length > 0 && (
|
|
||||||
<DropdownMenu items={menu} placement='top'>
|
|
||||||
<SidebarNavigationLink
|
|
||||||
icon={dotsCircleHorizontalIcon}
|
|
||||||
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
|
|
||||||
/>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{account && (
|
{account && (
|
||||||
<ComposeButton />
|
<div className='mt-12'>
|
||||||
|
<ProfileDropdown account={account} placement='top'>
|
||||||
|
<div className='w-full p-2'>
|
||||||
|
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
|
||||||
|
</div>
|
||||||
|
</ProfileDropdown>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
updateStatusHoverCard,
|
updateStatusHoverCard,
|
||||||
} from 'soapbox/actions/status-hover-card.ts';
|
} from 'soapbox/actions/status-hover-card.ts';
|
||||||
import { fetchStatus } from 'soapbox/actions/statuses.ts';
|
import { fetchStatus } from 'soapbox/actions/statuses.ts';
|
||||||
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
|
||||||
import StatusContainer from 'soapbox/containers/status-container.tsx';
|
import StatusContainer from 'soapbox/containers/status-container.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
|
@ -67,20 +66,7 @@ export const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true })
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!statusId) return null;
|
if (!status) return null;
|
||||||
|
|
||||||
const renderStatus = (statusId: string) => {
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<StatusContainer
|
|
||||||
key={statusId}
|
|
||||||
id={statusId}
|
|
||||||
hoverable={false}
|
|
||||||
hideActionBar
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -94,11 +80,9 @@ export const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true })
|
||||||
onMouseEnter={handleMouseEnter()}
|
onMouseEnter={handleMouseEnter()}
|
||||||
onMouseLeave={handleMouseLeave()}
|
onMouseLeave={handleMouseLeave()}
|
||||||
>
|
>
|
||||||
<Card className='relative'>
|
<div className='overflow-hidden rounded-xl bg-white p-4 black:bg-black dark:bg-primary-900'>
|
||||||
<CardBody>
|
<StatusContainer id={status.id} hoverable={false} hideActionBar muted slim />
|
||||||
{renderStatus(statusId)}
|
</div>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,8 +36,6 @@ interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
emptyMessage: React.ReactNode;
|
emptyMessage: React.ReactNode;
|
||||||
/** ID of the timeline in Redux. */
|
/** ID of the timeline in Redux. */
|
||||||
timelineId?: string;
|
timelineId?: string;
|
||||||
/** Whether to display a gap or border between statuses in the list. */
|
|
||||||
divideType?: 'space' | 'border';
|
|
||||||
/** Whether to display ads. */
|
/** Whether to display ads. */
|
||||||
showAds?: boolean;
|
showAds?: boolean;
|
||||||
/** Whether to show group information. */
|
/** Whether to show group information. */
|
||||||
|
@ -52,7 +50,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
statusIds,
|
statusIds,
|
||||||
lastStatusId,
|
lastStatusId,
|
||||||
featuredStatusIds,
|
featuredStatusIds,
|
||||||
divideType = 'border',
|
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
timelineId,
|
timelineId,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -131,7 +128,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
showGroup={showGroup}
|
showGroup={showGroup}
|
||||||
variant={divideType === 'border' ? 'slim' : 'rounded'}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -159,7 +155,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
showGroup={showGroup}
|
showGroup={showGroup}
|
||||||
variant={divideType === 'border' ? 'slim' : 'default'}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
@ -234,15 +229,10 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={isLoading && statusIds.size === 0}
|
showLoading={isLoading && statusIds.size === 0}
|
||||||
onLoadMore={handleLoadOlder}
|
onLoadMore={handleLoadOlder}
|
||||||
placeholderComponent={() => <PlaceholderStatus variant={divideType === 'border' ? 'slim' : 'rounded'} />}
|
placeholderComponent={() => <PlaceholderStatus />}
|
||||||
placeholderCount={20}
|
placeholderCount={20}
|
||||||
ref={node}
|
ref={node}
|
||||||
listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
|
listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', className)}
|
||||||
'divide-none': divideType !== 'border',
|
|
||||||
}, className)}
|
|
||||||
itemClassName={clsx({
|
|
||||||
'pb-3': divideType !== 'border',
|
|
||||||
})}
|
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
{renderScrollableContent()}
|
{renderScrollableContent()}
|
||||||
|
|
|
@ -57,9 +57,9 @@ export interface IStatus {
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
hideActionBar?: boolean;
|
hideActionBar?: boolean;
|
||||||
hoverable?: boolean;
|
hoverable?: boolean;
|
||||||
variant?: 'default' | 'rounded' | 'slim';
|
|
||||||
showGroup?: boolean;
|
showGroup?: boolean;
|
||||||
accountAction?: React.ReactElement;
|
accountAction?: React.ReactElement;
|
||||||
|
slim?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,8 +81,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
featured,
|
featured,
|
||||||
unread,
|
unread,
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
variant = 'rounded',
|
|
||||||
showGroup = true,
|
showGroup = true,
|
||||||
|
slim,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -417,11 +417,12 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
role='link'
|
role='link'
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
variant={variant}
|
|
||||||
className={clsx('status--wrapper space-y-4', {
|
className={clsx('status--wrapper space-y-4', {
|
||||||
'py-6 sm:p-5': variant === 'rounded', muted, read: unread === false,
|
muted,
|
||||||
|
read: unread === false,
|
||||||
})}
|
})}
|
||||||
data-id={status.id}
|
data-id={status.id}
|
||||||
|
slim={slim}
|
||||||
>
|
>
|
||||||
{renderStatusInfo()}
|
{renderStatusInfo()}
|
||||||
|
|
||||||
|
|
|
@ -9,23 +9,16 @@ import SvgIcon from 'soapbox/components/ui/svg-icon.tsx';
|
||||||
import HStack from './hstack.tsx';
|
import HStack from './hstack.tsx';
|
||||||
import Text from './text.tsx';
|
import Text from './text.tsx';
|
||||||
|
|
||||||
const sizes = {
|
|
||||||
md: 'p-4 sm:rounded-xl',
|
|
||||||
lg: 'p-4 sm:p-6 sm:rounded-xl',
|
|
||||||
xl: 'p-4 sm:p-10 sm:rounded-3xl',
|
|
||||||
};
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
back: { id: 'card.back.label', defaultMessage: 'Back' },
|
back: { id: 'card.back.label', defaultMessage: 'Back' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CardSizes = keyof typeof sizes
|
|
||||||
|
|
||||||
interface ICard {
|
interface ICard {
|
||||||
/** The type of card. */
|
rounded?: boolean;
|
||||||
variant?: 'default' | 'rounded' | 'slim';
|
transparent?: boolean;
|
||||||
|
slim?: boolean;
|
||||||
/** Card size preset. */
|
/** Card size preset. */
|
||||||
size?: CardSizes;
|
size?: 'md' | 'lg' | 'xl';
|
||||||
/** Extra classnames for the <div> element. */
|
/** Extra classnames for the <div> element. */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Elements inside the card. */
|
/** Elements inside the card. */
|
||||||
|
@ -34,15 +27,16 @@ interface ICard {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An opaque backdrop to hold a collection of related elements. */
|
/** An opaque backdrop to hold a collection of related elements. */
|
||||||
const Card = forwardRef<HTMLDivElement, ICard>(({ children, variant = 'default', size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
const Card = forwardRef<HTMLDivElement, ICard>(({ children, rounded, transparent, slim, size = 'md', className, ...filteredProps }, ref): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
'bg-white dark:bg-primary-900 black:bg-black text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none': variant === 'rounded',
|
'bg-white dark:bg-primary-900 black:bg-black': !transparent,
|
||||||
[sizes[size]]: variant === 'rounded',
|
'overflow-hidden': rounded,
|
||||||
'py-4': variant === 'slim',
|
'rounded-xl': rounded && size !== 'xl',
|
||||||
'black:rounded-none': size !== 'xl',
|
'rounded-3xl': rounded && size === 'xl',
|
||||||
|
'py-4 px-5': !slim,
|
||||||
}, className)}
|
}, className)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { throttle } from 'es-toolkit';
|
import { forwardRef } from 'react';
|
||||||
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import Helmet from 'soapbox/components/helmet.tsx';
|
import Helmet from 'soapbox/components/helmet.tsx';
|
||||||
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
|
||||||
import { Card, CardBody, CardHeader, CardTitle, type CardSizes } from './card.tsx';
|
import { Card, CardBody, CardHeader, CardTitle } from './card.tsx';
|
||||||
|
|
||||||
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' | 'className' | 'action'>;
|
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' | 'className' | 'action'>;
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ export interface IColumn {
|
||||||
label?: string;
|
label?: string;
|
||||||
/** Whether this column should have a transparent background. */
|
/** Whether this column should have a transparent background. */
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
|
/** Whether to display the column without padding. */
|
||||||
|
slim?: boolean;
|
||||||
/** Whether this column should have a title and back button. */
|
/** Whether this column should have a title and back button. */
|
||||||
withHeader?: boolean;
|
withHeader?: boolean;
|
||||||
/** Extra class name for top <div> element. */
|
/** Extra class name for top <div> element. */
|
||||||
|
@ -60,26 +62,13 @@ export interface IColumn {
|
||||||
/** Action for the ColumnHeader, displayed at the end. */
|
/** Action for the ColumnHeader, displayed at the end. */
|
||||||
action?: React.ReactNode;
|
action?: React.ReactNode;
|
||||||
/** Column size, inherited from Card. */
|
/** Column size, inherited from Card. */
|
||||||
size?: CardSizes;
|
size?: 'md' | 'lg' | 'xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A backdrop for the main section of the UI. */
|
/** A backdrop for the main section of the UI. */
|
||||||
const Column = forwardRef<HTMLDivElement, IColumn>((props, ref): JSX.Element => {
|
const Column = forwardRef<HTMLDivElement, IColumn>((props, ref): JSX.Element => {
|
||||||
const { backHref, children, label, transparent = false, withHeader = true, className, bodyClassName, action, size } = props;
|
const { backHref, children, label, transparent = false, slim, withHeader = true, className, bodyClassName, action, size } = props;
|
||||||
const soapboxConfig = useSoapboxConfig();
|
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 (
|
return (
|
||||||
<div role='region' className='relative' ref={ref} aria-label={label} column-type={transparent ? 'transparent' : 'filled'}>
|
<div role='region' className='relative' ref={ref} aria-label={label} column-type={transparent ? 'transparent' : 'filled'}>
|
||||||
|
@ -95,25 +84,25 @@ const Column = forwardRef<HTMLDivElement, IColumn>((props, ref): JSX.Element =>
|
||||||
)}
|
)}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Card size={size} variant={transparent ? undefined : 'rounded'} className={className}>
|
<Stack>
|
||||||
{withHeader && (
|
{withHeader && (
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
label={label}
|
label={label}
|
||||||
backHref={backHref}
|
backHref={backHref}
|
||||||
className={clsx({
|
className={clsx('px-5 py-4', {
|
||||||
'rounded-t-3xl': !isScrolled && !transparent,
|
'sticky top-12 z-20 bg-white/90 dark:bg-primary-900/90 black:bg-black/90 backdrop-blur lg:top-0': !transparent,
|
||||||
'sticky top-12 z-10 bg-white/90 dark:bg-primary-900/90 black:bg-black/90 backdrop-blur lg:top-16': !transparent,
|
'-mb-4': !slim,
|
||||||
'p-4 sm:p-0 sm:pb-4 black:p-4': transparent,
|
|
||||||
'-mt-4 p-4': size !== 'lg' && !transparent,
|
|
||||||
'-mt-4 p-4 sm:-mt-6 sm:-mx-6 sm:p-6': size === 'lg' && !transparent,
|
|
||||||
})}
|
})}
|
||||||
action={action}
|
action={action}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CardBody className={bodyClassName}>
|
|
||||||
{children}
|
<Card size={size} transparent={transparent} className={className} slim={slim}>
|
||||||
</CardBody>
|
<CardBody className={bodyClassName}>
|
||||||
</Card>
|
{children}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface IDivider {
|
||||||
const Divider = ({ text, textSize = 'md' }: IDivider) => (
|
const Divider = ({ text, textSize = 'md' }: IDivider) => (
|
||||||
<div className='relative' data-testid='divider'>
|
<div className='relative' data-testid='divider'>
|
||||||
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
||||||
<div className='w-full border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800' />
|
<div className='w-full border-t border-solid border-gray-100 dark:border-gray-800' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{text && (
|
{text && (
|
||||||
|
|
|
@ -95,7 +95,7 @@ const Input = forwardRef<HTMLInputElement, IInput>(
|
||||||
'text-gray-900 dark:text-gray-100': !props.disabled,
|
'text-gray-900 dark:text-gray-100': !props.disabled,
|
||||||
'text-gray-600': props.disabled,
|
'text-gray-600': props.disabled,
|
||||||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800 black:bg-black': theme === 'normal',
|
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800 black:bg-black': theme === 'normal',
|
||||||
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white dark:focus:bg-gray-900': theme === 'search',
|
'rounded-full bg-white border-gray-200 dark:bg-gray-900 dark:border-gray-800 py-2.5': theme === 'search',
|
||||||
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
|
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
|
||||||
'pl-8': typeof icon !== 'undefined',
|
'pl-8': typeof icon !== 'undefined',
|
||||||
'pl-16': typeof prepend !== 'undefined',
|
'pl-16': typeof prepend !== 'undefined',
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface LayoutComponent extends React.FC<ILayout> {
|
||||||
|
|
||||||
/** Layout container, to hold Sidebar, Main, and Aside. */
|
/** Layout container, to hold Sidebar, Main, and Aside. */
|
||||||
const Layout: LayoutComponent = ({ children }) => (
|
const Layout: LayoutComponent = ({ children }) => (
|
||||||
<div className='relative flex grow flex-col black:pt-0 sm:pt-4'>
|
<div className='relative flex grow flex-col'>
|
||||||
<div className='mx-auto w-full max-w-3xl grow sm:px-6 md:grid md:max-w-7xl md:grid-cols-12 md:gap-8 md:px-8'>
|
<div className='mx-auto w-full max-w-3xl grow sm:px-6 md:grid md:max-w-7xl md:grid-cols-12 md:gap-8 md:px-8'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ const Layout: LayoutComponent = ({ children }) => (
|
||||||
/** Left sidebar container in the UI. */
|
/** Left sidebar container in the UI. */
|
||||||
const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
||||||
<div className='hidden lg:col-span-3 lg:block'>
|
<div className='hidden lg:col-span-3 lg:block'>
|
||||||
<StickyBox offsetTop={80} className='pb-4'>
|
<StickyBox>
|
||||||
{children}
|
{children}
|
||||||
</StickyBox>
|
</StickyBox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,11 +39,7 @@ const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
||||||
|
|
||||||
/** Center column container in the UI. */
|
/** Center column container in the UI. */
|
||||||
const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className }) => (
|
const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className }) => (
|
||||||
<main
|
<main className={clsx('border-gray-200 bg-white pb-6 black:border-gray-800 black:bg-black dark:border-gray-800 dark:bg-primary-900 md:col-span-12 lg:col-span-9 lg:border-l xl:col-span-6 xl:border-r', className)}>
|
||||||
className={clsx({
|
|
||||||
'md:col-span-12 lg:col-span-9 xl:col-span-6 pb-36 black:border-gray-800 lg:black:border-l xl:black:border-r': true,
|
|
||||||
}, className)}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
@ -51,7 +47,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
|
||||||
/** Right sidebar container in the UI. */
|
/** Right sidebar container in the UI. */
|
||||||
const Aside: React.FC<IAside> = ({ children }) => (
|
const Aside: React.FC<IAside> = ({ children }) => (
|
||||||
<aside className='hidden xl:col-span-3 xl:block'>
|
<aside className='hidden xl:col-span-3 xl:block'>
|
||||||
<StickyBox offsetTop={80} className='space-y-6 pb-12'>
|
<StickyBox className='space-y-6 py-6 pb-12'>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{children}
|
{children}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -36,6 +36,6 @@ const MenuList: React.FC<IMenuList> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Divides menu items. */
|
/** Divides menu items. */
|
||||||
const MenuDivider = () => <hr className='mx-2 my-1 border-t-2 border-gray-100 black:border-t dark:border-gray-800' />;
|
const MenuDivider = () => <hr className='mx-2 my-1 border-t border-gray-100 dark:border-gray-800' />;
|
||||||
|
|
||||||
export { Menu, MenuButton, MenuDivider, MenuItems, MenuItem, MenuList, MenuLink };
|
export { Menu, MenuButton, MenuDivider, MenuItems, MenuItem, MenuList, MenuLink };
|
||||||
|
|
|
@ -69,7 +69,7 @@ const AboutPage: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card variant='rounded'>
|
<Card>
|
||||||
<div className='prose mx-auto py-4 dark:prose-invert sm:p-6'>
|
<div className='prose mx-auto py-4 dark:prose-invert sm:p-6'>
|
||||||
<div dangerouslySetInnerHTML={{ __html: pageHtml }} />
|
<div dangerouslySetInnerHTML={{ __html: pageHtml }} />
|
||||||
{alsoAvailable}
|
{alsoAvailable}
|
||||||
|
|
|
@ -137,9 +137,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 lg:h-48' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 sm:px-6'>
|
<div className='px-4 sm:px-6'>
|
||||||
|
@ -679,13 +679,13 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
const acceptsZaps = account.ditto.accepts_zaps === true;
|
const acceptsZaps = account.ditto.accepts_zaps === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
<div>
|
||||||
{(account.moved && typeof account.moved === 'object') && (
|
{(account.moved && typeof account.moved === 'object') && (
|
||||||
<MovedNote from={account} to={account.moved as Account} />
|
<MovedNote from={account} to={account.moved as Account} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 black:rounded-t-none dark:bg-gray-900/50 md:rounded-t-xl lg:h-48'>
|
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 dark:bg-gray-900/50 lg:h-48'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
<div className='absolute left-2 top-2'>
|
<div className='absolute left-2 top-2'>
|
||||||
|
|
|
@ -101,7 +101,7 @@ const AuthTokenList: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.header)} transparent withHeader={false}>
|
<Column label={intl.formatMessage(messages.header)} transparent withHeader={false}>
|
||||||
<Card variant='rounded'>
|
<Card>
|
||||||
<CardHeader backHref='/settings'>
|
<CardHeader backHref='/settings'>
|
||||||
<CardTitle title={intl.formatMessage(messages.header)} />
|
<CardTitle title={intl.formatMessage(messages.header)} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
|
@ -84,7 +84,7 @@ const Backups = () => {
|
||||||
const showLoading = isLoading && backups.count() === 0;
|
const showLoading = isLoading && backups.count() === 0;
|
||||||
|
|
||||||
const emptyMessage = (
|
const emptyMessage = (
|
||||||
<Card variant='rounded' size='lg'>
|
<Card size='lg'>
|
||||||
{intl.formatMessage(messages.emptyMessage, {
|
{intl.formatMessage(messages.emptyMessage, {
|
||||||
action: (
|
action: (
|
||||||
<Link to={'/'} className='inline-flex'>
|
<Link to={'/'} className='inline-flex'>
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { useBookmarks } from 'soapbox/api/hooks/index.ts';
|
||||||
import PullToRefresh from 'soapbox/components/pull-to-refresh.tsx';
|
import PullToRefresh from 'soapbox/components/pull-to-refresh.tsx';
|
||||||
import PureStatusList from 'soapbox/components/pure-status-list.tsx';
|
import PureStatusList from 'soapbox/components/pure-status-list.tsx';
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -15,9 +13,6 @@ const messages = defineMessages({
|
||||||
const Bookmarks: React.FC = () => {
|
const Bookmarks: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleLoadMore = debounce(() => {
|
const handleLoadMore = debounce(() => {
|
||||||
fetchNextPage();
|
fetchNextPage();
|
||||||
}, 300, { edges: ['leading'] });
|
}, 300, { edges: ['leading'] });
|
||||||
|
@ -31,7 +26,7 @@ const Bookmarks: React.FC = () => {
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<PullToRefresh onRefresh={handleRefresh}>
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
<PureStatusList
|
<PureStatusList
|
||||||
className='black:p-4 black:sm:p-5'
|
className='black:p-4 black:sm:p-5'
|
||||||
|
@ -41,7 +36,6 @@ const Bookmarks: React.FC = () => {
|
||||||
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
||||||
onLoadMore={() => handleLoadMore()}
|
onLoadMore={() => handleLoadMore()}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -8,15 +8,12 @@ import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import Timeline from '../ui/components/timeline.tsx';
|
import Timeline from '../ui/components/timeline.tsx';
|
||||||
|
|
||||||
const CommunityTimeline = () => {
|
const CommunityTimeline = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const theme = useTheme();
|
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
@ -24,7 +21,6 @@ const CommunityTimeline = () => {
|
||||||
const next = useAppSelector(state => state.timelines.get('community')?.next);
|
const next = useAppSelector(state => state.timelines.get('community')?.next);
|
||||||
|
|
||||||
const timelineId = 'community';
|
const timelineId = 'community';
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia }));
|
dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia }));
|
||||||
|
@ -41,7 +37,7 @@ const CommunityTimeline = () => {
|
||||||
}, [onlyMedia]);
|
}, [onlyMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column className='-mt-3 sm:mt-0' label={instance.domain} transparent={!isMobile}>
|
<Column label={instance.domain} slim withHeader={false}>
|
||||||
<PullToRefresh onRefresh={handleRefresh}>
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
<Timeline
|
<Timeline
|
||||||
className='black:p-4 black:sm:p-5'
|
className='black:p-4 black:sm:p-5'
|
||||||
|
@ -50,7 +46,6 @@ const CommunityTimeline = () => {
|
||||||
prefix='home'
|
prefix='home'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -154,13 +154,11 @@ const SearchResults = () => {
|
||||||
|
|
||||||
if (results.statuses && results.statuses.size > 0) {
|
if (results.statuses && results.statuses.size > 0) {
|
||||||
searchResults = results.statuses.map((statusId: string) => (
|
searchResults = results.statuses.map((statusId: string) => (
|
||||||
// @ts-ignore
|
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={statusId}
|
key={statusId}
|
||||||
id={statusId}
|
id={statusId}
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
variant='slim'
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
resultsIds = results.statuses;
|
resultsIds = results.statuses;
|
||||||
|
@ -173,7 +171,6 @@ const SearchResults = () => {
|
||||||
id={statusId}
|
id={statusId}
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
variant='slim'
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
resultsIds = trendingStatuses;
|
resultsIds = trendingStatuses;
|
||||||
|
@ -227,7 +224,9 @@ const SearchResults = () => {
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
) : renderFilterBar()}
|
) : (
|
||||||
|
<div className='px-4'>{renderFilterBar()}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{noResultsMessage || (
|
{noResultsMessage || (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
|
@ -244,7 +243,8 @@ const SearchResults = () => {
|
||||||
listClassName={clsx({
|
listClassName={clsx({
|
||||||
'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses',
|
'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses',
|
||||||
})}
|
})}
|
||||||
itemClassName={clsx('px-4', {
|
itemClassName={clsx({
|
||||||
|
'px-4': selectedFilter !== 'statuses',
|
||||||
'pb-4': selectedFilter === 'accounts',
|
'pb-4': selectedFilter === 'accounts',
|
||||||
'pb-3': selectedFilter === 'hashtags',
|
'pb-3': selectedFilter === 'hashtags',
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -52,7 +52,7 @@ const DeleteAccount = () => {
|
||||||
}, [password, dispatch, intl]);
|
}, [password, dispatch, intl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded'>
|
<Card>
|
||||||
<CardHeader backHref='/settings'>
|
<CardHeader backHref='/settings'>
|
||||||
<CardTitle title={intl.formatMessage(messages.deleteHeader)} />
|
<CardTitle title={intl.formatMessage(messages.deleteHeader)} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
|
@ -36,7 +36,7 @@ const DirectTimeline = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)} slim>
|
||||||
<AccountSearch
|
<AccountSearch
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||||
onSelected={handleSuggestion}
|
onSelected={handleSuggestion}
|
||||||
|
@ -47,7 +47,6 @@ const DirectTimeline = () => {
|
||||||
timelineId='direct'
|
timelineId='direct'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
divideType='border'
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -55,7 +55,7 @@ const EmbeddedStatus: React.FC<IEmbeddedStatus> = ({ params }) => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else if (status) {
|
} else if (status) {
|
||||||
return <Status status={status} accountAction={logo} variant='default' />;
|
return <Status status={status} accountAction={logo} />;
|
||||||
} else {
|
} else {
|
||||||
return <MissingIndicator nested />;
|
return <MissingIndicator nested />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||||
ref={scroller}
|
ref={scroller}
|
||||||
hasMore={!!next}
|
hasMore={!!next}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
placeholderComponent={() => <PlaceholderStatus variant='slim' />}
|
placeholderComponent={() => <PlaceholderStatus />}
|
||||||
initialTopMostItemIndex={0}
|
initialTopMostItemIndex={0}
|
||||||
emptyMessage={<FormattedMessage id='event.discussion.empty' defaultMessage='No one has commented this event yet. When someone does, they will appear here.' />}
|
emptyMessage={<FormattedMessage id='event.discussion.empty' defaultMessage='No one has commented this event yet. When someone does, they will appear here.' />}
|
||||||
>
|
>
|
||||||
|
|
|
@ -49,7 +49,7 @@ const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMe
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='lg'>
|
<Card size='lg'>
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,7 +98,7 @@ const FeedSuggestions: React.FC<IFeedSuggesetions> = ({ statusId, onMoveUp, onMo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<Card size='lg' variant='rounded' className='focusable space-y-6' tabIndex={0}>
|
<Card size='lg' className='focusable space-y-6' tabIndex={0}>
|
||||||
<HStack justifyContent='between' alignItems='center'>
|
<HStack justifyContent='between' alignItems='center'>
|
||||||
<CardTitle title={intl.formatMessage(messages.heading)} />
|
<CardTitle title={intl.formatMessage(messages.heading)} />
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,9 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
|
<div data-testid='group-header-missing'>
|
||||||
<div>
|
<div>
|
||||||
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 lg:h-48' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 sm:px-6'>
|
<div className='px-4 sm:px-6'>
|
||||||
|
@ -123,7 +123,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
<div>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,6 @@ const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||||
scrollKey='group_timeline'
|
scrollKey='group_timeline'
|
||||||
timelineId={`group:tags:${groupId}:${tag.name}`}
|
timelineId={`group:tags:${groupId}:${tag.name}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
divideType='border'
|
|
||||||
showGroup={false}
|
showGroup={false}
|
||||||
emptyMessageCard={false}
|
emptyMessageCard={false}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
|
|
|
@ -130,7 +130,6 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
emptyMessageCard={false}
|
emptyMessageCard={false}
|
||||||
divideType='border'
|
|
||||||
showGroup={false}
|
showGroup={false}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,9 +11,7 @@ import Timeline from 'soapbox/features/ui/components/timeline.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts';
|
import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
interface IHashtagTimeline {
|
interface IHashtagTimeline {
|
||||||
params?: {
|
params?: {
|
||||||
|
@ -29,8 +27,6 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const tag = useAppSelector((state) => state.tags.get(id));
|
const tag = useAppSelector((state) => state.tags.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
||||||
const { isLoggedIn } = useLoggedIn();
|
const { isLoggedIn } = useLoggedIn();
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
|
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
|
||||||
|
@ -57,7 +53,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`} transparent={!isMobile}>
|
<Column label={`#${id}`} slim>
|
||||||
{features.followHashtags && isLoggedIn && (
|
{features.followHashtags && isLoggedIn && (
|
||||||
<List>
|
<List>
|
||||||
<ListItem
|
<ListItem
|
||||||
|
@ -76,7 +72,6 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,8 +12,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||||
|
@ -24,10 +22,8 @@ const HomeTimeline: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const polling = useRef<NodeJS.Timeout | null>(null);
|
const polling = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
||||||
const next = useAppSelector(state => state.timelines.get('home')?.next);
|
const next = useAppSelector(state => state.timelines.get('home')?.next);
|
||||||
|
@ -68,14 +64,12 @@ const HomeTimeline: React.FC = () => {
|
||||||
}, [isPartial]);
|
}, [isPartial]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column className='py-0' label={intl.formatMessage(messages.title)} transparent={!isMobile} withHeader={false}>
|
<Column label={intl.formatMessage(messages.title)} withHeader={false} slim>
|
||||||
<PullToRefresh onRefresh={handleRefresh}>
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
<Timeline
|
<Timeline
|
||||||
className='black:p-4 black:sm:p-5'
|
|
||||||
scrollKey='home_timeline'
|
scrollKey='home_timeline'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
timelineId='home'
|
timelineId='home'
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<Stack space={1}>
|
<Stack space={1}>
|
||||||
<Text size='xl' weight='medium' align='center'>
|
<Text size='xl' weight='medium' align='center'>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import AboutPage from '../about/index.tsx';
|
import AboutPage from '../about/index.tsx';
|
||||||
import Timeline from '../ui/components/timeline.tsx';
|
import Timeline from '../ui/components/timeline.tsx';
|
||||||
|
@ -19,7 +18,6 @@ import { SiteBanner } from './components/site-banner.tsx';
|
||||||
const LandingTimeline = () => {
|
const LandingTimeline = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local;
|
const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local;
|
||||||
|
@ -48,7 +46,7 @@ const LandingTimeline = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column transparent={!isMobile} withHeader={false}>
|
<Column transparent={!isMobile} withHeader={false} slim>
|
||||||
<div className='my-12 mb-16 px-4 sm:mb-20'>
|
<div className='my-12 mb-16 px-4 sm:mb-20'>
|
||||||
<SiteBanner />
|
<SiteBanner />
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,7 +60,6 @@ const LandingTimeline = () => {
|
||||||
prefix='home'
|
prefix='home'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -12,16 +12,12 @@ import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import Timeline from '../ui/components/timeline.tsx';
|
import Timeline from '../ui/components/timeline.tsx';
|
||||||
|
|
||||||
const ListTimeline: React.FC = () => {
|
const ListTimeline: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const list = useAppSelector((state) => state.lists.get(id));
|
const list = useAppSelector((state) => state.lists.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
|
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
|
||||||
|
@ -66,14 +62,13 @@ const ListTimeline: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={title} transparent={!isMobile}>
|
<Column label={title}>
|
||||||
<Timeline
|
<Timeline
|
||||||
className='black:p-4 black:sm:p-5'
|
className='black:p-4 black:sm:p-5'
|
||||||
scrollKey='list_timeline'
|
scrollKey='list_timeline'
|
||||||
timelineId={`list:${id}`}
|
timelineId={`list:${id}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,9 +19,7 @@ import Portal from 'soapbox/components/ui/portal.tsx';
|
||||||
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification.tsx';
|
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import FilterBar from './components/filter-bar.tsx';
|
import FilterBar from './components/filter-bar.tsx';
|
||||||
import Notification from './components/notification.tsx';
|
import Notification from './components/notification.tsx';
|
||||||
|
@ -63,9 +61,6 @@ const Notifications = () => {
|
||||||
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
||||||
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const node = useRef<VirtuosoHandle>(null);
|
const node = useRef<VirtuosoHandle>(null);
|
||||||
const column = useRef<HTMLDivElement>(null);
|
const column = useRef<HTMLDivElement>(null);
|
||||||
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
||||||
|
@ -186,7 +181,7 @@ const Notifications = () => {
|
||||||
ref={column}
|
ref={column}
|
||||||
label={intl.formatMessage(messages.title)}
|
label={intl.formatMessage(messages.title)}
|
||||||
withHeader={false}
|
withHeader={false}
|
||||||
className={clsx({ '!p-0': isMobile || theme === 'black' })}
|
className='!p-0'
|
||||||
>
|
>
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,12 @@ import PlaceholderDisplayName from './placeholder-display-name.tsx';
|
||||||
import PlaceholderStatusContent from './placeholder-status-content.tsx';
|
import PlaceholderStatusContent from './placeholder-status-content.tsx';
|
||||||
|
|
||||||
interface IPlaceholderStatus {
|
interface IPlaceholderStatus {
|
||||||
variant?: 'rounded' | 'slim' | 'default';
|
slim?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fake status to display while data is loading. */
|
/** Fake status to display while data is loading. */
|
||||||
const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ variant }) => (
|
const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ slim }) => (
|
||||||
<div
|
<div className={clsx('status-placeholder bg-white black:bg-black dark:bg-primary-900', { 'p-4': !slim })}>
|
||||||
className={clsx({
|
|
||||||
'status-placeholder bg-white black:bg-black dark:bg-primary-900': true,
|
|
||||||
'shadow-xl dark:shadow-none sm:rounded-xl px-4 py-6 sm:p-5': variant === 'rounded',
|
|
||||||
'py-4': variant === 'slim',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className='w-full animate-pulse overflow-hidden'>
|
<div className='w-full animate-pulse overflow-hidden'>
|
||||||
<div>
|
<div>
|
||||||
<HStack space={3} alignItems='center'>
|
<HStack space={3} alignItems='center'>
|
||||||
|
|
|
@ -14,9 +14,7 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker.tsx';
|
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker.tsx';
|
||||||
import Timeline from '../ui/components/timeline.tsx';
|
import Timeline from '../ui/components/timeline.tsx';
|
||||||
|
@ -30,7 +28,6 @@ const PublicTimeline = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const [language, setLanguage] = useState<string>(localStorage.getItem('soapbox:global:language') || '');
|
const [language, setLanguage] = useState<string>(localStorage.getItem('soapbox:global:language') || '');
|
||||||
|
|
||||||
|
@ -40,7 +37,6 @@ const PublicTimeline = () => {
|
||||||
const next = useAppSelector(state => state.timelines.get('public')?.next);
|
const next = useAppSelector(state => state.timelines.get('public')?.next);
|
||||||
|
|
||||||
const timelineId = 'public';
|
const timelineId = 'public';
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const explanationBoxExpanded = settings.explanationBox;
|
const explanationBoxExpanded = settings.explanationBox;
|
||||||
const showExplanationBox = settings.showExplanationBox && !features.nostr;
|
const showExplanationBox = settings.showExplanationBox && !features.nostr;
|
||||||
|
@ -74,10 +70,9 @@ const PublicTimeline = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
className='-mt-3 sm:mt-0'
|
|
||||||
label={intl.formatMessage(messages.title)}
|
label={intl.formatMessage(messages.title)}
|
||||||
transparent={!isMobile}
|
|
||||||
action={features.publicTimelineLanguage ? <LanguageDropdown language={language} setLanguage={setLanguage} /> : null}
|
action={features.publicTimelineLanguage ? <LanguageDropdown language={language} setLanguage={setLanguage} /> : null}
|
||||||
|
slim
|
||||||
>
|
>
|
||||||
<PinnedHostsPicker />
|
<PinnedHostsPicker />
|
||||||
|
|
||||||
|
@ -118,7 +113,6 @@ const PublicTimeline = () => {
|
||||||
prefix='home'
|
prefix='home'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
|
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
|
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
|
||||||
|
@ -23,7 +22,6 @@ const Quotes: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { statusId } = useParams<{ statusId: string }>();
|
const { statusId } = useParams<{ statusId: string }>();
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>()));
|
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>()));
|
||||||
|
@ -51,7 +49,6 @@ const Quotes: React.FC = () => {
|
||||||
onLoadMore={() => handleLoadMore(statusId, dispatch)}
|
onLoadMore={() => handleLoadMore(statusId, dispatch)}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,9 +11,7 @@ import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
import { useSettings } from 'soapbox/hooks/useSettings.ts';
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
import Timeline from '../ui/components/timeline.tsx';
|
import Timeline from '../ui/components/timeline.tsx';
|
||||||
|
|
||||||
|
@ -29,7 +27,6 @@ interface IRemoteTimeline {
|
||||||
const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const instance = params?.instance as string;
|
const instance = params?.instance as string;
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
@ -39,7 +36,6 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
const next = useAppSelector(state => state.timelines.get('remote')?.next);
|
const next = useAppSelector(state => state.timelines.get('remote')?.next);
|
||||||
|
|
||||||
const pinned = settings.remote_timeline.pinnedHosts.includes(instance);
|
const pinned = settings.remote_timeline.pinnedHosts.includes(instance);
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const handleCloseClick: React.MouseEventHandler = () => {
|
const handleCloseClick: React.MouseEventHandler = () => {
|
||||||
history.push('/timeline/fediverse');
|
history.push('/timeline/fediverse');
|
||||||
|
@ -56,7 +52,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
}, [onlyMedia]);
|
}, [onlyMedia]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={instance} transparent={!isMobile}>
|
<Column label={instance} slim>
|
||||||
{instance && <PinnedHostsPicker host={instance} />}
|
{instance && <PinnedHostsPicker host={instance} />}
|
||||||
|
|
||||||
{!pinned && (
|
{!pinned && (
|
||||||
|
@ -84,7 +80,6 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
values={{ instance }}
|
values={{ instance }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import SearchResults from 'soapbox/features/compose/components/search-results.tsx';
|
import SearchResults from 'soapbox/features/compose/components/search-results.tsx';
|
||||||
import Search from 'soapbox/features/compose/components/search.tsx';
|
import Search from 'soapbox/features/compose/components/search.tsx';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.search', defaultMessage: 'Discover' },
|
heading: { id: 'column.search', defaultMessage: 'Discover' },
|
||||||
|
@ -14,20 +12,14 @@ const messages = defineMessages({
|
||||||
const SearchPage = () => {
|
const SearchPage = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column label={intl.formatMessage(messages.heading)} slim>
|
||||||
label={intl.formatMessage(messages.heading)}
|
<Stack space={4}>
|
||||||
className={clsx({ '!px-0': isMobile || theme === 'black' })}
|
<div className='px-4'>
|
||||||
>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div className='px-4 sm:py-0'>
|
|
||||||
<Search autoSubmit />
|
<Search autoSubmit />
|
||||||
</div>
|
</div>
|
||||||
<SearchResults />
|
<SearchResults />
|
||||||
</div>
|
</Stack>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -68,8 +68,8 @@ const Settings = () => {
|
||||||
const displayName = account.display_name || account.username;
|
const displayName = account.display_name || account.username;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.settings)} transparent withHeader={false}>
|
<Column label={intl.formatMessage(messages.settings)} transparent withHeader={false} slim>
|
||||||
<Card className='space-y-4' variant='rounded'>
|
<Card className='space-y-4'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle title={intl.formatMessage(messages.profile)} />
|
<CardTitle title={intl.formatMessage(messages.profile)} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { defaultSettings } from 'soapbox/actions/settings.ts';
|
import { defaultSettings } from 'soapbox/actions/settings.ts';
|
||||||
import SiteLogo from 'soapbox/components/site-logo.tsx';
|
import SiteLogo from 'soapbox/components/site-logo.tsx';
|
||||||
import BackgroundShapes from 'soapbox/features/ui/components/background-shapes.tsx';
|
|
||||||
import { useSystemTheme } from 'soapbox/hooks/useSystemTheme.ts';
|
import { useSystemTheme } from 'soapbox/hooks/useSystemTheme.ts';
|
||||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts';
|
import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts';
|
||||||
import { generateThemeCss } from 'soapbox/utils/theme.ts';
|
import { generateThemeCss } from 'soapbox/utils/theme.ts';
|
||||||
|
@ -39,7 +38,6 @@ const SitePreview: React.FC<ISitePreview> = ({ soapbox }) => {
|
||||||
<div className={bodyClass}>
|
<div className={bodyClass}>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
<style>{`.site-preview {${generateThemeCss(soapboxConfig)}}`}</style>
|
<style>{`.site-preview {${generateThemeCss(soapboxConfig)}}`}</style>
|
||||||
<BackgroundShapes position='absolute' />
|
|
||||||
|
|
||||||
<div className='absolute z-[2] self-center overflow-hidden rounded-lg bg-accent-500 p-2 text-white'>
|
<div className='absolute z-[2] self-center overflow-hidden rounded-lg bg-accent-500 p-2 text-white'>
|
||||||
<FormattedMessage id='site_preview.preview' defaultMessage='Preview' />
|
<FormattedMessage id='site_preview.preview' defaultMessage='Preview' />
|
||||||
|
|
|
@ -15,7 +15,7 @@ const ThreadLoginCta: React.FC = () => {
|
||||||
if (!displayCta) return null;
|
if (!displayCta) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='space-y-6 px-6 py-12 text-center' variant='rounded'>
|
<Card className='space-y-6 px-6 py-12 text-center'>
|
||||||
<Stack>
|
<Stack>
|
||||||
<CardTitle title={<FormattedMessage id='thread_login.title' defaultMessage='Continue the conversation' />} />
|
<CardTitle title={<FormattedMessage id='thread_login.title' defaultMessage='Continue the conversation' />} />
|
||||||
<Text>
|
<Text>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const ThreadStatus: React.FC<IThreadStatus> = (props): JSX.Element => {
|
||||||
// @ts-ignore FIXME
|
// @ts-ignore FIXME
|
||||||
<StatusContainer {...props} showGroup={false} />
|
<StatusContainer {...props} showGroup={false} />
|
||||||
) : (
|
) : (
|
||||||
<PlaceholderStatus variant='default' />
|
<PlaceholderStatus slim />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -399,9 +399,7 @@ const Thread = (props: IThread) => {
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
||||||
{hasDescendants && (
|
<hr className='-mx-4 mt-2 max-w-[100vw] border-t-2 black:border-t dark:border-gray-800' />
|
||||||
<hr className='-mx-4 mt-2 max-w-[100vw] border-t-2 black:border-t dark:border-gray-800' />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -445,7 +443,7 @@ const Thread = (props: IThread) => {
|
||||||
ref={scroller}
|
ref={scroller}
|
||||||
hasMore={!!next}
|
hasMore={!!next}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
placeholderComponent={() => <PlaceholderStatus variant='slim' />}
|
placeholderComponent={() => <PlaceholderStatus />}
|
||||||
initialTopMostItemIndex={initialTopMostItemIndex}
|
initialTopMostItemIndex={initialTopMostItemIndex}
|
||||||
useWindowScroll={useWindowScroll}
|
useWindowScroll={useWindowScroll}
|
||||||
itemClassName={itemClassName}
|
itemClassName={itemClassName}
|
||||||
|
|
|
@ -12,8 +12,6 @@ import { expandTimelineSuccess } from 'soapbox/actions/timelines.ts';
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import Timeline from 'soapbox/features/ui/components/timeline.tsx';
|
import Timeline from 'soapbox/features/ui/components/timeline.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.test', defaultMessage: 'Test timeline' },
|
title: { id: 'column.test', defaultMessage: 'Test timeline' },
|
||||||
|
@ -31,8 +29,6 @@ const onlyMedia = false;
|
||||||
const TestTimeline: React.FC = () => {
|
const TestTimeline: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(importFetchedStatuses(MOCK_STATUSES));
|
dispatch(importFetchedStatuses(MOCK_STATUSES));
|
||||||
|
@ -40,12 +36,11 @@ const TestTimeline: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} transparent={!isMobile}>
|
<Column label={intl.formatMessage(messages.title)} slim>
|
||||||
<Timeline
|
<Timeline
|
||||||
scrollKey={`${timelineId}_timeline`}
|
scrollKey={`${timelineId}_timeline`}
|
||||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
|
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
|
||||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface IBackgroundShapes {
|
|
||||||
/** Whether the shapes should be absolute positioned or fixed. */
|
|
||||||
position?: 'fixed' | 'absolute';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gradient that appears in the background of the UI. */
|
|
||||||
const BackgroundShapes: React.FC<IBackgroundShapes> = ({ position = 'fixed' }) => (
|
|
||||||
<div className={clsx(position, 'pointer-events-none inset-x-0 top-0 flex justify-center overflow-hidden black:hidden')}>
|
|
||||||
<div className='bg-gradient-sm lg:bg-gradient-light lg:dark:bg-gradient-dark h-screen w-screen' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BackgroundShapes;
|
|
|
@ -2,7 +2,7 @@ import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
||||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||||
|
|
||||||
const ColumnLoading = () => (
|
const ColumnLoading = () => (
|
||||||
<Card variant='rounded'>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
|
@ -21,7 +21,6 @@ import Search from 'soapbox/features/compose/components/search.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
import { useRegistrationStatus } from 'soapbox/hooks/useRegistrationStatus.ts';
|
import { useRegistrationStatus } from 'soapbox/hooks/useRegistrationStatus.ts';
|
||||||
import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications.ts';
|
import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications.ts';
|
||||||
|
@ -44,7 +43,6 @@ const Navbar = () => {
|
||||||
const { isOpen } = useRegistrationStatus();
|
const { isOpen } = useRegistrationStatus();
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const node = useRef(null);
|
const node = useRef(null);
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const settingsNotifications = useSettingsNotifications();
|
const settingsNotifications = useSettingsNotifications();
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
const [isLoading, setLoading] = useState<boolean>(false);
|
||||||
|
@ -87,10 +85,7 @@ const Navbar = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={clsx(
|
className='border-b border-gray-200 bg-white shadow black:border-gray-800 black:bg-black dark:border-gray-800 dark:bg-primary-900'
|
||||||
'sticky top-0 z-50 border-gray-200 bg-white shadow black:border-b black:border-b-gray-800 black:bg-black dark:border-gray-800 dark:bg-primary-900',
|
|
||||||
{ 'border-b': isMobile },
|
|
||||||
)}
|
|
||||||
ref={node}
|
ref={node}
|
||||||
data-testid='navbar'
|
data-testid='navbar'
|
||||||
>
|
>
|
||||||
|
|
|
@ -65,7 +65,6 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
||||||
'py-6 sm:p-5': !thread,
|
'py-6 sm:p-5': !thread,
|
||||||
'status-reply': !!status.in_reply_to_id,
|
'status-reply': !!status.in_reply_to_id,
|
||||||
})}
|
})}
|
||||||
variant={thread ? 'default' : 'rounded'}
|
|
||||||
>
|
>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<HStack justifyContent='between' alignItems='start'>
|
<HStack justifyContent='between' alignItems='start'>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useFloating } from '@floating-ui/react';
|
import { Placement, useFloating } from '@floating-ui/react';
|
||||||
import logoutIcon from '@tabler/icons/outline/logout.svg';
|
import logoutIcon from '@tabler/icons/outline/logout.svg';
|
||||||
import plusIcon from '@tabler/icons/outline/plus.svg';
|
import plusIcon from '@tabler/icons/outline/plus.svg';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
@ -29,6 +29,7 @@ const messages = defineMessages({
|
||||||
interface IProfileDropdown {
|
interface IProfileDropdown {
|
||||||
account: AccountEntity;
|
account: AccountEntity;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IMenuItem = {
|
type IMenuItem = {
|
||||||
|
@ -39,13 +40,13 @@ type IMenuItem = {
|
||||||
action?: (event: React.MouseEvent) => void;
|
action?: (event: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children, placement = 'bottom-end' }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement: 'bottom-end' });
|
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement });
|
||||||
|
|
||||||
const getOtherAccounts = useCallback(makeGetOtherAccounts(), []);
|
const getOtherAccounts = useCallback(makeGetOtherAccounts(), []);
|
||||||
const otherAccounts = useAppSelector((state) => getOtherAccounts(state));
|
const otherAccounts = useAppSelector((state) => getOtherAccounts(state));
|
||||||
|
@ -117,7 +118,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className='rounded-full focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus:ring-primary-500'
|
className='w-full rounded-full focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus:ring-primary-500'
|
||||||
type='button'
|
type='button'
|
||||||
ref={refs.setReference}
|
ref={refs.setReference}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
|
|
|
@ -127,7 +127,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
<div>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
|
@ -147,7 +147,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||||
const badges = getBadges();
|
const badges = getBadges();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
<div>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useAccount } from 'soapbox/api/hooks/index.ts';
|
|
||||||
import StillImage from 'soapbox/components/still-image.tsx';
|
|
||||||
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
|
||||||
import VerificationBadge from 'soapbox/components/verification-badge.tsx';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
|
||||||
import { getAcct } from 'soapbox/utils/accounts.ts';
|
|
||||||
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers.tsx';
|
|
||||||
import { displayFqn } from 'soapbox/utils/state.ts';
|
|
||||||
|
|
||||||
interface IUserPanel {
|
|
||||||
accountId: string;
|
|
||||||
action?: JSX.Element;
|
|
||||||
badges?: JSX.Element[];
|
|
||||||
domain?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { account } = useAccount(accountId);
|
|
||||||
const fqn = useAppSelector((state) => displayFqn(state));
|
|
||||||
|
|
||||||
if (!account) return null;
|
|
||||||
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
|
||||||
const header = account.header;
|
|
||||||
const verified = account.verified;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative'>
|
|
||||||
<Stack space={2}>
|
|
||||||
<Stack>
|
|
||||||
<div className='relative -mx-4 -mt-4 h-24 overflow-hidden bg-gray-200'>
|
|
||||||
{header && (
|
|
||||||
<StillImage src={account.header} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HStack justifyContent='between'>
|
|
||||||
<Link
|
|
||||||
to={`/@${account.acct}`}
|
|
||||||
title={acct}
|
|
||||||
className='-mt-12 block'
|
|
||||||
>
|
|
||||||
<Avatar src={account.avatar} size={80} className='size-20 overflow-hidden bg-gray-50 ring-2 ring-white' />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{action && (
|
|
||||||
<div className='mt-2'>{action}</div>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack>
|
|
||||||
<Link to={`/@${account.acct}`}>
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
<Text size='lg' weight='bold' truncate>
|
|
||||||
{emojifyText(account.display_name, account.emojis)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{verified && <VerificationBadge />}
|
|
||||||
|
|
||||||
{badges && badges.length > 0 && (
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
{badges}
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<HStack>
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<Text size='sm' theme='muted' direction='ltr' truncate>
|
|
||||||
@{getAcct(account, fqn)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<HStack alignItems='center' space={3}>
|
|
||||||
{account.followers_count >= 0 && (
|
|
||||||
<Link to={`/@${account.acct}/followers`} title={intl.formatNumber(account.followers_count)}>
|
|
||||||
<HStack alignItems='center' space={1}>
|
|
||||||
<Text theme='primary' weight='bold' size='sm'>
|
|
||||||
{shortNumberFormat(account.followers_count)}
|
|
||||||
</Text>
|
|
||||||
<Text weight='bold' size='sm'>
|
|
||||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{account.following_count >= 0 && (
|
|
||||||
<Link to={`/@${account.acct}/following`} title={intl.formatNumber(account.following_count)}>
|
|
||||||
<HStack alignItems='center' space={1}>
|
|
||||||
<Text theme='primary' weight='bold' size='sm'>
|
|
||||||
{shortNumberFormat(account.following_count)}
|
|
||||||
</Text>
|
|
||||||
<Text weight='bold' size='sm'>
|
|
||||||
<FormattedMessage id='account.follows' defaultMessage='Following' />
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserPanel;
|
|
|
@ -43,7 +43,6 @@ import SearchPage from 'soapbox/pages/search-page.tsx';
|
||||||
import StatusPage from 'soapbox/pages/status-page.tsx';
|
import StatusPage from 'soapbox/pages/status-page.tsx';
|
||||||
import WidePage from 'soapbox/pages/wide-page.tsx';
|
import WidePage from 'soapbox/pages/wide-page.tsx';
|
||||||
|
|
||||||
import BackgroundShapes from './components/background-shapes.tsx';
|
|
||||||
import FloatingActionButton from './components/floating-action-button.tsx';
|
import FloatingActionButton from './components/floating-action-button.tsx';
|
||||||
import Navbar from './components/navbar.tsx';
|
import Navbar from './components/navbar.tsx';
|
||||||
import {
|
import {
|
||||||
|
@ -505,10 +504,10 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BackgroundShapes />
|
|
||||||
|
|
||||||
<div className='z-10 flex min-h-screen flex-col'>
|
<div className='z-10 flex min-h-screen flex-col'>
|
||||||
<Navbar />
|
<div className='sticky top-0 z-50 sm:hidden'>
|
||||||
|
<Navbar />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Sidebar>
|
<Layout.Sidebar>
|
||||||
|
|
|
@ -82,7 +82,6 @@ export const ServerInfo = lazy(() => import('soapbox/features/server-info/index.
|
||||||
export const Dashboard = lazy(() => import('soapbox/features/admin/index.tsx'));
|
export const Dashboard = lazy(() => import('soapbox/features/admin/index.tsx'));
|
||||||
export const ModerationLog = lazy(() => import('soapbox/features/admin/moderation-log.tsx'));
|
export const ModerationLog = lazy(() => import('soapbox/features/admin/moderation-log.tsx'));
|
||||||
export const ThemeEditor = lazy(() => import('soapbox/features/theme-editor/index.tsx'));
|
export const ThemeEditor = lazy(() => import('soapbox/features/theme-editor/index.tsx'));
|
||||||
export const UserPanel = lazy(() => import('soapbox/features/ui/components/user-panel.tsx'));
|
|
||||||
export const PromoPanel = lazy(() => import('soapbox/features/ui/components/promo-panel.tsx'));
|
export const PromoPanel = lazy(() => import('soapbox/features/ui/components/promo-panel.tsx'));
|
||||||
export const SignUpPanel = lazy(() => import('soapbox/features/ui/components/panels/sign-up-panel.tsx'));
|
export const SignUpPanel = lazy(() => import('soapbox/features/ui/components/panels/sign-up-panel.tsx'));
|
||||||
export const CtaBanner = lazy(() => import('soapbox/features/ui/components/cta-banner.tsx'));
|
export const CtaBanner = lazy(() => import('soapbox/features/ui/components/cta-banner.tsx'));
|
||||||
|
|
|
@ -25,7 +25,7 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||||
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
||||||
const dsn = soapboxConfig.sentryDsn;
|
const dsn = soapboxConfig.sentryDsn;
|
||||||
|
|
||||||
const bodyClass = clsx('h-full bg-white text-base black:bg-black dark:bg-gray-800', {
|
const bodyClass = clsx('h-full bg-white text-base black:bg-black dark:bg-primary-900', {
|
||||||
'no-reduce-motion': !reduceMotion,
|
'no-reduce-motion': !reduceMotion,
|
||||||
'underline-links': underlineLinks,
|
'underline-links': underlineLinks,
|
||||||
'demetricator': demetricator,
|
'demetricator': demetricator,
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { uploadCompose } from 'soapbox/actions/compose.ts';
|
import { uploadCompose } from 'soapbox/actions/compose.ts';
|
||||||
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||||
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
||||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import Layout from 'soapbox/components/ui/layout.tsx';
|
import Layout from 'soapbox/components/ui/layout.tsx';
|
||||||
|
import Tabs from 'soapbox/components/ui/tabs.tsx';
|
||||||
import LinkFooter from 'soapbox/features/ui/components/link-footer.tsx';
|
import LinkFooter from 'soapbox/features/ui/components/link-footer.tsx';
|
||||||
import {
|
import {
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
|
@ -24,11 +25,11 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useDraggedFiles } from 'soapbox/hooks/useDraggedFiles.ts';
|
import { useDraggedFiles } from 'soapbox/hooks/useDraggedFiles.ts';
|
||||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
|
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||||
|
|
||||||
|
|
||||||
import ComposeForm from '../features/compose/components/compose-form.tsx';
|
import ComposeForm from '../features/compose/components/compose-form.tsx';
|
||||||
|
|
||||||
interface IHomePage {
|
interface IHomePage {
|
||||||
|
@ -38,11 +39,13 @@ interface IHomePage {
|
||||||
const HomePage: React.FC<IHomePage> = ({ children }) => {
|
const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
const { instance } = useInstance();
|
||||||
|
|
||||||
const composeId = 'home';
|
const composeId = 'home';
|
||||||
const composeBlock = useRef<HTMLDivElement>(null);
|
const composeBlock = useRef<HTMLDivElement>(null);
|
||||||
|
@ -61,15 +64,14 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout.Main className={clsx('black:space-y-0 dark:divide-gray-800', { 'pt-3 sm:pt-0 space-y-3': !isMobile })}>
|
<Layout.Main className={clsx('space-y-0 dark:divide-gray-800')}>
|
||||||
{me && (
|
{me && (
|
||||||
<Card
|
<Card
|
||||||
className={clsx('relative z-[1] border-gray-200 transition black:border-b black:border-gray-800 dark:border-gray-800', {
|
className={clsx('relative z-[1] border-b border-gray-200 transition black:border-gray-800 dark:border-gray-800', {
|
||||||
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
|
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
|
||||||
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
|
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
|
||||||
'border-b': isMobile,
|
'border-b': isMobile,
|
||||||
})}
|
})}
|
||||||
variant='rounded'
|
|
||||||
ref={composeBlock}
|
ref={composeBlock}
|
||||||
>
|
>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
@ -91,6 +93,16 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className='sticky top-12 z-20 bg-white/90 backdrop-blur black:bg-black/90 dark:bg-primary-900/90 lg:top-0'>
|
||||||
|
<Tabs
|
||||||
|
items={[
|
||||||
|
{ name: 'home', text: <FormattedMessage id='tabs_bar.home' defaultMessage='Home' />, to: '/' },
|
||||||
|
{ name: 'local', text: <div className='block max-w-xs truncate'>{instance.domain}</div>, to: '/timeline/local' },
|
||||||
|
]}
|
||||||
|
activeItem={pathname === '/timeline/local' ? 'local' : 'home'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{!me && (
|
{!me && (
|
||||||
|
@ -105,22 +117,22 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
{me && features.announcements && (
|
{me && features.announcements && (
|
||||||
<AnnouncementsPanel />
|
<AnnouncementsPanel />
|
||||||
)}
|
)}
|
||||||
{(hasCrypto && cryptoLimit > 0 && me) && (
|
|
||||||
<CryptoDonatePanel limit={cryptoLimit} />
|
|
||||||
)}
|
|
||||||
{(hasPatron && me) && (
|
|
||||||
<FundingPanel />
|
|
||||||
)}
|
|
||||||
{features.birthdays && (
|
|
||||||
<BirthdayPanel limit={10} />
|
|
||||||
)}
|
|
||||||
{features.trends && (
|
{features.trends && (
|
||||||
<TrendsPanel limit={5} />
|
<TrendsPanel limit={5} />
|
||||||
)}
|
)}
|
||||||
{features.suggestions && (
|
{features.suggestions && (
|
||||||
<WhoToFollowPanel limit={3} />
|
<WhoToFollowPanel limit={3} />
|
||||||
)}
|
)}
|
||||||
|
{features.birthdays && (
|
||||||
|
<BirthdayPanel limit={10} />
|
||||||
|
)}
|
||||||
<PromoPanel />
|
<PromoPanel />
|
||||||
|
{(hasCrypto && cryptoLimit > 0 && me) && (
|
||||||
|
<CryptoDonatePanel limit={cryptoLimit} />
|
||||||
|
)}
|
||||||
|
{(hasPatron && me) && (
|
||||||
|
<FundingPanel />
|
||||||
|
)}
|
||||||
<LinkFooter />
|
<LinkFooter />
|
||||||
</Layout.Aside>
|
</Layout.Aside>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Redirect, useHistory } from 'react-router-dom';
|
||||||
import { useAccountLookup } from 'soapbox/api/hooks/index.ts';
|
import { useAccountLookup } from 'soapbox/api/hooks/index.ts';
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import Layout from 'soapbox/components/ui/layout.tsx';
|
import Layout from 'soapbox/components/ui/layout.tsx';
|
||||||
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Tabs from 'soapbox/components/ui/tabs.tsx';
|
import Tabs from 'soapbox/components/ui/tabs.tsx';
|
||||||
import Header from 'soapbox/features/account/components/header.tsx';
|
import Header from 'soapbox/features/account/components/header.tsx';
|
||||||
import LinkFooter from 'soapbox/features/ui/components/link-footer.tsx';
|
import LinkFooter from 'soapbox/features/ui/components/link-footer.tsx';
|
||||||
|
@ -91,17 +92,20 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout.Main>
|
<Layout.Main>
|
||||||
<Column size='lg' label={account ? `@${getAcct(account, displayFqn)}` : ''} withHeader={false}>
|
<Column size='lg' label={account ? `@${getAcct(account, displayFqn)}` : ''} withHeader={false} slim>
|
||||||
<div className='space-y-4'>
|
<Stack space={4}>
|
||||||
<Header account={account} />
|
<Header account={account} />
|
||||||
<ProfileInfoPanel username={username} account={account} />
|
|
||||||
|
|
||||||
{account && showTabs && (
|
<Stack space={4} className='px-6'>
|
||||||
<Tabs key={`profile-tabs-${account.id}`} items={tabItems} activeItem={activeItem} />
|
<ProfileInfoPanel username={username} account={account} />
|
||||||
)}
|
|
||||||
|
{account && showTabs && (
|
||||||
|
<Tabs key={`profile-tabs-${account.id}`} items={tabItems} activeItem={activeItem} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Stack>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
{!me && (
|
{!me && (
|
||||||
|
|
Ładowanie…
Reference in New Issue