Merge branch 'mobile-condensed' into 'main'

Condense feeds on mobile

See merge request soapbox-pub/soapbox!3047
environments/review-main-yi2y9f/deployments/4663
Alex Gleason 4 months ago
commit 1e9a86ee26

@ -12,6 +12,7 @@ import PullToRefresh from 'soapbox/components/pull-to-refresh';
import StatusList from 'soapbox/components/status-list';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import toast from 'soapbox/toast';
const messages = defineMessages({
@ -40,6 +41,7 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
const intl = useIntl();
const history = useHistory();
const theme = useTheme();
const isMobile = useIsMobile();
const folderId = params?.id;
@ -106,7 +108,7 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
action={
<DropdownMenu items={items} src={require('@tabler/icons/outline/dots-vertical.svg')} />
}
transparent
transparent={!isMobile}
>
<PullToRefresh onRefresh={handleRefresh}>
<StatusList
@ -117,7 +119,7 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={() => handleLoadMore(dispatch, folderId)}
emptyMessage={emptyMessage}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

@ -6,6 +6,7 @@ import { useCommunityStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useSettings, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import Timeline from '../ui/components/timeline';
@ -23,6 +24,7 @@ const CommunityTimeline = () => {
const next = useAppSelector(state => state.timelines.get('community')?.next);
const timelineId = 'community';
const isMobile = useIsMobile();
const handleLoadMore = (maxId: string) => {
dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia }));
@ -39,7 +41,7 @@ const CommunityTimeline = () => {
}, [onlyMedia]);
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent>
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent={!isMobile}>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
@ -48,7 +50,7 @@ const CommunityTimeline = () => {
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

@ -8,7 +8,7 @@ import { useAccount } from 'soapbox/api/hooks';
import Hashtag from 'soapbox/components/hashtag';
import IconButton from 'soapbox/components/icon-button';
import ScrollableList from 'soapbox/components/scrollable-list';
import { HStack, Tabs, Text } from 'soapbox/components/ui';
import { HStack, Spinner, Tabs, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import StatusContainer from 'soapbox/containers/status-container';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
@ -62,7 +62,7 @@ const SearchResults = () => {
name: 'accounts',
},
);
items.push(
{
text: intl.formatMessage(messages.hashtags),
@ -70,7 +70,7 @@ const SearchResults = () => {
name: 'hashtags',
},
);
return <Tabs items={items} activeItem={selectedFilter} />;
};
@ -172,6 +172,8 @@ const SearchResults = () => {
/>
</div>
);
} else {
noResultsMessage = <Spinner />;
}
}
@ -224,7 +226,7 @@ const SearchResults = () => {
onLoadMore={handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={clsx({
listClassName={clsx({
'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses',
})}
itemClassName={clsx({

@ -8,6 +8,7 @@ import List, { ListItem } from 'soapbox/components/list';
import { Column, Toggle } from 'soapbox/components/ui';
import Timeline from 'soapbox/features/ui/components/timeline';
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
interface IHashtagTimeline {
params?: {
@ -24,6 +25,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
const { isLoggedIn } = useLoggedIn();
const theme = useTheme();
const isMobile = useIsMobile();
const handleLoadMore = (maxId: string) => {
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
@ -50,7 +52,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
}, [id]);
return (
<Column label={`#${id}`} transparent>
<Column label={`#${id}`} transparent={!isMobile}>
{features.followHashtags && isLoggedIn && (
<List>
<ListItem
@ -69,7 +71,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
timelineId={`hashtag:${id}`}
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</Column>
);

@ -7,6 +7,7 @@ import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column, Stack, Text } from 'soapbox/components/ui';
import Timeline from 'soapbox/features/ui/components/timeline';
import { useAppSelector, useAppDispatch, useFeatures, useInstance, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
@ -20,6 +21,7 @@ const HomeTimeline: React.FC = () => {
const theme = useTheme();
const polling = useRef<NodeJS.Timeout | null>(null);
const isMobile = useIsMobile();
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
const next = useAppSelector(state => state.timelines.get('home')?.next);
@ -60,14 +62,14 @@ const HomeTimeline: React.FC = () => {
}, [isPartial]);
return (
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
<Column className='py-0' label={intl.formatMessage(messages.title)} transparent={!isMobile} withHeader={false}>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey='home_timeline'
onLoadMore={handleLoadMore}
timelineId='home'
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
showAds
emptyMessage={
<Stack space={1}>

@ -6,6 +6,7 @@ import { useCommunityStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useInstance, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import AboutPage from '../about';
import Timeline from '../ui/components/timeline';
@ -16,6 +17,7 @@ const LandingTimeline = () => {
const dispatch = useAppDispatch();
const instance = useInstance();
const theme = useTheme();
const isMobile = useIsMobile();
const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local;
const next = useAppSelector(state => state.timelines.get('community')?.next);
@ -43,7 +45,7 @@ const LandingTimeline = () => {
}, []);
return (
<Column transparent withHeader={false}>
<Column transparent={!isMobile} withHeader={false}>
<div className='my-12 mb-16 px-4 sm:mb-20'>
<SiteBanner />
</div>
@ -57,7 +59,7 @@ const LandingTimeline = () => {
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</PullToRefresh>
) : (

@ -9,6 +9,7 @@ import { useListStream } from 'soapbox/api/hooks';
import MissingIndicator from 'soapbox/components/missing-indicator';
import { Column, Button, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import Timeline from '../ui/components/timeline';
@ -16,6 +17,7 @@ const ListTimeline: React.FC = () => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id: string }>();
const theme = useTheme();
const isMobile = useIsMobile();
const list = useAppSelector((state) => state.lists.get(id));
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
@ -60,14 +62,14 @@ const ListTimeline: React.FC = () => {
);
return (
<Column label={title} transparent>
<Column label={title} transparent={!isMobile}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey='list_timeline'
timelineId={`list:${id}`}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</Column>
);

@ -8,6 +8,7 @@ import { usePublicStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Accordion, Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useInstance, useSettings, useTheme, useFeatures } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker';
import Timeline from '../ui/components/timeline';
@ -29,6 +30,7 @@ const PublicTimeline = () => {
const next = useAppSelector(state => state.timelines.get('public')?.next);
const timelineId = 'public';
const isMobile = useIsMobile();
const explanationBoxExpanded = settings.explanationBox;
const showExplanationBox = settings.showExplanationBox && !features.nostr;
@ -56,7 +58,7 @@ const PublicTimeline = () => {
}, [onlyMedia]);
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent>
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent={!isMobile}>
<PinnedHostsPicker />
{showExplanationBox && (
@ -96,7 +98,7 @@ const PublicTimeline = () => {
prefix='home'
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' />}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

@ -8,6 +8,7 @@ import { expandStatusQuotes, fetchStatusQuotes } from 'soapbox/actions/status-qu
import StatusList from 'soapbox/components/status-list';
import { Column } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
const messages = defineMessages({
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
@ -21,6 +22,7 @@ const Quotes: React.FC = () => {
const intl = useIntl();
const { statusId } = useParams<{ statusId: string }>();
const theme = useTheme();
const isMobile = useIsMobile();
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>()));
const isLoading = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'isLoading'], true));
@ -37,7 +39,7 @@ const Quotes: React.FC = () => {
const emptyMessage = <FormattedMessage id='empty_column.quotes' defaultMessage='This post has not been quoted yet.' />;
return (
<Column label={intl.formatMessage(messages.heading)} transparent>
<Column label={intl.formatMessage(messages.heading)} transparent={!isMobile}>
<StatusList
className='black:p-4 black:sm:p-5'
statusIds={statusIds as ImmutableOrderedSet<string>}
@ -47,7 +49,7 @@ const Quotes: React.FC = () => {
onLoadMore={() => handleLoadMore(statusId, dispatch)}
onRefresh={handleRefresh}
emptyMessage={emptyMessage}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</Column>
);

@ -7,6 +7,7 @@ import { useRemoteStream } from 'soapbox/api/hooks';
import IconButton from 'soapbox/components/icon-button';
import { Column, HStack, Text } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useSettings, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import Timeline from '../ui/components/timeline';
@ -32,6 +33,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
const next = useAppSelector(state => state.timelines.get('remote')?.next);
const pinned = settings.remote_timeline.pinnedHosts.includes(instance);
const isMobile = useIsMobile();
const handleCloseClick: React.MouseEventHandler = () => {
history.push('/timeline/fediverse');
@ -48,7 +50,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
}, [onlyMedia]);
return (
<Column label={instance} transparent>
<Column label={instance} transparent={!isMobile}>
{instance && <PinnedHostsPicker host={instance} />}
{!pinned && (
@ -76,7 +78,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
values={{ instance }}
/>
}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</Column>
);

@ -4,6 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { importFetchedStatuses } from 'soapbox/actions/importer';
import { expandTimelineSuccess } from 'soapbox/actions/timelines';
import { useAppDispatch, useTheme } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import { Column } from '../../components/ui';
import Timeline from '../ui/components/timeline';
@ -32,6 +33,7 @@ const TestTimeline: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const theme = useTheme();
const isMobile = useIsMobile();
React.useEffect(() => {
dispatch(importFetchedStatuses(MOCK_STATUSES));
@ -39,12 +41,12 @@ const TestTimeline: React.FC = () => {
}, []);
return (
<Column label={intl.formatMessage(messages.title)} transparent>
<Column label={intl.formatMessage(messages.title)} transparent={!isMobile}>
<Timeline
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
divideType={theme === 'black' ? 'border' : 'space'}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</Column>
);

@ -11,6 +11,7 @@ import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search';
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import { isStandalone } from 'soapbox/utils/state';
import ProfileDropdown from './profile-dropdown';
@ -33,6 +34,7 @@ const Navbar = () => {
const { isOpen } = useRegistrationStatus();
const { account } = useOwnAccount();
const node = useRef(null);
const isMobile = useIsMobile();
const [isLoading, setLoading] = useState<boolean>(false);
const [username, setUsername] = useState<string>('');
@ -72,7 +74,14 @@ const Navbar = () => {
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
return (
<nav className='sticky top-0 z-50 bg-white shadow black:border-b black:border-b-gray-800 black:bg-black dark:bg-primary-900' ref={node} data-testid='navbar'>
<nav
className={clsx(
'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}
data-testid='navbar'
>
<div className='mx-auto max-w-7xl px-2 sm:px-6 lg:px-8'>
<div className='relative flex h-12 justify-between lg:h-16'>
{account && (

@ -0,0 +1,6 @@
import { useScreenWidth } from './useScreenWidth';
export function useIsMobile() {
const screenWidth = useScreenWidth();
return screenWidth <= 581;
}

@ -0,0 +1,19 @@
import { useState, useEffect } from 'react';
export function useScreenWidth() {
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
useEffect(() => {
const checkWindowSize = () => {
setScreenWidth(window.innerWidth);
};
window.addEventListener('resize', checkWindowSize);
return () => {
window.removeEventListener('resize', checkWindowSize);
};
}, []);
return screenWidth;
}

@ -17,6 +17,7 @@ import {
AnnouncementsPanel,
} from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig, useDraggedFiles, useAppDispatch } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import { Avatar, Card, CardBody, HStack, Layout } from '../components/ui';
import ComposeForm from '../features/compose/components/compose-form';
@ -36,6 +37,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
const composeId = 'home';
const composeBlock = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile();
const hasPatron = soapboxConfig.extensions.getIn(['patron', 'enabled']) === true;
const hasCrypto = typeof soapboxConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
@ -50,12 +52,13 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
return (
<>
<Layout.Main className='space-y-3 pt-3 black:space-y-0 sm:pt-0 dark:divide-gray-800'>
<Layout.Main className={clsx('black:space-y-0 dark:divide-gray-800', { 'pt-3 sm:pt-0 space-y-3': !isMobile })}>
{me && (
<Card
className={clsx('relative z-[1] transition black:border-b black:border-gray-800', {
className={clsx('relative z-[1] border-gray-200 transition black:border-b black:border-gray-800 dark:border-gray-800', {
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
'border-b': isMobile,
})}
variant='rounded'
ref={composeBlock}

Loading…
Cancel
Save