From 0c600e9db6f343cdf8068554e27590e126035229 Mon Sep 17 00:00:00 2001 From: Jessica Stokes Date: Wed, 4 Jan 2017 19:30:02 -0800 Subject: Move "getting started" to its own route --- app/assets/javascripts/components/containers/mastodon.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 670455376..026daeb06 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -16,6 +16,7 @@ import { useRouterHistory, Router, Route, + IndexRedirect, IndexRoute } from 'react-router'; import { useScroll } from 'react-router-scroll'; @@ -107,8 +108,9 @@ const Mastodon = React.createClass({ - + + -- cgit From 989c3f40022bc65d69915be597acda3c4d58de60 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 6 Jan 2017 22:09:55 +0100 Subject: Add tab bar alternative to desktop UI, upgrade react & react-redux --- .../javascripts/components/containers/mastodon.jsx | 7 +-- .../features/compose/components/drawer.jsx | 73 ++++++++++++++++++---- .../features/compose/components/navigation_bar.jsx | 2 +- .../components/features/compose/index.jsx | 5 +- .../components/features/ui/components/tabs_bar.jsx | 2 +- .../javascripts/components/features/ui/index.jsx | 9 ++- app/assets/stylesheets/components.scss | 22 +++++++ package.json | 9 +-- yarn.lock | 6 +- 9 files changed, 103 insertions(+), 32 deletions(-) (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 026daeb06..6c0d28053 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -9,7 +9,6 @@ import { import { updateNotifications } from '../actions/notifications'; import { setAccessToken } from '../actions/meta'; import { setAccountSelf } from '../actions/accounts'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; import createBrowserHistory from 'history/lib/createBrowserHistory'; import { applyRouterMiddleware, @@ -63,8 +62,6 @@ const Mastodon = React.createClass({ locale: React.PropTypes.string.isRequired }, - mixins: [PureRenderMixin], - componentWillMount() { const { token, account, locale } = this.props; @@ -108,9 +105,9 @@ const Mastodon = React.createClass({ - + - + diff --git a/app/assets/javascripts/components/features/compose/components/drawer.jsx b/app/assets/javascripts/components/features/compose/components/drawer.jsx index d31d0e453..b694cdd2a 100644 --- a/app/assets/javascripts/components/features/compose/components/drawer.jsx +++ b/app/assets/javascripts/components/features/compose/components/drawer.jsx @@ -1,26 +1,75 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { Link } from 'react-router'; +import { injectIntl, defineMessages } from 'react-intl'; -const style = { +const messages = defineMessages({ + start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } +}); + +const outerStyle = { + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + overflowY: 'hidden' +}; + +const innerStyle = { boxSizing: 'border-box', - background: '#454b5e', padding: '0', display: 'flex', flexDirection: 'column', - overflowY: 'auto' + overflowY: 'auto', + flexGrow: '1' +}; + +const tabStyle = { + display: 'block', + flex: '1 1 auto', + padding: '15px', + paddingBottom: '13px', + color: '#9baec8', + textDecoration: 'none', + textAlign: 'center', + fontSize: '16px', + borderBottom: '2px solid transparent' }; -const Drawer = React.createClass({ +const tabActiveStyle = { + color: '#2b90d9', + borderBottom: '2px solid #2b90d9' +}; - mixins: [PureRenderMixin], +const Drawer = ({ children, withHeader, intl }) => { + let header = ''; - render () { - return ( -
- {this.props.children} + if (withHeader) { + header = ( +
+ + + +
); } -}); + return ( +
+ {header} + +
+ {children} +
+
+ ); +}; + +Drawer.propTypes = { + withHeader: React.PropTypes.bool, + children: React.PropTypes.node, + intl: React.PropTypes.object +}; -export default Drawer; +export default injectIntl(Drawer); diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx index 71b50fc3a..289e2dddf 100644 --- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx +++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx @@ -21,7 +21,7 @@ const NavigationBar = React.createClass({
{this.props.account.get('acct')} - ยท +
); diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index 4017c8949..f6095c0c6 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -10,7 +10,8 @@ import { mountCompose, unmountCompose } from '../../actions/compose'; const Compose = React.createClass({ propTypes: { - dispatch: React.PropTypes.func.isRequired + dispatch: React.PropTypes.func.isRequired, + withHeader: React.PropTypes.bool }, mixins: [PureRenderMixin], @@ -25,7 +26,7 @@ const Compose = React.createClass({ render () { return ( - + diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx index aa40a488f..2f8a28fad 100644 --- a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx +++ b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx @@ -30,7 +30,7 @@ const TabsBar = () => { - + ); }; diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 76e3dd940..db793f945 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -14,6 +14,11 @@ import { connect } from 'react-redux'; const UI = React.createClass({ + propTypes: { + dispatch: React.PropTypes.func.isRequired, + children: React.PropTypes.node + }, + getInitialState () { return { width: window.innerWidth @@ -41,7 +46,7 @@ const UI = React.createClass({ handleDrop (e) { e.preventDefault(); - if (e.dataTransfer) { + if (e.dataTransfer && e.dataTransfer.files.length === 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } }, @@ -72,7 +77,7 @@ const UI = React.createClass({ } else { mountedColumns = ( - + {this.props.children} diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index c2fcd76b3..69595995c 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -349,6 +349,28 @@ width: 280px; } +.drawer__inner { + background: linear-gradient(rgba(69, 75, 94, 1), rgba(69, 75, 94, 0.65)); +} + +.drawer__header { + flex: 0 0 auto; + font-size: 16px; + background: darken(#454b5e, 5%); + margin-bottom: 10px; + display: flex; + flex-direction: row; + + a { + transition: all 100ms ease-in; + + &:hover { + background: darken(#454b5e, 10%); + transition: all 200ms ease-out; + } + } +} + .column, .drawer { margin-left: 5px; margin-right: 5px; diff --git a/package.json b/package.json index 3a9365d3e..6a072ca06 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "chai": "^3.5.0", "chai-enzyme": "^0.5.2", "css-loader": "^0.26.1", - "emojione": "^2.2.6", + "emojione": "latest", "enzyme": "^2.4.1", "es6-promise": "^3.2.1", "http-link-header": "^0.5.0", @@ -39,22 +39,19 @@ "react-motion": "^0.4.5", "react-notification": "^6.4.0", "react-proxy": "^1.1.8", - "react-redux": "^5.0.0-beta.3", + "react-redux": "^5.0.1", "react-redux-loading-bar": "^2.4.1", "react-router": "^2.8.0", "react-router-scroll": "^0.3.2", "react-simple-dropdown": "^1.1.4", "react-storybook-addon-intl": "^0.1.0", "react-toggle": "^2.1.1", - "redux": "^3.5.2", + "redux": "^3.6.0", "redux-immutable": "^3.0.8", "redux-thunk": "^2.1.0", "reselect": "^2.5.4", "sass-loader": "^4.0.2", "sinon": "^1.17.6", "style-loader": "^0.13.1" - }, - "dependencies": { - "emojione": "latest" } } diff --git a/yarn.lock b/yarn.lock index db5f7d408..948de9ba8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4301,9 +4301,9 @@ react-redux@^4.4.5: lodash "^4.2.0" loose-envify "^1.1.0" -react-redux@^5.0.0-beta.3: - version "5.0.0-beta.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.0-beta.3.tgz#d50bfb00799cf7d2a9fd55fe34d6b3ecc24d3072" +react-redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.1.tgz#84a41bd4cdd180452bb6922bc79ad25bd5abb7c4" dependencies: hoist-non-react-statics "^1.0.3" invariant "^2.0.0" -- cgit From d64c454cfe0db2e0f8205e37be4b0161309c5c2c Mon Sep 17 00:00:00 2001 From: blackle Date: Sun, 8 Jan 2017 05:04:01 -0500 Subject: Switch to compose view when tapping 'mention' in dropdown on mobile --- .../javascripts/components/components/status_action_bar.jsx | 2 +- app/assets/javascripts/components/containers/status_container.jsx | 6 +++++- app/assets/javascripts/components/features/account/index.jsx | 8 ++++++++ app/assets/javascripts/components/features/status/index.jsx | 4 ++++ app/assets/javascripts/components/features/ui/index.jsx | 5 ++--- app/assets/javascripts/components/is_mobile.jsx | 5 +++++ 6 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/components/is_mobile.jsx (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index afaf82561..c037bc573 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -49,7 +49,7 @@ const StatusActionBar = React.createClass({ }, handleMentionClick () { - this.props.onMention(this.props.status.get('account')); + this.props.onMention(this.props.status.get('account'), this.context.router); }, handleBlockClick () { diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index 6a882eab4..ad2be03d1 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -15,6 +15,7 @@ import { blockAccount } from '../actions/accounts'; import { deleteStatus } from '../actions/statuses'; import { openMedia } from '../actions/modal'; import { createSelector } from 'reselect' +import { isMobile } from '../is_mobile' const mapStateToProps = (state, props) => ({ statusBase: state.getIn(['statuses', props.id]), @@ -86,8 +87,11 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(deleteStatus(status.get('id'))); }, - onMention (account) { + onMention (account, router) { dispatch(mentionCompose(account)); + if (isMobile(window.innerWidth)) { + router.push('/statuses/new'); + } }, onOpenMedia (url) { diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index c2cc58bb2..2a9eba28a 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -20,6 +20,7 @@ import LoadingIndicator from '../../components/loading_indicator'; import ActionBar from './components/action_bar'; import Column from '../ui/components/column'; import ColumnBackButton from '../../components/column_back_button'; +import { isMobile } from '../../is_mobile' const makeMapStateToProps = () => { const getAccount = makeGetAccount(); @@ -34,6 +35,10 @@ const makeMapStateToProps = () => { const Account = React.createClass({ + contextTypes: { + router: React.PropTypes.object + }, + propTypes: { params: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, @@ -71,6 +76,9 @@ const Account = React.createClass({ handleMention () { this.props.dispatch(mentionCompose(this.props.account)); + if (isMobile(window.innerWidth)) { + this.context.router.push('/statuses/new'); + } }, render () { diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 0a1528fe9..27a252759 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -23,6 +23,7 @@ import { ScrollContainer } from 'react-router-scroll'; import ColumnBackButton from '../../components/column_back_button'; import StatusContainer from '../../containers/status_container'; import { openMedia } from '../../actions/modal'; +import { isMobile } from '../../is_mobile' const makeMapStateToProps = () => { const getStatus = makeGetStatus(); @@ -80,6 +81,9 @@ const Status = React.createClass({ handleMentionClick (account) { this.props.dispatch(mentionCompose(account)); + if (isMobile(window.innerWidth)) { + this.context.router.push('/statuses/new'); + } }, handleOpenMedia (url) { diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index db793f945..ee2e29d6f 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -11,6 +11,7 @@ import Notifications from '../notifications'; import { debounce } from 'react-decoration'; import { uploadCompose } from '../../actions/compose'; import { connect } from 'react-redux'; +import { isMobile } from '../../is_mobile' const UI = React.createClass({ @@ -64,11 +65,9 @@ const UI = React.createClass({ }, render () { - const layoutBreakpoint = 1024; - let mountedColumns; - if (this.state.width <= layoutBreakpoint) { + if (isMobile(this.state.width)) { mountedColumns = ( {this.props.children} diff --git a/app/assets/javascripts/components/is_mobile.jsx b/app/assets/javascripts/components/is_mobile.jsx new file mode 100644 index 000000000..eaa6221e4 --- /dev/null +++ b/app/assets/javascripts/components/is_mobile.jsx @@ -0,0 +1,5 @@ +const LAYOUT_BREAKPOINT = 1024; + +export function isMobile(width) { + return width <= LAYOUT_BREAKPOINT; +}; -- cgit 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 --- .../javascripts/components/actions/accounts.jsx | 9 --- app/assets/javascripts/components/actions/meta.jsx | 8 --- .../javascripts/components/actions/store.jsx | 17 ++++++ .../javascripts/components/containers/mastodon.jsx | 28 ++++----- .../compose/containers/navigation_container.jsx | 8 ++- .../javascripts/components/reducers/accounts.jsx | 69 +++++++++++----------- .../javascripts/components/reducers/compose.jsx | 8 +-- .../javascripts/components/reducers/meta.jsx | 18 +++--- .../components/store/configureStore.jsx | 13 ++-- app/helpers/home_helper.rb | 2 - app/views/home/index.html.haml | 18 ++++++ 11 files changed, 108 insertions(+), 90 deletions(-) delete mode 100644 app/assets/javascripts/components/actions/meta.jsx create mode 100644 app/assets/javascripts/components/actions/store.jsx (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 8d28b051f..7ae87f30e 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -1,8 +1,6 @@ import api, { getLinks } from '../api' import Immutable from 'immutable'; -export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF'; - export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; @@ -67,13 +65,6 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; -export function setAccountSelf(account) { - return { - type: ACCOUNT_SET_SELF, - account - }; -}; - export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchAccountRequest(id)); diff --git a/app/assets/javascripts/components/actions/meta.jsx b/app/assets/javascripts/components/actions/meta.jsx deleted file mode 100644 index d0adbce3f..000000000 --- a/app/assets/javascripts/components/actions/meta.jsx +++ /dev/null @@ -1,8 +0,0 @@ -export const ACCESS_TOKEN_SET = 'ACCESS_TOKEN_SET'; - -export function setAccessToken(token) { - return { - type: ACCESS_TOKEN_SET, - token: token - }; -}; diff --git a/app/assets/javascripts/components/actions/store.jsx b/app/assets/javascripts/components/actions/store.jsx new file mode 100644 index 000000000..3bba99549 --- /dev/null +++ b/app/assets/javascripts/components/actions/store.jsx @@ -0,0 +1,17 @@ +import Immutable from 'immutable'; + +export const STORE_HYDRATE = 'STORE_HYDRATE'; + +const convertState = rawState => + Immutable.fromJS(rawState, (k, v) => + Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => + Number.isNaN(x * 1) ? x : x * 1)); + +export function hydrateStore(rawState) { + const state = convertState(rawState); + + return { + type: STORE_HYDRATE, + state + }; +}; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 6c0d28053..143a280c3 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -7,8 +7,6 @@ import { refreshTimeline } from '../actions/timelines'; import { updateNotifications } from '../actions/notifications'; -import { setAccessToken } from '../actions/meta'; -import { setAccountSelf } from '../actions/accounts'; import createBrowserHistory from 'history/lib/createBrowserHistory'; import { applyRouterMiddleware, @@ -44,9 +42,12 @@ import pt from 'react-intl/locale-data/pt'; import hu from 'react-intl/locale-data/hu'; import uk from 'react-intl/locale-data/uk'; import getMessagesForLocale from '../locales'; +import { hydrateStore } from '../actions/store'; const store = configureStore(); +store.dispatch(hydrateStore(window.INITIAL_STATE)); + const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); @@ -56,29 +57,26 @@ addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]); const Mastodon = React.createClass({ propTypes: { - token: React.PropTypes.string.isRequired, - timelines: React.PropTypes.object, - account: React.PropTypes.string, locale: React.PropTypes.string.isRequired }, componentWillMount() { - const { token, account, locale } = this.props; - - store.dispatch(setAccessToken(token)); - store.dispatch(setAccountSelf(JSON.parse(account))); + const { locale } = this.props; if (typeof App !== 'undefined') { this.subscription = App.cable.subscriptions.create('TimelineChannel', { received (data) { switch(data.type) { - case 'update': - return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); - case 'delete': - return store.dispatch(deleteFromTimelines(data.id)); - case 'notification': - return store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale)); + case 'update': + store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); + break; + case 'delete': + store.dispatch(deleteFromTimelines(data.id)); + break; + case 'notification': + store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale)); + break; } } diff --git a/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx b/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx index 51e2513d8..0006608da 100644 --- a/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/navigation_container.jsx @@ -1,8 +1,10 @@ import { connect } from 'react-redux'; import NavigationBar from '../components/navigation_bar'; -const mapStateToProps = (state, props) => ({ - account: state.getIn(['accounts', state.getIn(['meta', 'me'])]) -}); +const mapStateToProps = (state, props) => { + return { + account: state.getIn(['accounts', state.getIn(['meta', 'me'])]) + }; +}; export default connect(mapStateToProps)(NavigationBar); diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx index 7f2f89d0a..ae048df3b 100644 --- a/app/assets/javascripts/components/reducers/accounts.jsx +++ b/app/assets/javascripts/components/reducers/accounts.jsx @@ -1,5 +1,4 @@ import { - ACCOUNT_SET_SELF, ACCOUNT_FETCH_SUCCESS, FOLLOWERS_FETCH_SUCCESS, FOLLOWERS_EXPAND_SUCCESS, @@ -33,6 +32,7 @@ import { NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS } from '../actions/notifications'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account)); @@ -67,38 +67,39 @@ const initialState = Immutable.Map(); export default function accounts(state = initialState, action) { switch(action.type) { - case ACCOUNT_SET_SELF: - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case SEARCH_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - return normalizeAccounts(state, action.accounts); - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); - default: - return state; + case STORE_HYDRATE: + return state.merge(action.state.get('accounts')); + case ACCOUNT_FETCH_SUCCESS: + case NOTIFICATIONS_UPDATE: + return normalizeAccount(state, action.account); + case FOLLOWERS_FETCH_SUCCESS: + case FOLLOWERS_EXPAND_SUCCESS: + case FOLLOWING_FETCH_SUCCESS: + case FOLLOWING_EXPAND_SUCCESS: + case REBLOGS_FETCH_SUCCESS: + case FAVOURITES_FETCH_SUCCESS: + case COMPOSE_SUGGESTIONS_READY: + case SEARCH_SUGGESTIONS_READY: + case FOLLOW_REQUESTS_FETCH_SUCCESS: + return normalizeAccounts(state, action.accounts); + case NOTIFICATIONS_REFRESH_SUCCESS: + case NOTIFICATIONS_EXPAND_SUCCESS: + return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); + case TIMELINE_REFRESH_SUCCESS: + case TIMELINE_EXPAND_SUCCESS: + case ACCOUNT_TIMELINE_FETCH_SUCCESS: + case ACCOUNT_TIMELINE_EXPAND_SUCCESS: + case CONTEXT_FETCH_SUCCESS: + return normalizeAccountsFromStatuses(state, action.statuses); + case REBLOG_SUCCESS: + case FAVOURITE_SUCCESS: + case UNREBLOG_SUCCESS: + case UNFAVOURITE_SUCCESS: + return normalizeAccountFromStatus(state, action.response); + case TIMELINE_UPDATE: + case STATUS_FETCH_SUCCESS: + return normalizeAccountFromStatus(state, action.status); + default: + return state; } }; diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index 16215684e..baa7d7f5a 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -21,7 +21,7 @@ import { COMPOSE_LISTABILITY_CHANGE } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { ACCOUNT_SET_SELF } from '../actions/accounts'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -88,6 +88,8 @@ const insertSuggestion = (state, position, token, completion) => { export default function compose(state = initialState, action) { switch(action.type) { + case STORE_HYDRATE: + return state.merge(action.state.get('compose')); case COMPOSE_MOUNT: return state.set('mounted', true); case COMPOSE_UNMOUNT: @@ -97,7 +99,7 @@ export default function compose(state = initialState, action) { case COMPOSE_VISIBILITY_CHANGE: return state.set('private', action.checked); case COMPOSE_LISTABILITY_CHANGE: - return state.set('unlisted', action.checked); + return state.set('unlisted', action.checked); case COMPOSE_CHANGE: return state.set('text', action.text); case COMPOSE_REPLY: @@ -143,8 +145,6 @@ export default function compose(state = initialState, action) { } else { return state; } - case ACCOUNT_SET_SELF: - return state.set('me', action.account.id).set('private', action.account.locked); default: return state; } diff --git a/app/assets/javascripts/components/reducers/meta.jsx b/app/assets/javascripts/components/reducers/meta.jsx index c7222c60b..cd4b313d5 100644 --- a/app/assets/javascripts/components/reducers/meta.jsx +++ b/app/assets/javascripts/components/reducers/meta.jsx @@ -1,16 +1,16 @@ -import { ACCESS_TOKEN_SET } from '../actions/meta'; -import { ACCOUNT_SET_SELF } from '../actions/accounts'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; -const initialState = Immutable.Map(); +const initialState = Immutable.Map({ + access_token: null, + me: null +}); export default function meta(state = initialState, action) { switch(action.type) { - case ACCESS_TOKEN_SET: - return state.set('access_token', action.token); - case ACCOUNT_SET_SELF: - return state.set('me', action.account.id); - default: - return state; + case STORE_HYDRATE: + return state.merge(action.state.get('meta')); + default: + return state; } }; 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)); }; diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 6f87c7b72..d3c6b13a6 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -3,8 +3,6 @@ module HomeHelper def default_props { - token: @token, - account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json), locale: I18n.locale, } end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 498fae105..b4e935041 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,4 +1,22 @@ - content_for :header_tags do + :javascript + window.INITIAL_STATE = { + "meta": { + "access_token": "#{@token}", + "locale": "#{I18n.locale}", + "me": #{current_account.id} + }, + + "compose": { + "me": #{current_account.id}, + "private": #{current_account.locked?} + }, + + "accounts": { + #{current_account.id}: #{render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json)} + } + }; + = javascript_include_tag 'application' = react_component 'Mastodon', default_props, class: 'app-holder', prerender: false -- cgit From 1e9d2c4b1e00e8e68fefe5c04b48f66c827d31d5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 10 Jan 2017 13:50:40 +0100 Subject: Add "not found" component to UI --- .../components/components/loading_indicator.jsx | 22 ++++++++++++---------- .../components/components/missing_indicator.jsx | 17 +++++++++++++++++ .../javascripts/components/containers/mastodon.jsx | 2 ++ .../features/generic_not_found/index.jsx | 10 ++++++++++ .../components/features/status/index.jsx | 4 +++- 5 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/components/components/missing_indicator.jsx create mode 100644 app/assets/javascripts/components/features/generic_not_found/index.jsx (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/components/loading_indicator.jsx b/app/assets/javascripts/components/components/loading_indicator.jsx index fd5acae84..c8a263924 100644 --- a/app/assets/javascripts/components/components/loading_indicator.jsx +++ b/app/assets/javascripts/components/components/loading_indicator.jsx @@ -1,15 +1,17 @@ import { FormattedMessage } from 'react-intl'; -const LoadingIndicator = () => { - const style = { - textAlign: 'center', - fontSize: '16px', - fontWeight: '500', - color: '#616b86', - paddingTop: '120px' - }; - - return
; +const style = { + textAlign: 'center', + fontSize: '16px', + fontWeight: '500', + color: '#616b86', + paddingTop: '120px' }; +const LoadingIndicator = () => ( +
+ +
+); + export default LoadingIndicator; diff --git a/app/assets/javascripts/components/components/missing_indicator.jsx b/app/assets/javascripts/components/components/missing_indicator.jsx new file mode 100644 index 000000000..ed8b4fe24 --- /dev/null +++ b/app/assets/javascripts/components/components/missing_indicator.jsx @@ -0,0 +1,17 @@ +import { FormattedMessage } from 'react-intl'; + +const style = { + textAlign: 'center', + fontSize: '16px', + fontWeight: '500', + color: '#616b86', + paddingTop: '120px' +}; + +const MissingIndicator = () => ( +
+ +
+); + +export default MissingIndicator; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 143a280c3..af495652f 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -33,6 +33,7 @@ import Favourites from '../features/favourites'; 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 { IntlProvider, addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; import de from 'react-intl/locale-data/de'; @@ -125,6 +126,7 @@ const Mastodon = React.createClass({
+
diff --git a/app/assets/javascripts/components/features/generic_not_found/index.jsx b/app/assets/javascripts/components/features/generic_not_found/index.jsx new file mode 100644 index 000000000..a7afe29b0 --- /dev/null +++ b/app/assets/javascripts/components/features/generic_not_found/index.jsx @@ -0,0 +1,10 @@ +import Column from '../ui/components/column'; +import MissingIndicator from '../../components/missing_indicator'; + +const GenericNotFound = () => ( + + + +); + +export default GenericNotFound; diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 27a252759..389549849 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -48,7 +48,8 @@ const Status = React.createClass({ dispatch: React.PropTypes.func.isRequired, status: ImmutablePropTypes.map, ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list + descendantsIds: ImmutablePropTypes.list, + me: React.PropTypes.number }, mixins: [PureRenderMixin], @@ -81,6 +82,7 @@ const Status = React.createClass({ handleMentionClick (account) { this.props.dispatch(mentionCompose(account)); + if (isMobile(window.innerWidth)) { this.context.router.push('/statuses/new'); } -- 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/containers') 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 6cf44ca92c3b92df5bda32adb59258104f1ac9c5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 16 Jan 2017 19:36:32 +0100 Subject: Improve how the list entry Account component looks when target is blocked/follow is requested --- .../javascripts/components/components/account.jsx | 26 +++++++++++++++++----- .../components/containers/account_container.jsx | 12 +++++++++- .../features/ui/containers/modal_container.jsx | 12 +++++++++- app/models/account.rb | 2 +- 4 files changed, 44 insertions(+), 8 deletions(-) (limited to 'app/assets/javascripts/components/containers') diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 814d8a9c8..108401b2f 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -8,7 +8,9 @@ import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' } + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock' } }); const outerStyle = { @@ -42,7 +44,9 @@ const Account = React.createClass({ account: ImmutablePropTypes.map.isRequired, me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, - withNote: React.PropTypes.bool + onBlock: React.PropTypes.func.isRequired, + withNote: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired }, getDefaultProps () { @@ -57,6 +61,10 @@ const Account = React.createClass({ this.props.onFollow(this.props.account); }, + handleBlock () { + this.props.onBlock(this.props.account); + }, + render () { const { account, me, withNote, intl } = this.props; @@ -70,10 +78,18 @@ const Account = React.createClass({ note =
{account.get('note')}
; } - if (account.get('id') !== me && account.get('relationship', null) != null) { + if (account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); - - buttons = ; + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + + if (requested) { + buttons = + } else if (blocking) { + buttons = ; + } else { + buttons = ; + } } return ( diff --git a/app/assets/javascripts/components/containers/account_container.jsx b/app/assets/javascripts/components/containers/account_container.jsx index 1f49f9819..889c0ac4c 100644 --- a/app/assets/javascripts/components/containers/account_container.jsx +++ b/app/assets/javascripts/components/containers/account_container.jsx @@ -3,7 +3,9 @@ import { makeGetAccount } from '../selectors'; import Account from '../components/account'; import { followAccount, - unfollowAccount + unfollowAccount, + blockAccount, + unblockAccount } from '../actions/accounts'; const makeMapStateToProps = () => { @@ -24,6 +26,14 @@ const mapDispatchToProps = (dispatch) => ({ } else { dispatch(followAccount(account.get('id'))); } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } } }); diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index eeec55ff5..66dfe915e 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -25,7 +25,17 @@ const imageStyle = { maxHeight: '80vh' }; -const preloader = () => ; +const loadingStyle = { + background: '#373b4a', + width: '400px', + paddingBottom: '120px' +}; + +const preloader = () => ( +
+ +
+); const Modal = React.createClass({ diff --git a/app/models/account.rb b/app/models/account.rb index 5f07615fc..5d6324a7b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -126,7 +126,7 @@ class Account < ApplicationRecord def save_with_optional_avatar! save! rescue ActiveRecord::RecordInvalid => invalid - if invalid.record.errors[:avatar_file_size] || invalid[:avatar_content_type] + if invalid.record.errors[:avatar_file_size] || invalid.record.errors[:avatar_content_type] self.avatar = nil retry end -- cgit