Support bubble timeline
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
7d89c34d3b
commit
48219389e4
12 changed files with 130 additions and 2 deletions
|
@ -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,
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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' />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
55
src/features/bubble-timeline/index.tsx
Normal file
55
src/features/bubble-timeline/index.tsx
Normal 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 };
|
|
@ -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} />}
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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/}
|
||||
|
|
Loading…
Reference in a new issue