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