From ee97e947793f15dc3ed8cc1a5fcc673cf628f585 Mon Sep 17 00:00:00 2001 From: marykatefain Date: Wed, 29 Jul 2020 21:08:36 +0000 Subject: [PATCH] Bookmark posts --- app/soapbox/actions/bookmarks.js | 90 +++++++++++++++++++ app/soapbox/actions/interactions.js | 83 +++++++++++++++++ app/soapbox/components/sidebar_menu.js | 5 ++ app/soapbox/components/status_action_bar.js | 9 ++ app/soapbox/containers/status_container.js | 10 +++ app/soapbox/features/bookmarks/index.js | 79 ++++++++++++++++ .../features/compose/components/action_bar.js | 2 + app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + .../reducers/__tests__/status_lists-test.js | 5 ++ app/soapbox/reducers/status_lists.js | 29 ++++++ 11 files changed, 318 insertions(+) create mode 100644 app/soapbox/actions/bookmarks.js create mode 100644 app/soapbox/features/bookmarks/index.js diff --git a/app/soapbox/actions/bookmarks.js b/app/soapbox/actions/bookmarks.js new file mode 100644 index 000000000..544ed2ff2 --- /dev/null +++ b/app/soapbox/actions/bookmarks.js @@ -0,0 +1,90 @@ +import api, { getLinks } from '../api'; +import { importFetchedStatuses } from './importer'; + +export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; +export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; +export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL'; + +export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST'; +export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'; +export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'; + +export function fetchBookmarkedStatuses() { + return (dispatch, getState) => { + if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(fetchBookmarkedStatusesRequest()); + + api(getState).get('/api/v1/bookmarks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchBookmarkedStatusesFail(error)); + }); + }; +}; + +export function fetchBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_FETCH_FAIL, + error, + }; +}; + +export function expandBookmarkedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(expandBookmarkedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandBookmarkedStatusesFail(error)); + }); + }; +}; + +export function expandBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +}; + +export function expandBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_EXPAND_FAIL, + error, + }; +}; diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index 7451c14f9..1acfa9c57 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; +import { showAlert } from 'soapbox/actions/alerts'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -33,6 +34,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST'; export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; export const UNPIN_FAIL = 'UNPIN_FAIL'; +export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST'; +export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS'; +export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL'; + +export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; +export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; +export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; + export function reblog(status) { return function(dispatch, getState) { if (!getState().get('me')) return; @@ -195,6 +204,80 @@ export function unfavouriteFail(status, error) { }; }; +export function bookmark(status) { + return function(dispatch, getState) { + dispatch(bookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) { + dispatch(importFetchedStatus(response.data)); + dispatch(bookmarkSuccess(status, response.data)); + dispatch(showAlert('', 'Bookmark added')); + }).catch(function(error) { + dispatch(bookmarkFail(status, error)); + }); + }; +}; + +export function unbookmark(status) { + return (dispatch, getState) => { + dispatch(unbookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unbookmarkSuccess(status, response.data)); + dispatch(showAlert('', 'Bookmark removed')); + }).catch(error => { + dispatch(unbookmarkFail(status, error)); + }); + }; +}; + +export function bookmarkRequest(status) { + return { + type: BOOKMARK_REQUEST, + status: status, + }; +}; + +export function bookmarkSuccess(status, response) { + return { + type: BOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function bookmarkFail(status, error) { + return { + type: BOOKMARK_FAIL, + status: status, + error: error, + }; +}; + +export function unbookmarkRequest(status) { + return { + type: UNBOOKMARK_REQUEST, + status: status, + }; +}; + +export function unbookmarkSuccess(status, response) { + return { + type: UNBOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function unbookmarkFail(status, error) { + return { + type: UNBOOKMARK_FAIL, + status: status, + error: error, + }; +}; + export function fetchReblogs(id) { return (dispatch, getState) => { if (!getState().get('me')) return; diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index a62c32957..f33802371 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -32,6 +32,7 @@ const messages = defineMessages({ security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, lists: { id: 'column.lists', defaultMessage: 'Lists' }, + bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' }, news: { id: 'tabs_bar.news', defaultMessage: 'News' }, donate: { id: 'donate', defaultMessage: 'Donate' }, @@ -144,6 +145,10 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.lists)} + + + {intl.formatMessage(messages.bookmarks)} +
diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index 807c8f3cf..1ee84ca00 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -31,6 +31,8 @@ const messages = defineMessages({ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, open: { id: 'status.open', defaultMessage: 'Expand this post' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, @@ -55,6 +57,7 @@ class StatusActionBar extends ImmutablePureComponent { onOpenUnauthorizedModal: PropTypes.func.isRequired, onReply: PropTypes.func, onFavourite: PropTypes.func, + onBookmark: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, @@ -149,6 +152,10 @@ class StatusActionBar extends ImmutablePureComponent { } } + handleBookmarkClick = () => { + this.props.onBookmark(this.props.status); + } + handleReblogClick = e => { const { me } = this.props; if (me) { @@ -246,6 +253,8 @@ class StatusActionBar extends ImmutablePureComponent { // menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick }); + if (!me) { return menu; } diff --git a/app/soapbox/containers/status_container.js b/app/soapbox/containers/status_container.js index d84141165..02dc76c21 100644 --- a/app/soapbox/containers/status_container.js +++ b/app/soapbox/containers/status_container.js @@ -12,6 +12,8 @@ import { favourite, unreblog, unfavourite, + bookmark, + unbookmark, pin, unpin, } from '../actions/interactions'; @@ -100,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onBookmark(status) { + if (status.get('bookmarked')) { + dispatch(unbookmark(status)); + } else { + dispatch(bookmark(status)); + } + }, + onPin(status) { if (status.get('pinned')) { dispatch(unpin(status)); diff --git a/app/soapbox/features/bookmarks/index.js b/app/soapbox/features/bookmarks/index.js new file mode 100644 index 000000000..60ef2e0c5 --- /dev/null +++ b/app/soapbox/features/bookmarks/index.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import StatusList from '../../components/status_list'; +import { fetchBookmarkedStatuses } from '../../actions/bookmarks'; + +const messages = defineMessages({ + heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +const mapStateToProps = state => ({ + statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), + hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Bookmarks extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + statusIds: ImmutablePropTypes.list.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + }; + + componentDidMount() { + const { dispatch } = this.props; + dispatch(fetchBookmarkedStatuses()); + } + + componentDidUpdate(prevProps) { + const { dispatch } = this.props; + dispatch(fetchBookmarkedStatuses()); + } + + handleLoadMore = maxId => { + const { dispatch } = this.props; + dispatch(fetchBookmarkedStatuses({ maxId })); + } + + + render() { + const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; + const pinned = !!columnId; + + const emptyMessage = ; + + return ( + + + + ); + } + +} diff --git a/app/soapbox/features/compose/components/action_bar.js b/app/soapbox/features/compose/components/action_bar.js index db1774903..411a30f17 100644 --- a/app/soapbox/features/compose/components/action_bar.js +++ b/app/soapbox/features/compose/components/action_bar.js @@ -11,6 +11,7 @@ const messages = defineMessages({ profile: { id: 'account.profile', defaultMessage: 'Profile' }, messages: { id: 'navigation_bar.messages', defaultMessage: 'Messages' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, @@ -69,6 +70,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.profile), to: `/@${meUsername}` }); menu.push({ text: intl.formatMessage(messages.messages), to: '/messages' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 11e0669a9..7ca9f788f 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -64,6 +64,7 @@ import { // GroupTimeline, ListTimeline, Lists, + Bookmarks, // GroupMembers, // GroupRemovedAccounts, // GroupCreate, @@ -227,6 +228,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 7d3805ef1..48f36a954 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -62,6 +62,10 @@ export function Lists() { return import(/* webpackChunkName: "features/lists" */'../../lists'); } +export function Bookmarks() { + return import(/* webpackChunkName: "features/bookmarks" */'../../bookmarks'); +} + export function Status() { return import(/* webpackChunkName: "features/status" */'../../status'); } diff --git a/app/soapbox/reducers/__tests__/status_lists-test.js b/app/soapbox/reducers/__tests__/status_lists-test.js index b996228a1..27f6ffde1 100644 --- a/app/soapbox/reducers/__tests__/status_lists-test.js +++ b/app/soapbox/reducers/__tests__/status_lists-test.js @@ -9,6 +9,11 @@ describe('status_lists reducer', () => { loaded: false, items: ImmutableList(), }), + bookmarks: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableList(), + }), pins: ImmutableMap({ next: null, loaded: false, diff --git a/app/soapbox/reducers/status_lists.js b/app/soapbox/reducers/status_lists.js index 6c5f33557..9f8f28dee 100644 --- a/app/soapbox/reducers/status_lists.js +++ b/app/soapbox/reducers/status_lists.js @@ -6,6 +6,14 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_FAIL, } from '../actions/favourites'; +import { + BOOKMARKED_STATUSES_FETCH_REQUEST, + BOOKMARKED_STATUSES_FETCH_SUCCESS, + BOOKMARKED_STATUSES_FETCH_FAIL, + BOOKMARKED_STATUSES_EXPAND_REQUEST, + BOOKMARKED_STATUSES_EXPAND_SUCCESS, + BOOKMARKED_STATUSES_EXPAND_FAIL, +} from '../actions/bookmarks'; import { PINNED_STATUSES_FETCH_SUCCESS, } from '../actions/pin_statuses'; @@ -13,6 +21,8 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { FAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS, + BOOKMARK_SUCCESS, + UNBOOKMARK_SUCCESS, PIN_SUCCESS, UNPIN_SUCCESS, } from '../actions/interactions'; @@ -23,6 +33,11 @@ const initialState = ImmutableMap({ loaded: false, items: ImmutableList(), }), + bookmarks: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableList(), + }), pins: ImmutableMap({ next: null, loaded: false, @@ -71,10 +86,24 @@ export default function statusLists(state = initialState, action) { return normalizeList(state, 'favourites', action.statuses, action.next); case FAVOURITED_STATUSES_EXPAND_SUCCESS: return appendToList(state, 'favourites', action.statuses, action.next); + case BOOKMARKED_STATUSES_FETCH_REQUEST: + case BOOKMARKED_STATUSES_EXPAND_REQUEST: + return state.setIn(['bookmarks', 'isLoading'], true); + case BOOKMARKED_STATUSES_FETCH_FAIL: + case BOOKMARKED_STATUSES_EXPAND_FAIL: + return state.setIn(['bookmarks', 'isLoading'], false); + case BOOKMARKED_STATUSES_FETCH_SUCCESS: + return normalizeList(state, 'bookmarks', action.statuses, action.next); + case BOOKMARKED_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'bookmarks', action.statuses, action.next); case FAVOURITE_SUCCESS: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: return removeOneFromList(state, 'favourites', action.status); + case BOOKMARK_SUCCESS: + return prependOneToList(state, 'bookmarks', action.status); + case UNBOOKMARK_SUCCESS: + return removeOneFromList(state, 'bookmarks', action.status); case PINNED_STATUSES_FETCH_SUCCESS: return normalizeList(state, 'pins', action.statuses, action.next); case PIN_SUCCESS: