From 23ebf60b95984764992c4b356048786ed0ab2953 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Jan 2017 12:37:15 +0100 Subject: Improve initialState loading --- app/assets/javascripts/components/store/configureStore.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'app/assets/javascripts/components/store') diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx index 3d03d4c19..2c1476e5d 100644 --- a/app/assets/javascripts/components/store/configureStore.jsx +++ b/app/assets/javascripts/components/store/configureStore.jsx @@ -1,11 +1,12 @@ import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import appReducer from '../reducers'; -import { loadingBarMiddleware } from 'react-redux-loading-bar'; -import errorsMiddleware from '../middleware/errors'; +import thunk from 'redux-thunk'; +import appReducer from '../reducers'; +import { loadingBarMiddleware } from 'react-redux-loading-bar'; +import errorsMiddleware from '../middleware/errors'; +import Immutable from 'immutable'; -export default function configureStore(initialState) { - return createStore(appReducer, initialState, compose(applyMiddleware(thunk, loadingBarMiddleware({ +export default function configureStore() { + return createStore(appReducer, compose(applyMiddleware(thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], }), errorsMiddleware()), window.devToolsExtension ? window.devToolsExtension() : f => f)); }; -- cgit From 7d53ee73f323b04757a78e83dcbe49c872d481eb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 16 Jan 2017 13:27:58 +0100 Subject: Fix #238 - Add "favourites" column --- .../javascripts/components/actions/favourites.jsx | 83 ++++++++++++++++++++++ .../javascripts/components/actions/timelines.jsx | 5 ++ .../javascripts/components/containers/mastodon.jsx | 2 + .../components/features/account_timeline/index.jsx | 3 +- .../features/favourited_statuses/index.jsx | 63 ++++++++++++++++ .../components/features/getting_started/index.jsx | 4 +- .../components/middleware/loading_bar.jsx | 25 +++++++ .../javascripts/components/reducers/accounts.jsx | 6 ++ .../javascripts/components/reducers/index.jsx | 2 + .../javascripts/components/reducers/modal.jsx | 18 ++--- .../components/reducers/status_lists.jsx | 39 ++++++++++ .../javascripts/components/reducers/statuses.jsx | 68 ++++++++++-------- .../javascripts/components/reducers/user_lists.jsx | 38 +++++----- .../components/store/configureStore.jsx | 2 +- app/controllers/api/v1/favourites_controller.rb | 2 +- 15 files changed, 297 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/components/actions/favourites.jsx create mode 100644 app/assets/javascripts/components/features/favourited_statuses/index.jsx create mode 100644 app/assets/javascripts/components/middleware/loading_bar.jsx create mode 100644 app/assets/javascripts/components/reducers/status_lists.jsx (limited to 'app/assets/javascripts/components/store') diff --git a/app/assets/javascripts/components/actions/favourites.jsx b/app/assets/javascripts/components/actions/favourites.jsx new file mode 100644 index 000000000..a25c1ae1c --- /dev/null +++ b/app/assets/javascripts/components/actions/favourites.jsx @@ -0,0 +1,83 @@ +import api, { getLinks } from '../api' + +export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; +export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; +export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; + +export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; +export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; +export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; + +export function fetchFavouritedStatuses() { + return (dispatch, getState) => { + dispatch(fetchFavouritedStatusesRequest()); + + api(getState).get('/api/v1/favourites').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchFavouritedStatusesFail(error)); + }); + }; +}; + +export function fetchFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_FETCH_REQUEST + }; +}; + +export function fetchFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_FETCH_SUCCESS, + statuses, + next + }; +}; + +export function fetchFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_FETCH_FAIL, + error + }; +}; + +export function expandFavouritedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'favourites', 'next'], null); + + if (url === null) { + return; + } + + dispatch(expandFavouritedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFavouritedStatusesFail(error)); + }); + }; +}; + +export function expandFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_EXPAND_REQUEST + }; +}; + +export function expandFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_EXPAND_SUCCESS, + statuses, + next + }; +}; + +export function expandFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_EXPAND_FAIL, + error + }; +}; diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 8bb939d31..975a0e090 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -97,6 +97,11 @@ export function expandTimeline(timeline, id = null) { return (dispatch, getState) => { const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last(); + if (!lastId) { + // If timeline is empty, don't try to load older posts since there are none + return; + } + dispatch(expandTimelineRequest(timeline)); let path = timeline; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index af495652f..5f4b2cf79 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -34,6 +34,7 @@ import HashtagTimeline from '../features/hashtag_timeline'; import Notifications from '../features/notifications'; import FollowRequests from '../features/follow_requests'; import GenericNotFound from '../features/generic_not_found'; +import FavouritedStatuses from '../features/favourited_statuses'; import { IntlProvider, addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; import de from 'react-intl/locale-data/de'; @@ -113,6 +114,7 @@ const Mastodon = React.createClass({ + diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx index 7a3dbe160..4a66dbbf5 100644 --- a/app/assets/javascripts/components/features/account_timeline/index.jsx +++ b/app/assets/javascripts/components/features/account_timeline/index.jsx @@ -18,7 +18,8 @@ const AccountTimeline = React.createClass({ propTypes: { params: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list + statusIds: ImmutablePropTypes.list, + me: React.PropTypes.number.isRequired }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/features/favourited_statuses/index.jsx b/app/assets/javascripts/components/features/favourited_statuses/index.jsx new file mode 100644 index 000000000..a2d521736 --- /dev/null +++ b/app/assets/javascripts/components/features/favourited_statuses/index.jsx @@ -0,0 +1,63 @@ +import { connect } from 'react-redux'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import LoadingIndicator from '../../components/loading_indicator'; +import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; +import Column from '../ui/components/column'; +import StatusList from '../../components/status_list'; +import ColumnBackButton from '../public_timeline/components/column_back_button'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + heading: { id: 'column.favourites', defaultMessage: 'Favourites' } +}); + +const mapStateToProps = state => ({ + statusIds: state.getIn(['status_lists', 'favourites', 'items']), + loaded: state.getIn(['status_lists', 'favourites', 'loaded']), + me: state.getIn(['meta', 'me']) +}); + +const Favourites = React.createClass({ + + propTypes: { + params: React.PropTypes.object.isRequired, + dispatch: React.PropTypes.func.isRequired, + statusIds: ImmutablePropTypes.list.isRequired, + loaded: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired, + me: React.PropTypes.number.isRequired + }, + + mixins: [PureRenderMixin], + + componentWillMount () { + this.props.dispatch(fetchFavouritedStatuses()); + }, + + handleScrollToBottom () { + this.props.dispatch(expandFavouritedStatuses()); + }, + + render () { + const { statusIds, loaded, intl, me } = this.props; + + if (!loaded) { + return ( + + + + ); + } + + return ( + + + + + ); + } + +}); + +export default connect(mapStateToProps)(injectIntl(Favourites)); diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index d7c2f8df7..42e0a9e24 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -10,7 +10,8 @@ const messages = defineMessages({ public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' } + sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' } }); const mapStateToProps = state => ({ @@ -29,6 +30,7 @@ const GettingStarted = ({ intl, me }) => {
+ {followRequests}
diff --git a/app/assets/javascripts/components/middleware/loading_bar.jsx b/app/assets/javascripts/components/middleware/loading_bar.jsx new file mode 100644 index 000000000..a98f1bb2b --- /dev/null +++ b/app/assets/javascripts/components/middleware/loading_bar.jsx @@ -0,0 +1,25 @@ +import { showLoading, hideLoading } from 'react-redux-loading-bar'; + +const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED']; + +export default function loadingBarMiddleware(config = {}) { + const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes; + + return ({ dispatch }) => next => (action) => { + if (action.type && !action.skipLoading) { + const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; + + const isPending = new RegExp(`${PENDING}$`, 'g'); + const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); + const isRejected = new RegExp(`${REJECTED}$`, 'g'); + + if (action.type.match(isPending)) { + dispatch(showLoading()); + } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) { + dispatch(hideLoading()); + } + } + + return next(action); + }; +}; diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx index ae048df3b..73dee9078 100644 --- a/app/assets/javascripts/components/reducers/accounts.jsx +++ b/app/assets/javascripts/components/reducers/accounts.jsx @@ -32,6 +32,10 @@ import { NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS } from '../actions/notifications'; +import { + FAVOURITED_STATUSES_FETCH_SUCCESS, + FAVOURITED_STATUSES_EXPAND_SUCCESS +} from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; @@ -90,6 +94,8 @@ export default function accounts(state = initialState, action) { case ACCOUNT_TIMELINE_FETCH_SUCCESS: case ACCOUNT_TIMELINE_EXPAND_SUCCESS: case CONTEXT_FETCH_SUCCESS: + case FAVOURITED_STATUSES_FETCH_SUCCESS: + case FAVOURITED_STATUSES_EXPAND_SUCCESS: return normalizeAccountsFromStatuses(state, action.statuses); case REBLOG_SUCCESS: case FAVOURITE_SUCCESS: diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx index 068491949..80c913d2d 100644 --- a/app/assets/javascripts/components/reducers/index.jsx +++ b/app/assets/javascripts/components/reducers/index.jsx @@ -12,6 +12,7 @@ import relationships from './relationships'; import search from './search'; import notifications from './notifications'; import settings from './settings'; +import status_lists from './status_lists'; export default combineReducers({ timelines, @@ -21,6 +22,7 @@ export default combineReducers({ loadingBar: loadingBarReducer, modal, user_lists, + status_lists, accounts, statuses, relationships, diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx index b529b6aa8..ac53ea210 100644 --- a/app/assets/javascripts/components/reducers/modal.jsx +++ b/app/assets/javascripts/components/reducers/modal.jsx @@ -8,14 +8,14 @@ const initialState = Immutable.Map({ export default function modal(state = initialState, action) { switch(action.type) { - case MEDIA_OPEN: - return state.withMutations(map => { - map.set('url', action.url); - map.set('open', true); - }); - case MODAL_CLOSE: - return state.set('open', false); - default: - return state; + case MEDIA_OPEN: + return state.withMutations(map => { + map.set('url', action.url); + map.set('open', true); + }); + case MODAL_CLOSE: + return state.set('open', false); + default: + return state; } }; diff --git a/app/assets/javascripts/components/reducers/status_lists.jsx b/app/assets/javascripts/components/reducers/status_lists.jsx new file mode 100644 index 000000000..b883b1c58 --- /dev/null +++ b/app/assets/javascripts/components/reducers/status_lists.jsx @@ -0,0 +1,39 @@ +import { + FAVOURITED_STATUSES_FETCH_SUCCESS, + FAVOURITED_STATUSES_EXPAND_SUCCESS +} from '../actions/favourites'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + favourites: Immutable.Map({ + next: null, + loaded: false, + items: Immutable.List() + }) +}); + +const normalizeList = (state, listType, statuses, next) => { + return state.update(listType, listMap => listMap.withMutations(map => { + map.set('next', next); + map.set('loaded', true); + map.set('items', Immutable.List(statuses.map(item => item.id))); + })); +}; + +const appendToList = (state, listType, statuses, next) => { + return state.update(listType, listMap => listMap.withMutations(map => { + map.set('next', next); + map.set('items', map.get('items').push(...statuses.map(item => item.id))); + })); +}; + +export default function statusLists(state = initialState, action) { + switch(action.type) { + case FAVOURITED_STATUSES_FETCH_SUCCESS: + return normalizeList(state, 'favourites', action.statuses, action.next); + case FAVOURITED_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'favourites', action.statuses, action.next); + default: + return state; + } +}; diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx index c740b6d64..084b6304c 100644 --- a/app/assets/javascripts/components/reducers/statuses.jsx +++ b/app/assets/javascripts/components/reducers/statuses.jsx @@ -28,6 +28,10 @@ import { NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS } from '../actions/notifications'; +import { + FAVOURITED_STATUSES_FETCH_SUCCESS, + FAVOURITED_STATUSES_EXPAND_SUCCESS +} from '../actions/favourites'; import Immutable from 'immutable'; const normalizeStatus = (state, status) => { @@ -77,36 +81,38 @@ const initialState = Immutable.Map(); export default function statuses(state = initialState, action) { switch(action.type) { - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeStatus(state, action.status); - case REBLOG_SUCCESS: - case UNREBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeStatus(state, action.response); - case FAVOURITE_REQUEST: - return state.setIn([action.status.get('id'), 'favourited'], true); - case FAVOURITE_FAIL: - return state.setIn([action.status.get('id'), 'favourited'], false); - case REBLOG_REQUEST: - return state.setIn([action.status.get('id'), 'reblogged'], true); - case REBLOG_FAIL: - return state.setIn([action.status.get('id'), 'reblogged'], false); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - return normalizeStatuses(state, action.statuses); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references); - case ACCOUNT_BLOCK_SUCCESS: - return filterStatuses(state, action.relationship); - default: - return state; + case TIMELINE_UPDATE: + case STATUS_FETCH_SUCCESS: + case NOTIFICATIONS_UPDATE: + return normalizeStatus(state, action.status); + case REBLOG_SUCCESS: + case UNREBLOG_SUCCESS: + case FAVOURITE_SUCCESS: + case UNFAVOURITE_SUCCESS: + return normalizeStatus(state, action.response); + case FAVOURITE_REQUEST: + return state.setIn([action.status.get('id'), 'favourited'], true); + case FAVOURITE_FAIL: + return state.setIn([action.status.get('id'), 'favourited'], false); + case REBLOG_REQUEST: + return state.setIn([action.status.get('id'), 'reblogged'], true); + case REBLOG_FAIL: + return state.setIn([action.status.get('id'), 'reblogged'], false); + case TIMELINE_REFRESH_SUCCESS: + case TIMELINE_EXPAND_SUCCESS: + case ACCOUNT_TIMELINE_FETCH_SUCCESS: + case ACCOUNT_TIMELINE_EXPAND_SUCCESS: + case CONTEXT_FETCH_SUCCESS: + case NOTIFICATIONS_REFRESH_SUCCESS: + case NOTIFICATIONS_EXPAND_SUCCESS: + case FAVOURITED_STATUSES_FETCH_SUCCESS: + case FAVOURITED_STATUSES_EXPAND_SUCCESS: + return normalizeStatuses(state, action.statuses); + case TIMELINE_DELETE: + return deleteStatus(state, action.id, action.references); + case ACCOUNT_BLOCK_SUCCESS: + return filterStatuses(state, action.relationship); + default: + return state; } }; diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx index 36093663f..72922f509 100644 --- a/app/assets/javascripts/components/reducers/user_lists.jsx +++ b/app/assets/javascripts/components/reducers/user_lists.jsx @@ -36,24 +36,24 @@ const appendToList = (state, type, id, accounts, next) => { export default function userLists(state = initialState, action) { switch(action.type) { - case FOLLOWERS_FETCH_SUCCESS: - return normalizeList(state, 'followers', action.id, action.accounts, action.next); - case FOLLOWERS_EXPAND_SUCCESS: - return appendToList(state, 'followers', action.id, action.accounts, action.next); - case FOLLOWING_FETCH_SUCCESS: - return normalizeList(state, 'following', action.id, action.accounts, action.next); - case FOLLOWING_EXPAND_SUCCESS: - return appendToList(state, 'following', action.id, action.accounts, action.next); - case REBLOGS_FETCH_SUCCESS: - return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id))); - case FAVOURITES_FETCH_SUCCESS: - return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id))); - case FOLLOW_REQUESTS_FETCH_SUCCESS: - return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); - case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - case FOLLOW_REQUEST_REJECT_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); - default: - return state; + case FOLLOWERS_FETCH_SUCCESS: + return normalizeList(state, 'followers', action.id, action.accounts, action.next); + case FOLLOWERS_EXPAND_SUCCESS: + return appendToList(state, 'followers', action.id, action.accounts, action.next); + case FOLLOWING_FETCH_SUCCESS: + return normalizeList(state, 'following', action.id, action.accounts, action.next); + case FOLLOWING_EXPAND_SUCCESS: + return appendToList(state, 'following', action.id, action.accounts, action.next); + case REBLOGS_FETCH_SUCCESS: + return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + case FAVOURITES_FETCH_SUCCESS: + return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + case FOLLOW_REQUESTS_FETCH_SUCCESS: + return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + case FOLLOW_REQUEST_REJECT_SUCCESS: + return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); + default: + return state; } }; diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx index 2c1476e5d..87f469999 100644 --- a/app/assets/javascripts/components/store/configureStore.jsx +++ b/app/assets/javascripts/components/store/configureStore.jsx @@ -1,7 +1,7 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import appReducer from '../reducers'; -import { loadingBarMiddleware } from 'react-redux-loading-bar'; +import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import Immutable from 'immutable'; diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index a71592acd..ea799fd55 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FavouritesController < ApiController set_maps(@statuses) set_counters_maps(@statuses) - next_path = api_v1_favourites_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT + next_path = api_v1_favourites_url(max_id: results.last.id) if results.size == DEFAULT_STATUSES_LIMIT prev_path = api_v1_favourites_url(since_id: results.first.id) unless results.empty? set_pagination_headers(next_path, prev_path) -- cgit From fcb5a85cdd21b8a48c16cd02885933bcbdb07ec2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 17 Jan 2017 20:09:03 +0100 Subject: Add sounds for notifications. Boop by @jk@mastodon.social --- Procfile | 1 + .../components/actions/notifications.jsx | 8 +++- .../components/components/column_collapsable.jsx | 2 +- .../notifications/components/column_settings.jsx | 7 +++- .../javascripts/components/reducers/settings.jsx | 9 ++++- .../components/store/configureStore.jsx | 14 +++++-- config/puma.rb | 44 +++------------------ package.json | 1 + public/sounds/boop.mp3 | Bin 0 -> 12070 bytes yarn.lock | 10 +++++ 10 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 public/sounds/boop.mp3 (limited to 'app/assets/javascripts/components/store') diff --git a/Procfile b/Procfile index c2c566e8c..6cdd89518 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ web: bundle exec puma -C config/puma.rb +worker: bundle exec sidekiq -q default -q mailers -q push diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 1e5b2c382..8688267f4 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -24,17 +24,21 @@ const fetchRelatedRelationships = (dispatch, notifications) => { export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { + const showAlert = getState().getIn(['notifications', 'settings', 'alerts', notification.type], false); + const playSound = getState().getIn(['notifications', 'settings', 'sounds', notification.type], false); + dispatch({ type: NOTIFICATIONS_UPDATE, notification, account: notification.account, - status: notification.status + status: notification.status, + meta: playSound ? { sound: 'boop' } : null }); fetchRelatedRelationships(dispatch, [notification]); // Desktop notifications - if (typeof window.Notification !== 'undefined' && getState().getIn(['notifications', 'settings', 'alerts', notification.type], false)) { + if (typeof window.Notification !== 'undefined' && showAlert) { const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); const body = $('

').html(notification.status ? notification.status.content : '').text(); diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx index 8d74fd8a7..203dc5e0c 100644 --- a/app/assets/javascripts/components/components/column_collapsable.jsx +++ b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -45,7 +45,7 @@ const ColumnCollapsable = React.createClass({

- + {({ opacity, height }) =>
{children} diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx index dfb59713c..b63c1881a 100644 --- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -36,15 +36,17 @@ const ColumnSettings = React.createClass({ const alertStr = ; const showStr = ; + const soundStr = ; return ( - +
+
@@ -52,6 +54,7 @@ const ColumnSettings = React.createClass({
+
@@ -59,6 +62,7 @@ const ColumnSettings = React.createClass({
+
@@ -66,6 +70,7 @@ const ColumnSettings = React.createClass({
+
diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx index 8bd9edae2..8acc3faca 100644 --- a/app/assets/javascripts/components/reducers/settings.jsx +++ b/app/assets/javascripts/components/reducers/settings.jsx @@ -23,6 +23,13 @@ const initialState = Immutable.Map({ favourite: true, reblog: true, mention: true + }), + + sounds: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true }) }) }); @@ -30,7 +37,7 @@ const initialState = Immutable.Map({ export default function settings(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('settings')); + return state.mergeDeep(action.state.get('settings')); case SETTING_CHANGE: return state.setIn(action.key, action.value); default: diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx index 87f469999..6f0823bf0 100644 --- a/app/assets/javascripts/components/store/configureStore.jsx +++ b/app/assets/javascripts/components/store/configureStore.jsx @@ -3,10 +3,18 @@ import thunk from 'redux-thunk'; import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; +import soundsMiddleware from 'redux-sounds'; import Immutable from 'immutable'; +const soundsData = { + boop: '/sounds/boop.mp3' +}; + export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware(thunk, loadingBarMiddleware({ - promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], - }), errorsMiddleware()), window.devToolsExtension ? window.devToolsExtension() : f => f)); + return createStore(appReducer, compose(applyMiddleware( + thunk, + loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), + errorsMiddleware(), + soundsMiddleware(soundsData) + ), window.devToolsExtension ? window.devToolsExtension() : f => f)); }; diff --git a/config/puma.rb b/config/puma.rb index e6b0da91b..550129bdc 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,52 +1,18 @@ -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum, this matches the default thread size of Active Record. -# -threads_count = ENV.fetch("MAX_THREADS") { 5 }.to_i +threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count -# Specifies the `port` that Puma will listen on to receive requests, default is 3000. -# -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } +environment ENV.fetch('RAILS_ENV') { 'development' } +workers ENV.fetch('WEB_CONCURRENCY') { 2 } -# Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("RAILS_ENV") { "development" } - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. If you use this option -# you need to make sure to reconnect any threads in the `on_worker_boot` -# block. -# preload_app! -# The code in the `on_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted this block will be run, if you are using `preload_app!` -# option you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, Ruby -# cannot share connections between processes. -# on_worker_boot do - - if ENV["HEROKU"] #Spwan the workers from Puma, to only use one dyno + if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno @sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q mailers -q push') end ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end -# Allow puma to be restarted by `rails restart` command. plugin :tmp_restart diff --git a/package.json b/package.json index 194bcfeba..dbcc032c6 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-toggle": "^2.1.1", "redux": "^3.6.0", "redux-immutable": "^3.0.8", + "redux-sounds": "^1.1.1", "redux-thunk": "^2.1.0", "reselect": "^2.5.4", "sass-loader": "^4.0.2", diff --git a/public/sounds/boop.mp3 b/public/sounds/boop.mp3 new file mode 100644 index 000000000..02a035d91 Binary files /dev/null and b/public/sounds/boop.mp3 differ diff --git a/yarn.lock b/yarn.lock index f681b8f14..ee3e57783 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,10 @@ hosted-git-info@^2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" +howler@^1.1.28: + version "1.1.29" + resolved "https://registry.yarnpkg.com/howler/-/howler-1.1.29.tgz#9a3a7fa69e9b9d805c65ad98f66e35893a597b63" + html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" @@ -4466,6 +4470,12 @@ redux-immutable@^3.0.8: dependencies: immutable "^3.7.6" +redux-sounds@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/redux-sounds/-/redux-sounds-1.1.1.tgz#7a31052dbc617d419c53056215865762f44adb7e" + dependencies: + howler "^1.1.28" + redux-thunk@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98" -- cgit From 9bd3b11cfb7fbfc42f0ebfecf238e037d44ca39d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 20 Jan 2017 20:14:02 +0100 Subject: Instead of refusing to create accounts, domain blocks auto-suspend new accounts from that domain --- app/assets/javascripts/components/store/configureStore.jsx | 3 +++ app/services/follow_remote_account_service.rb | 2 +- app/services/process_feed_service.rb | 2 +- app/services/update_remote_profile_service.rb | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) (limited to 'app/assets/javascripts/components/store') diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx index 6f0823bf0..ad0427b52 100644 --- a/app/assets/javascripts/components/store/configureStore.jsx +++ b/app/assets/javascripts/components/store/configureStore.jsx @@ -4,8 +4,11 @@ import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from 'redux-sounds'; +import Howler from 'howler'; import Immutable from 'immutable'; +Howler.mobileAutoEnable = false; + const soundsData = { boop: '/sounds/boop.mp3' }; diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb index f640222b0..d17cf0f45 100644 --- a/app/services/follow_remote_account_service.rb +++ b/app/services/follow_remote_account_service.rb @@ -14,7 +14,6 @@ class FollowRemoteAccountService < BaseService username, domain = uri.split('@') return Account.find_local(username) if TagManager.instance.local_domain?(domain) - return nil if DomainBlock.blocked?(domain) account = Account.find_remote(username, domain) return account unless account.nil? @@ -41,6 +40,7 @@ class FollowRemoteAccountService < BaseService account.url = data.link('http://webfinger.net/rel/profile-page').href account.public_key = magic_key_to_pem(data.link('magic-public-key').href) account.private_key = nil + account.suspended = true if DomainBlock.blocked?(domain) xml = get_feed(account.remote_url) hubs = get_hubs(xml) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index 4466abda3..fad03e580 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -61,7 +61,7 @@ class ProcessFeedService < BaseService status.save! NotifyService.new.call(status.reblog.account, status) if status.reblog? && status.reblog.account.local? - LinkCrawlWorker.perform_async(status.reblog? ? status.reblog_of_id : status.id) + # LinkCrawlWorker.perform_async(status.reblog? ? status.reblog_of_id : status.id) Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" DistributionWorker.perform_async(status.id) status diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index d961eda39..cfa547996 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -10,7 +10,7 @@ class UpdateRemoteProfileService < BaseService unless author_xml.nil? account.display_name = author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).nil? account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil? - account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank? + account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank? || account.suspended? end old_hub_url = account.hub_url -- cgit