diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb
index 91873619a..5f4343cea 100644
--- a/app/controllers/api/v1/bookmarks_controller.rb
+++ b/app/controllers/api/v1/bookmarks_controller.rb
@@ -28,20 +28,43 @@ class Api::V1::BookmarksController < Api::BaseController
end
def results
- @_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
+ @_results ||= filtered_account_bookmarks.to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
+ def filtered_account_bookmarks
+ account_bookmarks.joins(:status).eager_load(:status).tap do |scope|
+ scope.merge!(media_only_scope) if media_only?
+ scope.merge!(without_media_scope) if without_media?
+ end
+ end
+
def account_bookmarks
current_account.bookmarks
end
+ def media_only?
+ truthy_param?(:only_media)
+ end
+
+ def without_media?
+ truthy_param?(:without_media)
+ end
+
def compact?
truthy_param?(:compact)
end
+ def media_only_scope
+ Status.joins(:media_attachments)
+ end
+
+ def without_media_scope
+ Status.left_joins(:media_attachments).where(media_attachments: {status_id: nil})
+ end
+
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
@@ -67,6 +90,6 @@ class Api::V1::BookmarksController < Api::BaseController
end
def pagination_params(core_params)
- params.slice(:limit, :compact).permit(:limi, :compact).merge(core_params)
+ params_slice(:limit, :compact, :only_media, :without_media).merge(core_params)
end
end
diff --git a/app/controllers/api/v1/emoji_reactions_controller.rb b/app/controllers/api/v1/emoji_reactions_controller.rb
index af421cc0c..b5a88e01c 100644
--- a/app/controllers/api/v1/emoji_reactions_controller.rb
+++ b/app/controllers/api/v1/emoji_reactions_controller.rb
@@ -28,15 +28,17 @@ class Api::V1::EmojiReactionsController < Api::BaseController
end
def results
- @_results ||= filtered_emoji_reactions.joins(:status).eager_load(:status).to_a_paginated_by_id(
+ @_results ||= filtered_emoji_reactions.to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
def filtered_emoji_reactions
- account_emoji_reactions.tap do |emoji_reactions|
- emoji_reactions.merge!(emojis_scope) if emojis_requested?
+ account_emoji_reactions.joins(:status).eager_load(:status).tap do |emoji_reactions|
+ emoji_reactions.merge!(emojis_scope) if emojis_requested?
+ emoji_reactions.merge!(media_only_scope) if media_only?
+ emoji_reactions.merge!(without_media_scope) if without_media?
end
end
@@ -44,10 +46,43 @@ class Api::V1::EmojiReactionsController < Api::BaseController
current_account.emoji_reactions
end
+ def emojis_requested?
+ emoji_reactions_params[:emojis].present?
+ end
+
+ def media_only?
+ truthy_param?(:only_media)
+ end
+
+ def without_media?
+ truthy_param?(:without_media)
+ end
+
def compact?
truthy_param?(:compact)
end
+ def emojis_scope
+ emoji_reactions = EmojiReaction.none
+
+ emoji_reactions_params[:emojis].each do |emoji|
+ shortcode, domain = emoji.split('@')
+ custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
+
+ emoji_reactions = emoji_reactions.or(EmojiReaction.where(name: shortcode, custom_emoji: custom_emoji))
+ end
+
+ emoji_reactions
+ end
+
+ def media_only_scope
+ Status.joins(:media_attachments)
+ end
+
+ def without_media_scope
+ Status.left_joins(:media_attachments).where(media_attachments: {status_id: nil})
+ end
+
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
@@ -72,25 +107,8 @@ class Api::V1::EmojiReactionsController < Api::BaseController
results.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
- def emojis_requested?
- emoji_reactions_params[:emojis].present?
- end
-
- def emojis_scope
- emoji_reactions = EmojiReaction.none
-
- emoji_reactions_params[:emojis].each do |emoji|
- shortcode, domain = emoji.split('@')
- custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
-
- emoji_reactions = emoji_reactions.or(EmojiReaction.where(name: shortcode, custom_emoji: custom_emoji))
- end
-
- emoji_reactions
- end
-
def pagination_params(core_params)
- params.slice(:limit, :compact).permit(:limit, :compact).merge(core_params)
+ params_slice(:limit, :compact, :only_media, :without_media).merge(core_params)
end
def emoji_reactions_params
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index a52f517de..954a005fd 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -28,20 +28,43 @@ class Api::V1::FavouritesController < Api::BaseController
end
def results
- @_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
+ @_results ||= filtered_account_favourites.to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
+ def filtered_account_favourites
+ account_favourites.joins(:status).eager_load(:status).tap do |scope|
+ scope.merge!(media_only_scope) if media_only?
+ scope.merge!(without_media_scope) if without_media?
+ end
+ end
+
def account_favourites
current_account.favourites
end
+ def media_only?
+ truthy_param?(:only_media)
+ end
+
+ def without_media?
+ truthy_param?(:without_media)
+ end
+
def compact?
truthy_param?(:compact)
end
+ def media_only_scope
+ Status.joins(:media_attachments)
+ end
+
+ def without_media_scope
+ Status.left_joins(:media_attachments).where(media_attachments: {status_id: nil})
+ end
+
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
@@ -71,6 +94,6 @@ class Api::V1::FavouritesController < Api::BaseController
end
def pagination_params(core_params)
- params.slice(:limit, :compact).permit(:limit, :compact).merge(core_params)
+ params_slice(:limit, :compact, :only_media, :without_media).merge(core_params)
end
end
diff --git a/app/controllers/api/v1/timelines/personal_controller.rb b/app/controllers/api/v1/timelines/personal_controller.rb
index 9fc1e641b..1c584a05d 100644
--- a/app/controllers/api/v1/timelines/personal_controller.rb
+++ b/app/controllers/api/v1/timelines/personal_controller.rb
@@ -37,7 +37,19 @@ class Api::V1::Timelines::PersonalController < Api::BaseController
end
def personal_feed
- PersonalFeed.new(current_account)
+ PersonalFeed.new(
+ current_account,
+ only_media: media_only?,
+ without_media: without_media?,
+ )
+ end
+
+ def media_only?
+ truthy_param?(:only_media)
+ end
+
+ def without_media?
+ truthy_param?(:without_media)
end
def compact?
@@ -49,7 +61,7 @@ class Api::V1::Timelines::PersonalController < Api::BaseController
end
def pagination_params(core_params)
- params.slice(:local, :limit).permit(:local, :limit).merge(core_params)
+ params_slice(:limit, :compact, :only_media, :without_media).merge(core_params)
end
def next_path
diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js
index fbb19be02..779562e67 100644
--- a/app/javascript/mastodon/actions/bookmarks.js
+++ b/app/javascript/mastodon/actions/bookmarks.js
@@ -10,15 +10,18 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_RE
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
-export function fetchBookmarkedStatuses() {
+export function fetchBookmarkedStatuses({ onlyMedia, withoutMedia } = {}) {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
}
+ const params = ['compact=true', onlyMedia ? 'only_media=true' : null, withoutMedia ? 'without_media=true' : null];
+ const param_string = params.filter(e => !!e).join('&');
+
dispatch(fetchBookmarkedStatusesRequest());
- api(getState).get('/api/v1/bookmarks?compact=true').then(response => {
+ api(getState).get(`/api/v1/bookmarks?${param_string}`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
if ('statuses' in response.data && 'accounts' in response.data) {
const { statuses, referenced_statuses, accounts, relationships } = response.data;
diff --git a/app/javascript/mastodon/actions/emoji_reactions.js b/app/javascript/mastodon/actions/emoji_reactions.js
index c511b3bae..e6d63f7db 100644
--- a/app/javascript/mastodon/actions/emoji_reactions.js
+++ b/app/javascript/mastodon/actions/emoji_reactions.js
@@ -10,15 +10,18 @@ export const EMOJI_REACTIONED_STATUSES_EXPAND_REQUEST = 'EMOJI_REACTIONED_STATUS
export const EMOJI_REACTIONED_STATUSES_EXPAND_SUCCESS = 'EMOJI_REACTIONED_STATUSES_EXPAND_SUCCESS';
export const EMOJI_REACTIONED_STATUSES_EXPAND_FAIL = 'EMOJI_REACTIONED_STATUSES_EXPAND_FAIL';
-export function fetchEmojiReactionedStatuses() {
+export function fetchEmojiReactionedStatuses({ onlyMedia, withoutMedia } = {}) {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'emoji_reactions', 'isLoading'])) {
return;
}
+ const params = ['compact=true', onlyMedia ? 'only_media=true' : null, withoutMedia ? 'without_media=true' : null];
+ const param_string = params.filter(e => !!e).join('&');
+
dispatch(fetchEmojiReactionedStatusesRequest());
- api(getState).get('/api/v1/emoji_reactions?compact=true').then(response => {
+ api(getState).get(`/api/v1/emoji_reactions?${param_string}`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
if ('statuses' in response.data && 'accounts' in response.data) {
const { statuses, referenced_statuses, accounts, relationships } = response.data;
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 374633ac7..3fce253a5 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -10,15 +10,18 @@ export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_RE
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
-export function fetchFavouritedStatuses() {
+export function fetchFavouritedStatuses({ onlyMedia, withoutMedia } = {}) {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return;
}
+ const params = ['compact=true', onlyMedia ? 'only_media=true' : null, withoutMedia ? 'without_media=true' : null];
+ const param_string = params.filter(e => !!e).join('&');
+
dispatch(fetchFavouritedStatusesRequest());
- api(getState).get('/api/v1/favourites?compact=true').then(response => {
+ api(getState).get(`/api/v1/favourites?${param_string}`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
if ('statuses' in response.data && 'accounts' in response.data) {
const { statuses, referenced_statuses, accounts, relationships } = response.data;
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 7288bb206..27bf373b5 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -183,7 +183,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId, visibilities } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId, visibilities: visibilities }, done);
export const expandLimitedTimeline = ({ maxId, visibilities } = {}, done = noOp) => expandTimeline('limited', '/api/v1/timelines/home', { max_id: maxId, visibilities: visibilities }, done);
-export const expandPersonalTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('personal', '/api/v1/timelines/personal', { max_id: maxId}, done);
+export const expandPersonalTimeline = ({ maxId, onlyMedia, withoutMedia } = {}, done = noOp) => expandTimeline(`personal${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/personal', { max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, withoutMedia, withoutBot, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done);
export const expandDomainTimeline = (domain, { maxId, onlyMedia, withoutMedia, withoutBot } = {}, done = noOp) => expandTimeline(`domain${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done);
export const expandGroupTimeline = (id, { maxId, onlyMedia, withoutMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, tagged: tagged }, done);
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/components/column_settings.js b/app/javascript/mastodon/features/bookmarked_statuses/components/column_settings.js
new file mode 100644
index 000000000..19d26f658
--- /dev/null
+++ b/app/javascript/mastodon/features/bookmarked_statuses/components/column_settings.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+ static propTypes = {
+ settings: ImmutablePropTypes.map.isRequired,
+ onChange: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ columnId: PropTypes.string,
+ };
+
+ render () {
+ const { settings, onChange } = this.props;
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/containers/column_settings_container.js b/app/javascript/mastodon/features/bookmarked_statuses/containers/column_settings_container.js
new file mode 100644
index 000000000..117142f49
--- /dev/null
+++ b/app/javascript/mastodon/features/bookmarked_statuses/containers/column_settings_container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { changeSetting } from '../../../actions/settings';
+import { changeColumnParams } from '../../../actions/columns';
+
+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', 'bookmarked_statuses']),
+ };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+ return {
+ onChange (key, checked) {
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, key, checked));
+ } else {
+ dispatch(changeSetting(['bookmarked_statuses', ...key], checked));
+ }
+ },
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.js b/app/javascript/mastodon/features/bookmarked_statuses/index.js
index cf9e3971e..025e40c95 100644
--- a/app/javascript/mastodon/features/bookmarked_statuses/index.js
+++ b/app/javascript/mastodon/features/bookmarked_statuses/index.js
@@ -6,6 +6,7 @@ import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import ColumnSettingsContainer from './containers/column_settings_container';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -22,12 +23,16 @@ const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
+ const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'bookmarked_statuses', 'other', 'onlyMedia']);
+ const withoutMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'withoutMedia']) : state.getIn(['settings', 'bookmarked_statuses', 'other', 'withoutMedia']);
const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'bookmarked_statuses', 'columnWidth']);
return {
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
+ onlyMedia,
+ withoutMedia,
columnWidth: columnWidth ?? defaultColumnWidth,
};
};
@@ -43,21 +48,38 @@ class Bookmarks extends ImmutablePureComponent {
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
columnWidth: PropTypes.string,
+ onlyMedia: PropTypes.bool,
+ withoutMedia: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
- componentWillMount () {
- this.props.dispatch(fetchBookmarkedStatuses());
+ static defaultProps = {
+ onlyMedia: false,
+ withoutMedia: false,
+ };
+
+ componentDidMount () {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ dispatch(fetchBookmarkedStatuses({ onlyMedia, withoutMedia }));
+ }
+
+ componentDidUpdate (prevProps) {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ if (prevProps.onlyMedia !== onlyMedia || prevProps.withoutMedia !== withoutMedia) {
+ dispatch(fetchBookmarkedStatuses({ onlyMedia, withoutMedia }));
+ }
}
handlePin = () => {
- const { columnId, dispatch } = this.props;
+ const { columnId, dispatch, onlyMedia, withoutMedia } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
- dispatch(addColumn('BOOKMARKS', {}));
+ dispatch(addColumn('BOOKMARKS', { other: { onlyMedia, withoutMedia } }));
}
}
@@ -89,7 +111,7 @@ class Bookmarks extends ImmutablePureComponent {
}
render () {
- const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth } = this.props;
+ const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth, withoutMedia } = this.props;
const pinned = !!columnId;
const emptyMessage = ;
@@ -107,7 +129,9 @@ class Bookmarks extends ImmutablePureComponent {
columnWidth={columnWidth}
onWidthChange={this.handleWidthChange}
showBackButton
- />
+ >
+
+
);
diff --git a/app/javascript/mastodon/features/emoji_reactioned_statuses/components/column_settings.js b/app/javascript/mastodon/features/emoji_reactioned_statuses/components/column_settings.js
new file mode 100644
index 000000000..19d26f658
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji_reactioned_statuses/components/column_settings.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+ static propTypes = {
+ settings: ImmutablePropTypes.map.isRequired,
+ onChange: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ columnId: PropTypes.string,
+ };
+
+ render () {
+ const { settings, onChange } = this.props;
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/emoji_reactioned_statuses/containers/column_settings_container.js b/app/javascript/mastodon/features/emoji_reactioned_statuses/containers/column_settings_container.js
new file mode 100644
index 000000000..10d4bc871
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji_reactioned_statuses/containers/column_settings_container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { changeSetting } from '../../../actions/settings';
+import { changeColumnParams } from '../../../actions/columns';
+
+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', 'emoji_reactioned_statuses']),
+ };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+ return {
+ onChange (key, checked) {
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, key, checked));
+ } else {
+ dispatch(changeSetting(['emoji_reactioned_statuses', ...key], checked));
+ }
+ },
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/emoji_reactioned_statuses/index.js b/app/javascript/mastodon/features/emoji_reactioned_statuses/index.js
index d25c78aa8..24de644e2 100644
--- a/app/javascript/mastodon/features/emoji_reactioned_statuses/index.js
+++ b/app/javascript/mastodon/features/emoji_reactioned_statuses/index.js
@@ -6,6 +6,7 @@ import { fetchEmojiReactionedStatuses, expandEmojiReactionedStatuses } from '../
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import ColumnSettingsContainer from './containers/column_settings_container';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -22,12 +23,16 @@ const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
+ const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'emoji_reactioned_statuses', 'other', 'onlyMedia']);
+ const withoutMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'withoutMedia']) : state.getIn(['settings', 'emoji_reactioned_statuses', 'other', 'withoutMedia']);
const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'emoji_reactioned_statuses', 'columnWidth']);
return {
statusIds: state.getIn(['status_lists', 'emoji_reactions', 'items']),
isLoading: state.getIn(['status_lists', 'emoji_reactions', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'emoji_reactions', 'next']),
+ onlyMedia,
+ withoutMedia,
columnWidth: columnWidth ?? defaultColumnWidth,
};
};
@@ -43,21 +48,38 @@ class EmojiReactions extends ImmutablePureComponent {
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
columnWidth: PropTypes.string,
+ onlyMedia: PropTypes.bool,
+ withoutMedia: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
- componentWillMount () {
- this.props.dispatch(fetchEmojiReactionedStatuses());
+ static defaultProps = {
+ onlyMedia: false,
+ withoutMedia: false,
+ };
+
+ componentDidMount () {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ dispatch(fetchEmojiReactionedStatuses({ onlyMedia, withoutMedia }));
+ }
+
+ componentDidUpdate (prevProps) {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ if (prevProps.onlyMedia !== onlyMedia || prevProps.withoutMedia !== withoutMedia) {
+ dispatch(fetchEmojiReactionedStatuses({ onlyMedia, withoutMedia }));
+ }
}
handlePin = () => {
- const { columnId, dispatch } = this.props;
+ const { columnId, dispatch, onlyMedia, withoutMedia } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
- dispatch(addColumn('EMOJI_REACTIONS', {}));
+ dispatch(addColumn('EMOJI_REACTIONS', { other: { onlyMedia, withoutMedia } }));
}
}
@@ -89,7 +111,7 @@ class EmojiReactions extends ImmutablePureComponent {
}
render () {
- const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth } = this.props;
+ const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth, withoutMedia } = this.props;
const pinned = !!columnId;
const emptyMessage = ;
@@ -107,7 +129,9 @@ class EmojiReactions extends ImmutablePureComponent {
columnWidth={columnWidth}
onWidthChange={this.handleWidthChange}
showBackButton
- />
+ >
+
+
);
diff --git a/app/javascript/mastodon/features/favourited_statuses/components/column_settings.js b/app/javascript/mastodon/features/favourited_statuses/components/column_settings.js
new file mode 100644
index 000000000..19d26f658
--- /dev/null
+++ b/app/javascript/mastodon/features/favourited_statuses/components/column_settings.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+ static propTypes = {
+ settings: ImmutablePropTypes.map.isRequired,
+ onChange: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ columnId: PropTypes.string,
+ };
+
+ render () {
+ const { settings, onChange } = this.props;
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/favourited_statuses/containers/column_settings_container.js b/app/javascript/mastodon/features/favourited_statuses/containers/column_settings_container.js
new file mode 100644
index 000000000..b9c419e74
--- /dev/null
+++ b/app/javascript/mastodon/features/favourited_statuses/containers/column_settings_container.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { changeSetting } from '../../../actions/settings';
+import { changeColumnParams } from '../../../actions/columns';
+
+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', 'favourited_statuses']),
+ };
+};
+
+const mapDispatchToProps = (dispatch, { columnId }) => {
+ return {
+ onChange (key, checked) {
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, key, checked));
+ } else {
+ dispatch(changeSetting(['favourited_statuses', ...key], checked));
+ }
+ },
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index a638074ea..3b5e41070 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -6,6 +6,7 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import ColumnSettingsContainer from './containers/column_settings_container';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -22,12 +23,16 @@ const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
+ const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'favourited_statuses', 'other', 'onlyMedia']);
+ const withoutMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'withoutMedia']) : state.getIn(['settings', 'favourited_statuses', 'other', 'withoutMedia']);
const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'favourited_statuses', 'columnWidth']);
return {
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
+ onlyMedia,
+ withoutMedia,
columnWidth: columnWidth ?? defaultColumnWidth,
};
};
@@ -43,21 +48,38 @@ class Favourites extends ImmutablePureComponent {
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
columnWidth: PropTypes.string,
+ onlyMedia: PropTypes.bool,
+ withoutMedia: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
- componentWillMount () {
- this.props.dispatch(fetchFavouritedStatuses());
+ static defaultProps = {
+ onlyMedia: false,
+ withoutMedia: false,
+ };
+
+ componentDidMount () {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ dispatch(fetchFavouritedStatuses({ onlyMedia, withoutMedia }));
+ }
+
+ componentDidUpdate (prevProps) {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ if (prevProps.onlyMedia !== onlyMedia || prevProps.withoutMedia !== withoutMedia) {
+ dispatch(fetchFavouritedStatuses({ onlyMedia, withoutMedia }));
+ }
}
handlePin = () => {
- const { columnId, dispatch } = this.props;
+ const { columnId, dispatch, onlyMedia, withoutMedia } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
- dispatch(addColumn('FAVOURITES', {}));
+ dispatch(addColumn('FAVOURITES', { other: { onlyMedia, withoutMedia } }));
}
}
@@ -89,7 +111,7 @@ class Favourites extends ImmutablePureComponent {
}
render () {
- const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth } = this.props;
+ const { intl, statusIds, columnId, multiColumn, hasMore, isLoading, columnWidth, withoutMedia } = this.props;
const pinned = !!columnId;
const emptyMessage = ;
@@ -107,7 +129,9 @@ class Favourites extends ImmutablePureComponent {
columnWidth={columnWidth}
onWidthChange={this.handleWidthChange}
showBackButton
- />
+ >
+
+
);
diff --git a/app/javascript/mastodon/features/personal_timeline/components/column_settings.js b/app/javascript/mastodon/features/personal_timeline/components/column_settings.js
index c84a34e9a..5c84713e7 100644
--- a/app/javascript/mastodon/features/personal_timeline/components/column_settings.js
+++ b/app/javascript/mastodon/features/personal_timeline/components/column_settings.js
@@ -10,7 +10,6 @@ class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
- onChangeClear: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -22,7 +21,8 @@ class ColumnSettings extends React.PureComponent {
- } />
+ } />
+ } />
);
diff --git a/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js
index 92e6c995b..dae192495 100644
--- a/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/personal_timeline/containers/column_settings_container.js
@@ -1,21 +1,28 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from '../../../actions/settings';
+import { changeSetting } from '../../../actions/settings';
+import { changeColumnParams } from '../../../actions/columns';
-const mapStateToProps = state => ({
- settings: state.getIn(['settings', 'personal']),
-});
+const mapStateToProps = (state, { columnId }) => {
+ const uuid = columnId;
+ const columns = state.getIn(['settings', 'columns']);
+ const index = columns.findIndex(c => c.get('uuid') === uuid);
-const mapDispatchToProps = dispatch => ({
+ return {
+ settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'personal']),
+ };
+};
- onChange (key, checked) {
- dispatch(changeSetting(['personal', ...key], checked));
- },
-
- onSave () {
- dispatch(saveSettings());
- },
-
-});
+const mapDispatchToProps = (dispatch, { columnId }) => {
+ return {
+ onChange (key, checked) {
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, key, checked));
+ } else {
+ dispatch(changeSetting(['personal', ...key], checked));
+ }
+ },
+ };
+};
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/personal_timeline/index.js b/app/javascript/mastodon/features/personal_timeline/index.js
index 49b1e7aa2..914d42635 100644
--- a/app/javascript/mastodon/features/personal_timeline/index.js
+++ b/app/javascript/mastodon/features/personal_timeline/index.js
@@ -20,10 +20,14 @@ const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
+ const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'personal', 'other', 'onlyMedia']);
+ const withoutMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'withoutMedia']) : state.getIn(['settings', 'personal', 'other', 'withoutMedia']);
const columnWidth = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'columnWidth']) : state.getIn(['settings', 'personal', 'columnWidth']);
return {
hasUnread: state.getIn(['timelines', 'personal', 'unread']) > 0,
+ onlyMedia,
+ withoutMedia,
columnWidth: columnWidth ?? defaultColumnWidth,
};
};
@@ -37,17 +41,24 @@ class PersonalTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
columnId: PropTypes.string,
+ onlyMedia: PropTypes.bool,
+ withoutMedia: PropTypes.bool,
multiColumn: PropTypes.bool,
columnWidth: PropTypes.string,
};
+ static defaultProps = {
+ onlyMedia: false,
+ withoutMedia: false,
+ };
+
handlePin = () => {
- const { columnId, dispatch } = this.props;
+ const { columnId, dispatch, onlyMedia, withoutMedia } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
- dispatch(addColumn('PERSONAL', {}));
+ dispatch(addColumn('PERSONAL', { other: { onlyMedia, withoutMedia } }));
}
}
@@ -65,15 +76,23 @@ class PersonalTimeline extends React.PureComponent {
}
handleLoadMore = maxId => {
- const { dispatch } = this.props;
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
- dispatch(expandPersonalTimeline({ maxId }));
+ dispatch(expandPersonalTimeline({ maxId, onlyMedia, withoutMedia }));
}
componentDidMount () {
- const { dispatch } = this.props;
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
- dispatch(expandPersonalTimeline({}));
+ dispatch(expandPersonalTimeline({ onlyMedia, withoutMedia }));
+ }
+
+ componentDidUpdate (prevProps) {
+ const { dispatch, onlyMedia, withoutMedia } = this.props;
+
+ if (prevProps.onlyMedia !== onlyMedia || prevProps.withoutMedia !== withoutMedia) {
+ dispatch(expandPersonalTimeline({ onlyMedia, withoutMedia }));
+ }
}
handleWidthChange = (value) => {
@@ -87,7 +106,7 @@ class PersonalTimeline extends React.PureComponent {
}
render () {
- const { intl, hasUnread, columnId, multiColumn, columnWidth } = this.props;
+ const { intl, hasUnread, columnId, multiColumn, columnWidth, onlyMedia, withoutMedia } = this.props;
const pinned = !!columnId;
return (
@@ -103,15 +122,18 @@ class PersonalTimeline extends React.PureComponent {
multiColumn={multiColumn}
columnWidth={columnWidth}
onWidthChange={this.handleWidthChange}
- />
+ >
+
+
}
bindToDocument={!multiColumn}
+ showCard={!withoutMedia}
/>
);
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 7967da915..2a1bc86b7 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -140,6 +140,24 @@ const initialState = ImmutableMap({
body: '',
}),
}),
+
+ bookmarked_statuses: ImmutableMap({
+ regex: ImmutableMap({
+ body: '',
+ }),
+ }),
+
+ favourited_statuses: ImmutableMap({
+ regex: ImmutableMap({
+ body: '',
+ }),
+ }),
+
+ emoji_reactioned_statuses: ImmutableMap({
+ regex: ImmutableMap({
+ body: '',
+ }),
+ }),
});
const defaultColumns = fromJS([
diff --git a/app/models/personal_feed.rb b/app/models/personal_feed.rb
index f06a69ca3..2a0cc2bdd 100644
--- a/app/models/personal_feed.rb
+++ b/app/models/personal_feed.rb
@@ -3,7 +3,8 @@
class PersonalFeed
# @param [Account] account
# @param [Hash] options
- # @option [Boolean] :with_replies
+ # @option [Boolean] :only_media
+ # @option [Boolean] :without_media
def initialize(account, options = {})
@account = account
@options = options
@@ -17,6 +18,9 @@ class PersonalFeed
def get(limit, max_id = nil, since_id = nil, min_id = nil)
scope = personal_scope
+ scope.merge!(media_only_scope) if media_only?
+ scope.merge!(without_media_scope) if without_media?
+
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
end
@@ -24,6 +28,22 @@ class PersonalFeed
attr_reader :account, :options
+ def media_only?
+ options[:only_media]
+ end
+
+ def without_media?
+ options[:without_media]
+ end
+
+ def media_only_scope
+ Status.joins(:media_attachments).group(:id)
+ end
+
+ def without_media_scope
+ Status.left_joins(:media_attachments).where(media_attachments: {status_id: nil})
+ end
+
def personal_scope
Status.include_expired.where(account_id: account.id).without_reblogs.with_personal_visibility
end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 9ef92ce28..3809d4ae6 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -150,6 +150,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
:account_conversations,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
+ :timeline_bookmark_media_option,
+ :timeline_favourite_media_option,
+ :timeline_emoji_reaction_media_option,
+ :timeline_personal_media_option,
]
capabilities << :profile_search unless Chewy.enabled?