Support bubble timeline

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-07-23 14:18:58 +02:00
parent 7d89c34d3b
commit 48219389e4
12 changed files with 130 additions and 2 deletions

View file

@ -242,6 +242,9 @@ const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Recor
const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, intl?: IntlShape, done = noOp) =>
expandTimeline(`community${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { local: true, max_id: maxId, only_media: !!onlyMedia }, intl, done);
const expandBubbleTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, intl?: IntlShape, done = noOp) =>
expandTimeline(`bubble${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/bubble', url ? {} : { max_id: maxId, only_media: !!onlyMedia }, intl, done);
const expandDirectTimeline = ({ url, maxId }: Record<string, any> = {}, intl?: IntlShape, done = noOp) =>
expandTimeline('direct', url || '/api/v1/timelines/direct', url ? {} : { max_id: maxId }, intl, done);
@ -354,6 +357,7 @@ export {
expandPublicTimeline,
expandRemoteTimeline,
expandCommunityTimeline,
expandBubbleTimeline,
expandDirectTimeline,
expandAccountTimeline,
expandAccountFeaturedTimeline,

View file

@ -11,7 +11,7 @@ import { useAccount } from 'soapbox/api/hooks';
import Account from 'soapbox/components/account';
import { Stack, Divider, HStack, Icon, IconButton, Text } from 'soapbox/components/ui';
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
import { makeGetOtherAccounts } from 'soapbox/selectors';
import type { List as ImmutableList } from 'immutable';
@ -87,6 +87,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
const instance = useInstance();
const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated;
const containerRef = React.useRef<HTMLDivElement>(null);
const closeButtonRef = React.useRef(null);
@ -297,6 +300,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
onClick={onClose}
/>
)}
{features.bubbleTimeline && (
<SidebarLink
to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
onClick={onClose}
/>
)}
</>}
<Divider />
@ -368,6 +380,35 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
</Stack>
) : (
<Stack space={4}>
{features.publicTimeline && !restrictUnauth.timelines.local && <>
<SidebarLink
to='/timeline/local'
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
onClick={onClose}
/>
{features.federating && !restrictUnauth.timelines.federated && (
<SidebarLink
to='/timeline/fediverse'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
onClick={onClose}
/>
)}
{features.bubbleTimeline && !restrictUnauth.timelines.bubble && (
<SidebarLink
to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
onClick={onClose}
/>
)}
<Divider />
</>}
<SidebarLink
to='/login'
icon={require('@tabler/icons/outline/login.svg')}

View file

@ -241,6 +241,14 @@ const SidebarNavigation = () => {
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
/>
)}
{(features.bubbleTimeline && (account || !restrictUnauth.timelines.bubble)) && (
<SidebarNavigationLink
to='/timeline/bubble'
icon={require('@tabler/icons/outline/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
/>
)}
</>
)}

View file

@ -0,0 +1,55 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { expandBubbleTimeline } from 'soapbox/actions/timelines';
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';
const messages = defineMessages({
title: { id: 'column.bubble', defaultMessage: 'Bubble timeline' },
});
const BubbleTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const theme = useTheme();
const settings = useSettings();
const onlyMedia = settings.bubble.other.onlyMedia;
const next = useAppSelector(state => state.timelines.get('bubble')?.next);
const timelineId = 'bubble';
const isMobile = useIsMobile();
const handleLoadMore = (maxId: string) => {
dispatch(expandBubbleTimeline({ url: next, maxId, onlyMedia }, intl));
};
const handleRefresh = () => dispatch(expandBubbleTimeline({ onlyMedia }, intl));
useEffect(() => {
dispatch(expandBubbleTimeline({ onlyMedia }, intl));
}, [onlyMedia]);
return (
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent={!isMobile}>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-0 black:sm:p-4'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.bubble' defaultMessage='There is nothing here! Write something publicly to fill it up' />}
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>
);
};
export { BubbleTimeline as default };

View file

@ -130,6 +130,7 @@ import {
Rules,
DraftStatuses,
Circle,
BubbleTimeline,
} from './util/async-components';
import GlobalHotkeys from './util/global-hotkeys';
import { WrappedRoute } from './util/react-router-helpers';
@ -175,6 +176,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
*/}
{features.federating && <WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} publicRoute />}
{features.federating && <WrappedRoute path='/timeline/fediverse' exact page={HomePage} component={PublicTimeline} content={children} publicRoute />}
{features.bubbleTimeline && <WrappedRoute path='/timeline/bubble' exact page={HomePage} component={BubbleTimeline} content={children} publicRoute />}
{features.federating && <WrappedRoute path='/timeline/:instance' exact page={RemoteInstancePage} component={RemoteTimeline} content={children} />}
{features.conversations && <WrappedRoute path='/conversations' page={DefaultPage} component={Conversations} content={children} />}

View file

@ -160,3 +160,4 @@ export const Rules = lazy(() => import('soapbox/features/admin/rules'));
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));
export const DraftStatuses = lazy(() => import('soapbox/features/draft-statuses'));
export const Circle = lazy(() => import('soapbox/features/circle'));
export const BubbleTimeline = lazy(() => import('soapbox/features/bubble-timeline'));

View file

@ -328,6 +328,7 @@
"column.birthdays": "Birthdays",
"column.blocks": "Blocks",
"column.bookmarks": "Bookmarks",
"column.bubble": "Bubble timeline",
"column.chats": "Chats",
"column.circle": "Interactions circle",
"column.community": "Local timeline",
@ -700,6 +701,7 @@
"empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.",
"empty_column.bookmarks.folder": "You don't have any bookmarks in this folder yet. When you add one, it will show up here.",
"empty_column.bubble": "There is nothing here! Write something publicly to fill it up",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.dislikes": "No one has disliked this post yet. When someone does, they will show up here.",
@ -1484,6 +1486,7 @@
"sw.status": "Status",
"sw.url": "Script URL",
"tabs_bar.all": "All",
"tabs_bar.bubble": "Bubble",
"tabs_bar.dashboard": "Dashboard",
"tabs_bar.fediverse": "Fediverse",
"tabs_bar.groups": "Groups",

View file

@ -328,6 +328,7 @@
"column.birthdays": "Urodziny",
"column.blocks": "Zablokowani użytkownicy",
"column.bookmarks": "Zakładki",
"column.bubble": "Oś czasu bańki",
"column.chats": "Rozmowy",
"column.circle": "Koło interakcji",
"column.community": "Lokalna oś czasu",
@ -1481,6 +1482,7 @@
"sw.status": "Stan",
"sw.url": "Adres URL skryptu",
"tabs_bar.all": "Wszystkie",
"tabs_bar.bubble": "Bańka",
"tabs_bar.dashboard": "Panel administracyjny",
"tabs_bar.fediverse": "Fediwersum",
"tabs_bar.groups": "Grupy",

View file

@ -250,7 +250,7 @@ const getTimelinesForStatus = (status: APIEntity) => {
case 'direct':
return ['direct'];
case 'public':
return ['home', 'community', 'public'];
return ['home', 'community', 'public', 'bubble'];
default:
return ['home'];
}

View file

@ -121,6 +121,7 @@ const pleromaSchema = coerceObject({
remote: z.boolean().catch(false),
}),
timelines: coerceObject({
bubble: z.boolean().catch(false),
federated: z.boolean().catch(false),
local: z.boolean().catch(false),
}),

View file

@ -65,6 +65,11 @@ const settingsSchema = z.object({
onlyMedia: z.boolean().catch(false),
}),
}),
bubble: coerceObject({
other: coerceObject({
onlyMedia: z.boolean().catch(false),
}),
}),
notifications: coerceObject({
quickFilter: coerceObject({
active: z.string().catch('all'),

View file

@ -265,6 +265,12 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === GOTOSOCIAL,
]),
/**
* Can display a timeline of statuses from instances selected by instance admin.
* @see GET /api/v1/timelines/bubble
*/
bubbleTimeline: features.includes('bubble_timeline'),
/**
* Pleroma chats API.
* @see {@link https://docs.pleroma.social/backend/development/API/chats/}