diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 7b006c1be7..237b85d91e 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -22,7 +22,7 @@ import { fillHomeTimelineGaps, fillPublicTimelineGaps, fillCommunityTimelineGaps, - fillListTimelineGaps, + fillListTimelineGaps, fillBubbleTimelineGaps } from './timelines'; /** @@ -157,6 +157,12 @@ export const connectUserStream = () => export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); +/** + * @returns {function(): void} + */ +export const connectBubbleStream = () => + connectTimelineStream(`bubble`, `public:bubble`, {}, { fillGaps: () => (fillBubbleTimelineGaps()) }); + /** * @param {Object} options * @param {boolean} [options.onlyMedia] diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index eb5050f152..d379e17fb0 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -165,6 +165,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandBubbleTimeline = ({ maxId } = {}, done = noOp) => expandTimeline(`bubble`, '/api/v1/timelines/bubble', { local: true, max_id: maxId }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); @@ -184,6 +185,7 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done); export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done); export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done); +export const fillBubbleTimelineGaps = ({} = {}, done = noOp) => fillTimelineGaps(`bubble`, '/api/v1/timelines/bubble', { local: true }, done); export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done); export function expandTimelineRequest(timeline, isLoadingMore) { diff --git a/app/javascript/flavours/glitch/features/bubble_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/bubble_timeline/components/column_settings.jsx new file mode 100644 index 0000000000..e1aab76612 --- /dev/null +++ b/app/javascript/flavours/glitch/features/bubble_timeline/components/column_settings.jsx @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import SettingText from 'flavours/glitch/components/setting_text'; +import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; + +const messages = defineMessages({ + filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, + settings: { id: 'home.settings', defaultMessage: 'Column settings' }, +}); + +class ColumnSettings extends PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + }; + + render () { + const { settings, onChange, intl } = this.props; + + return ( +
+ + +
+ +
+
+ ); + } + +} + +export default injectIntl(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/bubble_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/bubble_timeline/containers/column_settings_container.js new file mode 100644 index 0000000000..95e589df8d --- /dev/null +++ b/app/javascript/flavours/glitch/features/bubble_timeline/containers/column_settings_container.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; + +import { changeColumnParams } from '../../../actions/columns'; +import { changeSetting } from '../../../actions/settings'; +import ColumnSettings from '../components/column_settings'; + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + + return { + settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'bubble']), + }; +}; + +const mapDispatchToProps = (dispatch, { columnId }) => { + return { + onChange (key, checked) { + if (columnId) { + dispatch(changeColumnParams(columnId, key, checked)); + } else { + dispatch(changeSetting(['bubble', ...key], checked)); + } + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/bubble_timeline/index.jsx b/app/javascript/flavours/glitch/features/bubble_timeline/index.jsx new file mode 100644 index 0000000000..ca6ec6ae43 --- /dev/null +++ b/app/javascript/flavours/glitch/features/bubble_timeline/index.jsx @@ -0,0 +1,160 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { connect } from 'react-redux'; + +import BubbleChartIcon from '@/material-icons/400-24px/bubble-chart.svg?react'; +import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; +import { domain } from 'flavours/glitch/initial_state'; + +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { connectBubbleStream } from '../../actions/streaming'; +import { expandBubbleTimeline } from '../../actions/timelines'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import StatusListContainer from '../ui/containers/status_list_container'; + +import ColumnSettingsContainer from './containers/column_settings_container'; + +const messages = defineMessages({ + title: { id: 'column.bubble', defaultMessage: 'Bubble timeline' }, +}); + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + const regex = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'regex', 'body']) : state.getIn(['settings', 'bubble', 'regex', 'body']); + const timelineState = state.getIn(['timelines', `bubble`]); + + return { + hasUnread: !!timelineState && timelineState.get('unread') > 0, + regex, + }; +}; + +class BubbleTimeline extends PureComponent { + static defaultProps = {}; + + static propTypes = { + identity: identityContextPropShape, + dispatch: PropTypes.func.isRequired, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + hasUnread: PropTypes.bool, + multiColumn: PropTypes.bool, + regex: PropTypes.string, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('BUBBLE')); + } + }; + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + }; + + handleHeaderClick = () => { + this.column.scrollTop(); + }; + + componentDidMount () { + const { dispatch } = this.props; + const { signedIn } = this.props.identity; + + dispatch(expandBubbleTimeline()); + + if (signedIn) { + this.disconnect = dispatch(connectBubbleStream()); + } + } + + componentDidUpdate (prevProps) { + const { signedIn } = this.props.identity; + + if (prevProps !== this.props) { + const { dispatch } = this.props; + + if (this.disconnect) { + this.disconnect(); + } + + dispatch(expandBubbleTimeline()); + + if (signedIn) { + this.disconnect = dispatch(connectBubbleStream()); + } + } + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + setRef = c => { + this.column = c; + }; + + handleLoadMore = maxId => { + const { dispatch } = this.props; + + dispatch(expandBubbleTimeline({ maxId })); + }; + + render () { + const { intl, hasUnread, columnId, multiColumn } = this.props; + const pinned = !!columnId; + + return ( + + + + + + } + trackScroll={!pinned} + scrollKey={`bubble_timeline-${columnId}`} + timelineId={`bubble`} + onLoadMore={this.handleLoadMore} + emptyMessage={} + bindToDocument={!multiColumn} + regex={this.props.regex} + /> + + + {intl.formatMessage(messages.title)} + + + + ); + } + +} + +export default withIdentity(connect(mapStateToProps)(injectIntl(BubbleTimeline))); diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx index 2a6d790d52..1aa0455390 100644 --- a/app/javascript/flavours/glitch/features/firehose/index.jsx +++ b/app/javascript/flavours/glitch/features/firehose/index.jsx @@ -10,8 +10,8 @@ import { useIdentity } from '@/flavours/glitch/identity_context'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import { addColumn } from 'flavours/glitch/actions/columns'; import { changeSetting } from 'flavours/glitch/actions/settings'; -import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming'; -import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; +import { connectPublicStream, connectCommunityStream, connectBubbleStream } from 'flavours/glitch/actions/streaming'; +import { expandPublicTimeline, expandCommunityTimeline, expandBubbleTimeline } from 'flavours/glitch/actions/timelines'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; import SettingText from 'flavours/glitch/components/setting_text'; import { domain } from 'flavours/glitch/initial_state'; @@ -90,6 +90,9 @@ const Firehose = ({ feedType, multiColumn }) => { case 'community': dispatch(addColumn('COMMUNITY', { other: { onlyMedia }, regex: { body: regex } })); break; + case 'bubble': + dispatch(addColumn('BUBBLE', { other: { onlyMedia }, regex: { body: regex } })); + break; case 'public': dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } })); break; @@ -107,6 +110,9 @@ const Firehose = ({ feedType, multiColumn }) => { case 'community': dispatch(expandCommunityTimeline({ maxId, onlyMedia })); break; + case 'bubble': + dispatch(expandBubbleTimeline({ maxId, onlyMedia })); + break; case 'public': dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly })); break; @@ -130,6 +136,12 @@ const Firehose = ({ feedType, multiColumn }) => { disconnect = dispatch(connectCommunityStream({ onlyMedia })); } break; + case 'bubble': + dispatch(expandBubbleTimeline()); + if (signedIn) { + disconnect = dispatch(connectBubbleStream()); + } + break; case 'public': dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly })); if (signedIn) { @@ -147,6 +159,7 @@ const Firehose = ({ feedType, multiColumn }) => { return () => disconnect?.(); }, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]); + // todo bubble const prependBanner = feedType === 'community' ? ( { ); - const emptyMessage = feedType === 'community' ? ( + const emptyMessage = feedType === 'community' || feedType === 'bubble' ? ( { + + + + diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 2d13d3d584..994df4548e 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; +import BubbleChartIcon from '@/material-icons/400-24px/bubble-chart.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; @@ -47,6 +48,7 @@ const messages = defineMessages({ navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' }, settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, + bubble_timeline: { id: 'navigation_bar.bubble_timeline', defaultMessage: 'Bubble timeline' }, explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, @@ -149,6 +151,10 @@ class GettingStarted extends ImmutablePureComponent { navItems.push(); } + if (!columns.find(item => item.get('id') === 'BUBBLE')) { + navItems.push(); + } + if (!columns.find(item => item.get('id') === 'PUBLIC')) { navItems.push(); } diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index b27b19c533..4c4e5c4918 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -210,6 +210,7 @@ class SwitchingColumnsArea extends PureComponent { + diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index e334e1a3b6..01a0620db7 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -30,6 +30,10 @@ export function CommunityTimeline () { return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'../../community_timeline'); } +export function BubbleTimeline () { + return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'../../bubble_timeline'); +} + export function Firehose () { return import(/* webpackChunkName: "flavours/glitch/async/firehose" */'../../firehose'); } diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index e6a6f50df5..1e33694ce3 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -9,12 +9,12 @@ const initialState = ImmutableMap({ stretch : true, side_arm : 'none', side_arm_reply_mode : 'keep', - show_reply_count : false, - always_show_spoilers_field: false, + show_reply_count : true, + always_show_spoilers_field: true, confirm_missing_media_description: false, confirm_boost_missing_media_description: false, confirm_before_clearing_draft: true, - prepend_cw_re: false, + prepend_cw_re: true, preselect_on_reply: false, inline_preview_cards: true, hicolor_privacy_icons: false, diff --git a/app/javascript/flavours/glitch/styles/modern/style.scss b/app/javascript/flavours/glitch/styles/modern/style.scss index 272bba2a53..54762c2bd6 100644 --- a/app/javascript/flavours/glitch/styles/modern/style.scss +++ b/app/javascript/flavours/glitch/styles/modern/style.scss @@ -97,10 +97,11 @@ a:focus-visible, background: none; } .account__avatar, +.account__avatar img, #profile_page_avatar, .account__avatar-composite, .account-card__title__avatar img { - border-radius: 30%; + border-radius: 25%; flex: none; } :not(body):not(.scrollable)::-webkit-scrollbar { @@ -3258,4 +3259,4 @@ a:focus-visible, } .emoji-picker-dropdown__modifiers { top: 16px; -} \ No newline at end of file +} diff --git a/app/javascript/material-icons/400-24px/bubble-chart.svg b/app/javascript/material-icons/400-24px/bubble-chart.svg new file mode 100644 index 0000000000..c8b53a18ce --- /dev/null +++ b/app/javascript/material-icons/400-24px/bubble-chart.svg @@ -0,0 +1 @@ + \ No newline at end of file