Skip to content

Commit c000a22

Browse files
authored
feat: add timeline settings (#410)
1 parent 117a2e0 commit c000a22

File tree

10 files changed

+215
-111
lines changed

10 files changed

+215
-111
lines changed

app/soapbox/actions/timelines.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const replaceHomeTimeline = (
152152
}));
153153
};
154154

155-
const expandTimeline = (timelineId: string, path: string, params: Record<string, any> = {}, done = noOp) =>
155+
const expandTimeline = (timelineId: string, path: string, params: Partial<{ all: any[], none: any[], instance: string, any: any[], since_id: string, max_id: string, exclude_replies: boolean, with_muted: boolean, only_media: boolean, local: boolean, pinned: boolean, limit: number }>, done = noOp) =>
156156
(dispatch: AppDispatch, getState: () => RootState) => {
157157
const timeline = getState().timelines.get(timelineId) || {} as Record<string, any>;
158158
const isLoadingMore = !!params.max_id;
@@ -192,17 +192,17 @@ const expandHomeTimeline = ({ accountId, maxId }: Record<string, any> = {}, done
192192
return expandTimeline('home', endpoint, params, done);
193193
};
194194

195-
const expandPublicTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
196-
expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
195+
const expandPublicTimeline = ({ maxId, onlyMedia, excludeReplies }: Record<string, any> = {}, done = noOp) =>
196+
expandTimeline(`public${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia, exclude_replies: excludeReplies }, done);
197197

198-
const expandBubbleTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
199-
expandTimeline(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/bubble', { max_id: maxId, only_media: !!onlyMedia }, done);
198+
const expandBubbleTimeline = ({ maxId, onlyMedia, excludeReplies }: Record<string, any> = {}, done = noOp) =>
199+
expandTimeline(`bubble${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`, '/api/v1/timelines/bubble', { max_id: maxId, only_media: !!onlyMedia, exclude_replies: excludeReplies }, done);
200200

201-
const expandRemoteTimeline = (instance: string, { maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
202-
expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
201+
const expandRemoteTimeline = (instance: string, { maxId, onlyMedia, excludeReplies }: Record<string, any> = {}, done = noOp) =>
202+
expandTimeline(`remote:${instance}${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia, exclude_replies: excludeReplies }, done);
203203

204-
const expandCommunityTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
205-
expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
204+
const expandCommunityTimeline = ({ maxId, onlyMedia, excludeReplies }: Record<string, any> = {}, done = noOp) =>
205+
expandTimeline(`community${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia, exclude_replies: excludeReplies }, done);
206206

207207
const expandDirectTimeline = ({ maxId }: Record<string, any> = {}, done = noOp) =>
208208
expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);

app/soapbox/components/list.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import { v4 as uuidv4 } from 'uuid';
44

55
import Icon from './icon';
66

7-
const List: React.FC = ({ children }) => (
8-
<div className='space-y-0.5'>{children}</div>
7+
const List = ({ children, className = '' }: { children: React.ReactNode, className?: string}) => (
8+
<div className={`${className} space-y-0.5`} >{children}</div>
99
);
1010

1111
interface IListItem {
1212
label: React.ReactNode,
1313
hint?: React.ReactNode,
1414
onClick?: () => void,
15+
className?: string,
1516
}
1617

17-
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
18+
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, className }) => {
1819
const id = uuidv4();
1920
const domId = `list-group-${id}`;
2021

@@ -39,6 +40,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
3940
className={classNames({
4041
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10 dark:from-slate-900/25 dark:to-slate-900/50': true,
4142
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-slate-900/40 dark:hover:to-slate-900/75': typeof onClick !== 'undefined',
43+
className,
4244
})}
4345
{...linkProps}
4446
>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { useCallback } from 'react';
2+
import { FormattedMessage } from 'react-intl';
3+
import { useDispatch } from 'react-redux';
4+
5+
import { changeSetting } from 'soapbox/actions/settings';
6+
import SettingToggle from 'soapbox/features/notifications/components/setting_toggle';
7+
import { useSettings } from 'soapbox/hooks';
8+
9+
import List, { ListItem } from './list';
10+
11+
type ITimelineSettings = {
12+
onClose: () => void
13+
timeline: string,
14+
className?: string,
15+
}
16+
17+
function TimelineSettings({ timeline, onClose, className = '' }: ITimelineSettings) {
18+
const settings = useSettings();
19+
const dispatch = useDispatch();
20+
21+
const onChange = useCallback((key: string[], checked: boolean) => {
22+
dispatch(changeSetting(key, checked));
23+
}, [dispatch]);
24+
25+
return (
26+
<List className={className}>
27+
<ListItem
28+
label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />}
29+
>
30+
<SettingToggle settings={settings} settingPath={[timeline, 'shows', 'reply']} onChange={onChange} />
31+
</ListItem>
32+
<ListItem
33+
label={<FormattedMessage id='home.column_settings.only_media' defaultMessage='Show only media' />}
34+
>
35+
<SettingToggle settings={settings} settingPath={[timeline, 'other', 'onlyMedia']} onChange={onChange} />
36+
</ListItem>
37+
</List>
38+
);
39+
}
40+
41+
export default TimelineSettings;

app/soapbox/features/bubble_timeline/index.tsx

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
33
import { Link } from 'react-router-dom';
44

55
import { changeSetting } from 'soapbox/actions/settings';
66
import { expandBubbleTimeline } from 'soapbox/actions/timelines';
77
import PullToRefresh from 'soapbox/components/pull-to-refresh';
88
import SubNavigation from 'soapbox/components/sub_navigation';
9-
import { Button, Column, Text } from 'soapbox/components/ui';
9+
import TimelineSettings from 'soapbox/components/timeline_settings';
10+
import { Button, Column, IconButton, Text } from 'soapbox/components/ui';
1011
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
1112

1213
import Timeline from '../ui/components/timeline';
1314

1415
const messages = defineMessages({
1516
title: { id: 'tabs_bar.bubble', defaultMessage: 'Featured' },
17+
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
1618
});
1719

1820
const BubbleTimeline = () => {
1921
const intl = useIntl();
2022
const dispatch = useAppDispatch();
2123

24+
const [showSettings, setShowSettings] = useState(false);
2225
const settings = useSettings();
23-
const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']);
26+
const onlyMedia = settings.getIn(['bubble', 'other', 'onlyMedia']);
27+
const excludeReplies = settings.getIn(['bubble', 'shows', 'reply']);
2428

2529
const siteTitle = useAppSelector((state) => state.instance.title);
2630
const showExplanationBox = settings.get('showExplanationBox');
@@ -30,62 +34,72 @@ const BubbleTimeline = () => {
3034
}, [dispatch]);
3135

3236
const handleLoadMore = React.useCallback((maxId: string) => {
33-
dispatch(expandBubbleTimeline({ maxId, onlyMedia }));
34-
}, [dispatch, onlyMedia]);
37+
dispatch(expandBubbleTimeline({ maxId, onlyMedia, excludeReplies }));
38+
}, [dispatch, excludeReplies, onlyMedia]);
3539

3640
const handleRefresh = React.useCallback(() => {
37-
return dispatch(expandBubbleTimeline({ onlyMedia } as any));
38-
}, [dispatch, onlyMedia]);
41+
return dispatch(expandBubbleTimeline({ onlyMedia, excludeReplies } as any));
42+
}, [dispatch, excludeReplies, onlyMedia]);
3943

4044
useEffect(() => {
41-
dispatch(expandBubbleTimeline({ onlyMedia } as any));
45+
dispatch(expandBubbleTimeline({ onlyMedia, excludeReplies } as any));
4246
// bubble timeline doesnt have streaming for now
43-
}, [onlyMedia]);
47+
}, [dispatch, excludeReplies, onlyMedia]);
4448

4549
return (
46-
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
47-
<div className='px-4 pt-4 sm:p-0'>
48-
<SubNavigation message={intl.formatMessage(messages.title)} />
49-
</div>
50-
{showExplanationBox && <div className='mb-4'>
51-
<Text size='lg' weight='bold' className='mb-2'>
52-
<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />
53-
</Text>
54-
<FormattedMessage
55-
id='fediverse_tab.explanation_box.explanation'
56-
defaultMessage='{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka "servers"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don&apos;t like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.'
57-
values={{
58-
site_title: siteTitle,
59-
local: (
60-
<Link to='/timeline/local'>
61-
<FormattedMessage
62-
id='empty_column.home.local_tab'
63-
defaultMessage='the {site_title} tab'
64-
values={{ site_title: siteTitle }}
65-
/>
66-
</Link>
67-
),
68-
}}
69-
/>
70-
<p className='mt-2'>
71-
<FormattedMessage id='fediverse_tab.explanation_box.bubble' defaultMessage='This timeline shows you all the statuses published on a selection of other instances curated by your moderators.' />
72-
</p>
73-
<div className='text-right'>
74-
<Button theme='link' onClick={dismissExplanationBox}>
75-
<FormattedMessage id='fediverse_tab.explanation_box.dismiss' defaultMessage="Don\'t show again" />
76-
</Button>
50+
<>
51+
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
52+
<div className='px-4 pt-4 sm:p-0 flex justify-between'>
53+
<SubNavigation message={intl.formatMessage(messages.title)} />
54+
<IconButton
55+
src={!showSettings ? require('@tabler/icons/chevron-down.svg') : require('@tabler/icons/chevron-up.svg')}
56+
onClick={() => setShowSettings(!showSettings)}
57+
title={intl.formatMessage(messages.settings)}
58+
/>
7759
</div>
78-
</div>}
79-
<PullToRefresh onRefresh={handleRefresh}>
80-
<Timeline
81-
scrollKey={'bubble_timeline'}
82-
timelineId={`bubble${onlyMedia ? ':media' : ''}`}
83-
onLoadMore={handleLoadMore}
84-
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' />}
85-
divideType='space'
86-
/>
87-
</PullToRefresh>
88-
</Column>
60+
{
61+
showSettings && <TimelineSettings className='mb-3' timeline='bubble' onClose={() => setShowSettings(false)} />
62+
}
63+
{showExplanationBox && <div className='mb-4'>
64+
<Text size='lg' weight='bold' className='mb-2'>
65+
<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />
66+
</Text>
67+
<FormattedMessage
68+
id='fediverse_tab.explanation_box.explanation'
69+
defaultMessage='{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka "servers"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don&apos;t like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.'
70+
values={{
71+
site_title: siteTitle,
72+
local: (
73+
<Link to='/timeline/local'>
74+
<FormattedMessage
75+
id='empty_column.home.local_tab'
76+
defaultMessage='the {site_title} tab'
77+
values={{ site_title: siteTitle }}
78+
/>
79+
</Link>
80+
),
81+
}}
82+
/>
83+
<p className='mt-2'>
84+
<FormattedMessage id='fediverse_tab.explanation_box.bubble' defaultMessage='This timeline shows you all the statuses published on a selection of other instances curated by your moderators.' />
85+
</p>
86+
<div className='text-right'>
87+
<Button theme='link' onClick={dismissExplanationBox}>
88+
<FormattedMessage id='fediverse_tab.explanation_box.dismiss' defaultMessage="Don\'t show again" />
89+
</Button>
90+
</div>
91+
</div>}
92+
<PullToRefresh onRefresh={handleRefresh}>
93+
<Timeline
94+
scrollKey={'bubble_timeline'}
95+
timelineId={`bubble${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`}
96+
onLoadMore={handleLoadMore}
97+
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' />}
98+
divideType='space'
99+
/>
100+
</PullToRefresh>
101+
</Column>
102+
</>
89103
);
90104
};
91105

app/soapbox/features/community_timeline/index.tsx

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,77 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
33

44
import { connectCommunityStream } from 'soapbox/actions/streaming';
55
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
66
import PullToRefresh from 'soapbox/components/pull-to-refresh';
77
import SubNavigation from 'soapbox/components/sub_navigation';
8-
import { Column } from 'soapbox/components/ui';
8+
import TimelineSettings from 'soapbox/components/timeline_settings';
9+
import { Column, IconButton } from 'soapbox/components/ui';
910
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
1011

1112
import Timeline from '../ui/components/timeline';
1213

1314
const messages = defineMessages({
1415
title: { id: 'column.community', defaultMessage: 'Local timeline' },
16+
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
1517
});
1618

1719
const CommunityTimeline = () => {
1820
const intl = useIntl();
1921
const dispatch = useAppDispatch();
2022

2123
const instance = useAppSelector((state) => state.instance);
24+
const timelineId = 'community';
25+
26+
const [showSettings, setShowSettings] = useState(false);
2227
const settings = useSettings();
23-
const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']);
28+
const onlyMedia = settings.getIn([timelineId, 'other', 'onlyMedia']);
29+
const excludeReplies = settings.getIn([timelineId, 'shows', 'reply']);
30+
2431

25-
const timelineId = 'community';
2632

2733
const handleLoadMore = (maxId: string) => {
28-
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
34+
dispatch(expandCommunityTimeline({ maxId, onlyMedia, excludeReplies }));
2935
};
3036

3137
const handleRefresh = () => {
32-
return dispatch(expandCommunityTimeline({ onlyMedia } as any));
38+
return dispatch(expandCommunityTimeline({ onlyMedia, excludeReplies }));
3339
};
3440

3541
useEffect(() => {
36-
dispatch(expandCommunityTimeline({ onlyMedia } as any));
37-
const disconnect = dispatch(connectCommunityStream({ onlyMedia } as any));
42+
dispatch(expandCommunityTimeline({ onlyMedia, excludeReplies }));
43+
const disconnect = dispatch(connectCommunityStream({ onlyMedia, excludeReplies }));
3844

3945
return () => {
4046
disconnect();
4147
};
42-
}, [onlyMedia]);
48+
}, [dispatch, excludeReplies, onlyMedia]);
4349

4450
return (
45-
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
46-
<div className='px-4 pt-4 sm:p-0'>
47-
<SubNavigation message={instance.title} />
48-
</div>
49-
<PullToRefresh onRefresh={handleRefresh}>
50-
<Timeline
51-
scrollKey={`${timelineId}_timeline`}
52-
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
53-
onLoadMore={handleLoadMore}
54-
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
55-
divideType='space'
56-
/>
57-
</PullToRefresh>
58-
</Column>
51+
<>
52+
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
53+
<div className='px-4 pt-4 sm:p-0 flex justify-between'>
54+
<SubNavigation message={instance.title} />
55+
<IconButton
56+
src={!showSettings ? require('@tabler/icons/chevron-down.svg') : require('@tabler/icons/chevron-up.svg')}
57+
onClick={() => setShowSettings(!showSettings)}
58+
title={intl.formatMessage(messages.settings)}
59+
/>
60+
</div>
61+
{
62+
showSettings && <TimelineSettings className='mb-3' timeline={timelineId} onClose={() => setShowSettings(false)} />
63+
}
64+
<PullToRefresh onRefresh={handleRefresh}>
65+
<Timeline
66+
scrollKey={`${timelineId}_timeline`}
67+
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}${excludeReplies ? ':exclude_replies' : ''}`}
68+
onLoadMore={handleLoadMore}
69+
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
70+
divideType='space'
71+
/>
72+
</PullToRefresh>
73+
</Column>
74+
</>
5975
);
6076
};
6177

0 commit comments

Comments
 (0)