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?