diff --git a/src/actions/timelines.ts b/src/actions/timelines.ts index f5007a5b7..77e14dcca 100644 --- a/src/actions/timelines.ts +++ b/src/actions/timelines.ts @@ -242,6 +242,9 @@ const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Recor const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record = {}, 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 = {}, 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 = {}, 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, diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx index bcdc8f1b5..4ad63d01d 100644 --- a/src/components/sidebar-menu.tsx +++ b/src/components/sidebar-menu.tsx @@ -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(null); const closeButtonRef = React.useRef(null); @@ -297,6 +300,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { onClick={onClose} /> )} + + {features.bubbleTimeline && ( + } + onClick={onClose} + /> + )} } @@ -368,6 +380,35 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { ) : ( + {features.publicTimeline && !restrictUnauth.timelines.local && <> + : } + onClick={onClose} + /> + + {features.federating && !restrictUnauth.timelines.federated && ( + } + onClick={onClose} + /> + )} + + {features.bubbleTimeline && !restrictUnauth.timelines.bubble && ( + } + onClick={onClose} + /> + )} + + + } + { text={} /> )} + + {(features.bubbleTimeline && (account || !restrictUnauth.timelines.bubble)) && ( + } + /> + )} )} diff --git a/src/features/bubble-timeline/index.tsx b/src/features/bubble-timeline/index.tsx new file mode 100644 index 000000000..f42592d27 --- /dev/null +++ b/src/features/bubble-timeline/index.tsx @@ -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 ( + + + } + divideType={(theme === 'black' || isMobile) ? 'border' : 'space'} + /> + + + ); +}; + +export { BubbleTimeline as default }; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 4d701df01..b103da78d 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -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 = ({ children }) => */} {features.federating && } {features.federating && } + {features.bubbleTimeline && } {features.federating && } {features.conversations && } diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index ed5efbc5e..547ca879b 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -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')); diff --git a/src/locales/en.json b/src/locales/en.json index 214f0e707..cf2980d80 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/pl.json b/src/locales/pl.json index 042278df3..1f016e857 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -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", diff --git a/src/reducers/timelines.ts b/src/reducers/timelines.ts index 62205dbb6..5cb84d1ad 100644 --- a/src/reducers/timelines.ts +++ b/src/reducers/timelines.ts @@ -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']; } diff --git a/src/schemas/instance.ts b/src/schemas/instance.ts index ddb8688b5..81345488f 100644 --- a/src/schemas/instance.ts +++ b/src/schemas/instance.ts @@ -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), }), diff --git a/src/schemas/soapbox/settings.ts b/src/schemas/soapbox/settings.ts index ab77e7e34..12e3074ce 100644 --- a/src/schemas/soapbox/settings.ts +++ b/src/schemas/soapbox/settings.ts @@ -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'), diff --git a/src/utils/features.ts b/src/utils/features.ts index 56c3c1546..4a991e9fd 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -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/}