diff --git a/app/javascript/mastodon/actions/emoji_reactions.js b/app/javascript/mastodon/actions/emoji_reactions.js new file mode 100644 index 0000000000..a58e53d2fe --- /dev/null +++ b/app/javascript/mastodon/actions/emoji_reactions.js @@ -0,0 +1,94 @@ +import api, { getLinks } from '../api'; +import { importFetchedStatuses } from './importer'; + +export const EMOJI_REACTED_STATUSES_FETCH_REQUEST = 'EMOJI_REACTED_STATUSES_FETCH_REQUEST'; +export const EMOJI_REACTED_STATUSES_FETCH_SUCCESS = 'EMOJI_REACTED_STATUSES_FETCH_SUCCESS'; +export const EMOJI_REACTED_STATUSES_FETCH_FAIL = 'EMOJI_REACTED_STATUSES_FETCH_FAIL'; + +export const EMOJI_REACTED_STATUSES_EXPAND_REQUEST = 'EMOJI_REACTED_STATUSES_EXPAND_REQUEST'; +export const EMOJI_REACTED_STATUSES_EXPAND_SUCCESS = 'EMOJI_REACTED_STATUSES_EXPAND_SUCCESS'; +export const EMOJI_REACTED_STATUSES_EXPAND_FAIL = 'EMOJI_REACTED_STATUSES_EXPAND_FAIL'; + +export function fetchEmojiReactedStatuses() { + return (dispatch, getState) => { + if (getState().getIn(['status_lists', 'emoji_reactions', 'isLoading'])) { + return; + } + + dispatch(fetchEmojiReactedStatusesRequest()); + + api(getState).get('/api/v1/emoji_reactions').then(response => { + console.dir(response.data) + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchEmojiReactedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchEmojiReactedStatusesFail(error)); + }); + }; +} + +export function fetchEmojiReactedStatusesRequest() { + return { + type: EMOJI_REACTED_STATUSES_FETCH_REQUEST, + skipLoading: true, + }; +} + +export function fetchEmojiReactedStatusesSuccess(statuses, next) { + return { + type: EMOJI_REACTED_STATUSES_FETCH_SUCCESS, + statuses, + next, + skipLoading: true, + }; +} + +export function fetchEmojiReactedStatusesFail(error) { + return { + type: EMOJI_REACTED_STATUSES_FETCH_FAIL, + error, + skipLoading: true, + }; +} + +export function expandEmojiReactedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'emoji_reactions', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'emoji_reactions', 'isLoading'])) { + return; + } + + dispatch(expandEmojiReactedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandEmojiReactedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandEmojiReactedStatusesFail(error)); + }); + }; +} + +export function expandEmojiReactedStatusesRequest() { + return { + type: EMOJI_REACTED_STATUSES_EXPAND_REQUEST, + }; +} + +export function expandEmojiReactedStatusesSuccess(statuses, next) { + return { + type: EMOJI_REACTED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +} + +export function expandEmojiReactedStatusesFail(error) { + return { + type: EMOJI_REACTED_STATUSES_EXPAND_FAIL, + error, + }; +} diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index ee584cb1ba..714071c875 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -10,6 +10,7 @@ const messages = defineMessages({ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + emoji_reactions: { id: 'navigation_bar.emoji_reactions', defaultMessage: 'Emoji Reactions' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, @@ -44,6 +45,7 @@ class ActionBar extends React.PureComponent { menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' }); menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); diff --git a/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx b/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx new file mode 100644 index 0000000000..6d878d3da3 --- /dev/null +++ b/app/javascript/mastodon/features/emoji_reacted_statuses/index.jsx @@ -0,0 +1,108 @@ +import { debounce } from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; +import { fetchEmojiReactedStatuses, expandEmojiReactedStatuses } from 'mastodon/actions/emoji_reactions'; +import ColumnHeader from 'mastodon/components/column_header'; +import StatusList from 'mastodon/components/status_list'; +import Column from 'mastodon/features/ui/components/column'; + +const messages = defineMessages({ + heading: { id: 'column.emoji_reactions', defaultMessage: 'Emoji Reactions' }, +}); + +const mapStateToProps = state => ({ + 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']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class EmojiReactions extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + statusIds: ImmutablePropTypes.list.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + }; + + componentWillMount () { + this.props.dispatch(fetchEmojiReactedStatuses()); + } + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('EMOJI_REACTIONS', {})); + } + }; + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + }; + + handleHeaderClick = () => { + this.column.scrollTop(); + }; + + setRef = c => { + this.column = c; + }; + + handleLoadMore = debounce(() => { + this.props.dispatch(expandEmojiReactedStatuses()); + }, 300, { leading: true }); + + render () { + const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; + const pinned = !!columnId; + + const emptyMessage = ; + + return ( + + + + + + + {intl.formatMessage(messages.heading)} + + + + ); + } + +} diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index c1d346fed4..24f0bdf21f 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -111,6 +111,11 @@ class InteractionModal extends React.PureComponent { title = ; actionDescription = ; break; + case 'emoji_reaction': + icon = ; + title = ; + actionDescription = ; + break; case 'follow': icon = ; title = ; diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 161d105451..593656f203 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -258,7 +258,17 @@ class Status extends ImmutablePureComponent { handleEmojiReact = (status, emoji) => { const { dispatch } = this.props; - dispatch(emojiReact(status, emoji)); + const { signedIn } = this.context.identity; + + if (signedIn) { + dispatch(emojiReact(status, emoji)); + } else { + dispatch(openModal('INTERACTION', { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); + } }; handleUnEmojiReact = (status, emoji) => { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 1dd6e34e88..1ce14b4c5b 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -15,6 +15,7 @@ import { HashtagTimeline, DirectTimeline, FavouritedStatuses, + EmojiReactedStatuses, BookmarkedStatuses, ListTimeline, Directory, @@ -34,6 +35,7 @@ const componentMap = { 'HASHTAG': HashtagTimeline, 'DIRECT': DirectTimeline, 'FAVOURITES': FavouritedStatuses, + 'EMOJI_REACTIONS': EmojiReactedStatuses, 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, 'DIRECTORY': Directory, diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index e0cc5407fb..b24772b8f8 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -42,6 +42,7 @@ import { Notifications, FollowRequests, FavouritedStatuses, + EmojiReactedStatuses, BookmarkedStatuses, FollowedTags, ListTimeline, @@ -102,6 +103,7 @@ const keyMap = { goToDirect: 'g d', goToStart: 'g s', goToFavourites: 'g f', + goToEmojiReactions: 'g e', goToPinned: 'g p', goToProfile: 'g u', goToBlocked: 'g b', @@ -189,6 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent { + @@ -524,6 +527,10 @@ class UI extends React.PureComponent { this.context.router.history.push('/favourites'); }; + handleHotkeyGoToEmojiReactions = () => { + this.context.router.history.push('/emoji_reactions'); + }; + handleHotkeyGoToPinned = () => { this.context.router.history.push('/pinned'); }; @@ -563,6 +570,7 @@ class UI extends React.PureComponent { goToDirect: this.handleHotkeyGoToDirect, goToStart: this.handleHotkeyGoToStart, goToFavourites: this.handleHotkeyGoToFavourites, + goToEmojiReactions: this.handleHotkeyGoToEmojiReactions, goToPinned: this.handleHotkeyGoToPinned, goToProfile: this.handleHotkeyGoToProfile, goToBlocked: this.handleHotkeyGoToBlocked, diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 5e175c9413..71ab6a67c5 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -94,6 +94,10 @@ export function FavouritedStatuses () { return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); } +export function EmojiReactedStatuses () { + return import(/* webpackChunkName: "features/emoji_reacted_statuses" */'../../emoji_reacted_statuses'); +} + export function FollowedTags () { return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags'); } diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index b1716e9cf5..508637b8be 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -6,6 +6,14 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_FAIL, } from '../actions/favourites'; +import { + EMOJI_REACTED_STATUSES_FETCH_REQUEST, + EMOJI_REACTED_STATUSES_FETCH_SUCCESS, + EMOJI_REACTED_STATUSES_FETCH_FAIL, + EMOJI_REACTED_STATUSES_EXPAND_REQUEST, + EMOJI_REACTED_STATUSES_EXPAND_SUCCESS, + EMOJI_REACTED_STATUSES_EXPAND_FAIL, +} from '../actions/emoji_reactions'; import { BOOKMARKED_STATUSES_FETCH_REQUEST, BOOKMARKED_STATUSES_FETCH_SUCCESS, @@ -29,6 +37,8 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl import { FAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS, + EMOJIREACT_SUCCESS, + UNEMOJIREACT_SUCCESS, BOOKMARK_SUCCESS, UNBOOKMARK_SUCCESS, PIN_SUCCESS, @@ -45,6 +55,11 @@ const initialState = ImmutableMap({ loaded: false, items: ImmutableOrderedSet(), }), + emoji_reactions: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableOrderedSet(), + }), bookmarks: ImmutableMap({ next: null, loaded: false, @@ -105,6 +120,16 @@ 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 EMOJI_REACTED_STATUSES_FETCH_REQUEST: + case EMOJI_REACTED_STATUSES_EXPAND_REQUEST: + return state.setIn(['emoji_reactions', 'isLoading'], true); + case EMOJI_REACTED_STATUSES_FETCH_FAIL: + case EMOJI_REACTED_STATUSES_EXPAND_FAIL: + return state.setIn(['emoji_reactions', 'isLoading'], false); + case EMOJI_REACTED_STATUSES_FETCH_SUCCESS: + return normalizeList(state, 'emoji_reactions', action.statuses, action.next); + case EMOJI_REACTED_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'emoji_reactions', action.statuses, action.next); case BOOKMARKED_STATUSES_FETCH_REQUEST: case BOOKMARKED_STATUSES_EXPAND_REQUEST: return state.setIn(['bookmarks', 'isLoading'], true); @@ -129,6 +154,10 @@ export default function statusLists(state = initialState, action) { return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: return removeOneFromList(state, 'favourites', action.status); + case EMOJIREACT_SUCCESS: + return prependOneToList(state, 'emoji_reactions', action.status); + case UNEMOJIREACT_SUCCESS: + return removeOneFromList(state, 'emoji_reactions', action.status); case BOOKMARK_SUCCESS: return prependOneToList(state, 'bookmarks', action.status); case UNBOOKMARK_SUCCESS: diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 44e880d9fc..85d2914d13 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -18,7 +18,7 @@ class EmojiReaction < ApplicationRecord include Paginable EMOJI_REACTION_LIMIT = 32767 - EMOJI_REACTION_PER_ACCOUNT_LIMIT = 5 + EMOJI_REACTION_PER_ACCOUNT_LIMIT = 3 update_index('statuses', :status)