diff --git a/.dockerignore b/.dockerignore index 21d1f59a..920581ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,6 @@ node_modules storybook neo4j vendor/bundle +.DS_Store +*.swp +*~ diff --git a/.env.production.sample b/.env.production.sample index 7c82c014..9011dc21 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -1,6 +1,7 @@ # Service dependencies REDIS_HOST=redis REDIS_PORT=6379 +# REDIS_DB=0 DB_HOST=db DB_USER=postgres DB_NAME=postgres @@ -11,6 +12,10 @@ DB_PORT=5432 LOCAL_DOMAIN=example.com LOCAL_HTTPS=true +# Use this only if you need to run mastodon on a different domain than the one used for federation. +# Do not use this unless you know exactly what you are doing. +# WEB_DOMAIN=mastodon.example.com + # Application secrets # Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) PAPERCLIP_SECRET= @@ -41,6 +46,10 @@ SMTP_FROM_ADDRESS=notifications@example.com #SMTP_ENABLE_STARTTLS_AUTO=true +# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. +# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system +# PAPERCLIP_ROOT_URL=/system + # Optional asset host for multi-server setups # CDN_HOST=assets.example.com diff --git a/.env.test b/.env.test index b57f52e3..e25c040a 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,4 @@ # Federation LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_HTTPS=true +OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4 diff --git a/.eslintrc b/.eslintrc index f91385ce..a3640b26 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ "parser": "babel-eslint", "plugins": [ - "react" + "react", + "jsx-a11y" ], "parserOptions": { @@ -43,9 +44,36 @@ "no-mixed-spaces-and-tabs": 1, "no-nested-ternary": 1, "no-trailing-spaces": 1, - "react/wrap-multilines": 2, + + "react/jsx-wrap-multilines": 2, "react/self-closing-comp": 2, "react/prop-types": 2, - "react/no-multi-comp": 0 + "react/no-multi-comp": 0, + + "jsx-a11y/accessible-emoji": 1, + "jsx-a11y/anchor-has-content": 1, + "jsx-a11y/aria-activedescendant-has-tabindex": 1, + "jsx-a11y/aria-props": 1, + "jsx-a11y/aria-proptypes": 1, + "jsx-a11y/aria-role": 1, + "jsx-a11y/aria-unsupported-elements": 1, + "jsx-a11y/heading-has-content": 1, + "jsx-a11y/href-no-hash": 1, + "jsx-a11y/html-has-lang": 1, + "jsx-a11y/iframe-has-title": 1, + "jsx-a11y/img-has-alt": 1, + "jsx-a11y/img-redundant-alt": 1, + "jsx-a11y/label-has-for": 1, + "jsx-a11y/mouse-events-have-key-events": 1, + "jsx-a11y/no-access-key": 1, + "jsx-a11y/no-distracting-elements": 1, + "jsx-a11y/no-onchange": 1, + "jsx-a11y/no-redundant-roles": 1, + "jsx-a11y/onclick-has-focus": 1, + "jsx-a11y/onclick-has-role": 1, + "jsx-a11y/role-has-required-aria-props": 1, + "jsx-a11y/role-supports-aria-props": 1, + "jsx-a11y/scope": 1, + "jsx-a11y/tabindex-no-positive": 1 } } diff --git a/.gitignore b/.gitignore index 5c95f780..cda6b87b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,16 @@ neo4j/ # Ignore Capistrano customizations config/deploy/* - # Ignore IDE files .vscode/ # Ignore postgres + redis volume optionally created by docker-compose postgres redis + +# Ignore Apple files +.DS_Store + +# Ignore vim files +*~ +*.swp diff --git a/.nvmrc b/.nvmrc index f0e13c50..1e8b3149 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6.7.0 +6 diff --git a/.travis.yml b/.travis.yml index a9824ccf..45a71d83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,6 @@ notifications: email: false env: - matrix: - - TRAVIS_NODE_VERSION="4" global: - LOCAL_DOMAIN=cb6e6126.ngrok.io - LOCAL_HTTPS=true @@ -28,8 +26,7 @@ before_install: - sudo apt-get -qq update - sudo apt-get -qq install g++-4.8 install: - - nvm install $TRAVIS_NODE_VERSION - - npm install -g npm@3 + - nvm install - npm install -g yarn - bundle install - yarn install diff --git a/Dockerfile b/Dockerfile index a05525b3..dc1ebb06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN BUILD_DEPS=" \ imagemagick \ && npm install -g npm@3 && npm install -g yarn \ && bundle install --deployment --without test development \ - && yarn \ + && yarn --ignore-optional \ && yarn cache clean \ && npm -g cache clean \ && apk del $BUILD_DEPS \ diff --git a/Gemfile b/Gemfile index 7f0c6ec8..be3be29a 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem 'rabl' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' gem 'rack-timeout' +gem 'rails-i18n' gem 'rails-settings-cached' gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] gem 'rqrcode' @@ -70,7 +71,9 @@ group :development, :test do end group :test do + gem 'capybara' gem 'faker' + gem 'microformats2' gem 'rails-controller-testing' gem 'rspec-sidekiq' gem 'simplecov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ebf0eca8..2d22b0eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,13 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) + capybara (2.13.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) chunky_png (1.3.8) climate_control (0.1.0) cocaine (0.5.8) @@ -233,6 +240,10 @@ GEM mail (2.6.4) mime-types (>= 1.16, < 4) method_source (0.8.2) + microformats2 (2.1.0) + activesupport + json + nokogiri mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) @@ -308,6 +319,9 @@ GEM nokogiri (~> 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails-i18n (5.0.3) + i18n (~> 0.7) + railties (~> 5.0) rails-settings-cached (0.6.5) rails (>= 4.2.0) rails_12factor (0.0.3) @@ -447,6 +461,8 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) whatlanguage (1.0.6) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -466,6 +482,7 @@ DEPENDENCIES capistrano-rails capistrano-rbenv capistrano-yarn + capybara coffee-rails (~> 4.1.0) devise devise-two-factor @@ -490,6 +507,7 @@ DEPENDENCIES letter_opener_web link_header lograge + microformats2 nokogiri oj ostatus2 @@ -507,6 +525,7 @@ DEPENDENCIES rack-timeout rails (~> 5.0.2) rails-controller-testing + rails-i18n rails-settings-cached rails_12factor react-rails diff --git a/Vagrantfile b/Vagrantfile index 90f60464..66892e44 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -43,7 +43,7 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build -export PATH="$HOME/.rbenv/bin::$PATH" +export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)" cd /vagrant diff --git a/app/assets/javascripts/components/actions/mutes.jsx b/app/assets/javascripts/components/actions/mutes.jsx new file mode 100644 index 00000000..82482159 --- /dev/null +++ b/app/assets/javascripts/components/actions/mutes.jsx @@ -0,0 +1,82 @@ +import api, { getLinks } from '../api' +import { fetchRelationships } from './accounts'; + +export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; +export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; +export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL'; + +export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; +export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; +export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; + +export function fetchMutes() { + return (dispatch, getState) => { + dispatch(fetchMutesRequest()); + + api(getState).get('/api/v1/mutes').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchMutesFail(error))); + }; +}; + +export function fetchMutesRequest() { + return { + type: MUTES_FETCH_REQUEST + }; +}; + +export function fetchMutesSuccess(accounts, next) { + return { + type: MUTES_FETCH_SUCCESS, + accounts, + next + }; +}; + +export function fetchMutesFail(error) { + return { + type: MUTES_FETCH_FAIL, + error + }; +}; + +export function expandMutes() { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'mutes', 'next']); + + if (url === null) { + return; + } + + dispatch(expandMutesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandMutesFail(error))); + }; +}; + +export function expandMutesRequest() { + return { + type: MUTES_EXPAND_REQUEST + }; +}; + +export function expandMutesSuccess(accounts, next) { + return { + type: MUTES_EXPAND_SUCCESS, + accounts, + next + }; +}; + +export function expandMutesFail(error) { + return { + type: MUTES_EXPAND_FAIL, + error + }; +}; diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 8ce9b192..6fda5915 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -10,7 +10,8 @@ const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock' } + unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute' } }); const buttonsStyle = { @@ -25,6 +26,7 @@ const Account = React.createClass({ me: React.PropTypes.number.isRequired, onFollow: React.PropTypes.func.isRequired, onBlock: React.PropTypes.func.isRequired, + onMute: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired }, @@ -38,6 +40,10 @@ const Account = React.createClass({ this.props.onBlock(this.props.account); }, + handleMute () { + this.props.onMute(this.props.account); + }, + render () { const { account, me, intl } = this.props; @@ -51,11 +57,14 @@ const Account = React.createClass({ const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); if (requested) { buttons = } else if (blocking) { - buttons = ; + buttons = ; + } else if (muting) { + buttons = ; } else { buttons = ; } diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx index 74442466..04328aae 100644 --- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx +++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx @@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> {suggestions.map((suggestion, i) => ( -
+
))} diff --git a/app/assets/javascripts/components/components/button.jsx b/app/assets/javascripts/components/components/button.jsx index babc6b25..c3e18402 100644 --- a/app/assets/javascripts/components/components/button.jsx +++ b/app/assets/javascripts/components/components/button.jsx @@ -9,6 +9,7 @@ const Button = React.createClass({ block: React.PropTypes.bool, secondary: React.PropTypes.bool, size: React.PropTypes.number, + style: React.PropTypes.object, children: React.PropTypes.node }, diff --git a/app/assets/javascripts/components/components/column_back_button.jsx b/app/assets/javascripts/components/components/column_back_button.jsx index 6b5ffee5..91c3b92b 100644 --- a/app/assets/javascripts/components/components/column_back_button.jsx +++ b/app/assets/javascripts/components/components/column_back_button.jsx @@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({ mixins: [PureRenderMixin], handleClick () { - if (window.history && window.history.length == 1) this.context.router.push("/"); + if (window.history && window.history.length === 1) this.context.router.push("/"); else this.context.router.goBack(); }, render () { return ( -
+
diff --git a/app/assets/javascripts/components/components/column_back_button_slim.jsx b/app/assets/javascripts/components/components/column_back_button_slim.jsx index 780e3b18..536964b0 100644 --- a/app/assets/javascripts/components/components/column_back_button_slim.jsx +++ b/app/assets/javascripts/components/components/column_back_button_slim.jsx @@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({ render () { return (
-
+
diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx index 85ce6383..75dfc8a4 100644 --- a/app/assets/javascripts/components/components/column_collapsable.jsx +++ b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({ return (
-
+
+ +
{({ opacity, height }) => diff --git a/app/assets/javascripts/components/components/load_more.jsx b/app/assets/javascripts/components/components/load_more.jsx index 2cb9b09a..b4bc8b71 100644 --- a/app/assets/javascripts/components/components/load_more.jsx +++ b/app/assets/javascripts/components/components/load_more.jsx @@ -1,7 +1,7 @@ import { FormattedMessage } from 'react-intl'; const LoadMore = ({ onClick }) => ( - + ); diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx index 10b7d525..325fd815 100644 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ b/app/assets/javascripts/components/components/media_gallery.jsx @@ -220,7 +220,7 @@ const MediaGallery = React.createClass({ } children = ( -
+
{warning}
diff --git a/app/assets/javascripts/components/components/permalink.jsx b/app/assets/javascripts/components/components/permalink.jsx index ae2fb0d2..c39546b5 100644 --- a/app/assets/javascripts/components/components/permalink.jsx +++ b/app/assets/javascripts/components/components/permalink.jsx @@ -6,7 +6,8 @@ const Permalink = React.createClass({ propTypes: { href: React.PropTypes.string.isRequired, - to: React.PropTypes.string.isRequired + to: React.PropTypes.string.isRequired, + children: React.PropTypes.node }, handleClick (e) { diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 02424e77..51196071 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -92,10 +92,14 @@ const StatusActionBar = React.createClass({ menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); } + let reblogIcon = 'retweet'; + if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; + else if (status.get('visibility') === 'private') reblogIcon = 'lock'; + return (
-
+
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 33e407e4..ce8ead41 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -119,7 +119,7 @@ const StatusContent = React.createClass({ return (
{mentionsPlaceholder} diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index dce276c7..e9e860e3 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -194,7 +194,7 @@ const VideoPlayer = React.createClass({ if (!this.state.visible) { if (sensitive) { return ( -
+
{spoilerButton} @@ -202,7 +202,7 @@ const VideoPlayer = React.createClass({ ); } else { return ( -
+
{spoilerButton} @@ -213,7 +213,7 @@ const VideoPlayer = React.createClass({ if (this.state.preview && !autoplay) { return ( -
+
{spoilerButton}
@@ -225,7 +225,7 @@ const VideoPlayer = React.createClass({ {spoilerButton} {muteButton} {expandButton} -
); } diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 5cd72782..a771d126 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -37,6 +37,7 @@ import FollowRequests from '../features/follow_requests'; import GenericNotFound from '../features/generic_not_found'; import FavouritedStatuses from '../features/favourited_statuses'; import Blocks from '../features/blocks'; +import Mutes from '../features/mutes'; import Report from '../features/report'; import { IntlProvider, addLocaleData } from 'react-intl'; import en from 'react-intl/locale-data/en'; @@ -60,8 +61,8 @@ import { hydrateStore } from '../actions/store'; import createStream from '../stream'; const store = configureStore(); - -store.dispatch(hydrateStore(window.INITIAL_STATE)); +const initialState = JSON.parse(document.getElementById("initial-state").textContent); +store.dispatch(hydrateStore(initialState)); const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' @@ -94,9 +95,10 @@ const Mastodon = React.createClass({ componentDidMount() { const { locale } = this.props; + const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']); const accessToken = store.getState().getIn(['meta', 'access_token']); - this.subscription = createStream(accessToken, 'user', { + this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', { connected () { store.dispatch(connectTimeline('home')); @@ -171,6 +173,7 @@ const Mastodon = React.createClass({ + diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index a359963c..c4619a3c 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -43,7 +43,16 @@ const Avatar = React.createClass({ return ( {({ radius }) => - + {account.get('acct')} } diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx index acfc30b6..f7bc94d9 100644 --- a/app/assets/javascripts/components/features/community_timeline/index.jsx +++ b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -19,6 +19,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, + streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), accessToken: state.getIn(['meta', 'access_token']) }); @@ -29,6 +30,7 @@ const CommunityTimeline = React.createClass({ propTypes: { dispatch: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired, + streamingAPIBaseURL: React.PropTypes.string.isRequired, accessToken: React.PropTypes.string.isRequired, hasUnread: React.PropTypes.bool }, @@ -36,7 +38,7 @@ const CommunityTimeline = React.createClass({ mixins: [PureRenderMixin], componentDidMount () { - const { dispatch, accessToken } = this.props; + const { dispatch, streamingAPIBaseURL, accessToken } = this.props; dispatch(refreshTimeline('community')); @@ -44,7 +46,7 @@ const CommunityTimeline = React.createClass({ return; } - subscription = createStream(accessToken, 'public:local', { + subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', { connected () { dispatch(connectTimeline('community')); diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index d2e65359..c7df2276 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -92,7 +92,7 @@ const ComposeForm = React.createClass({ }, componentDidUpdate (prevProps) { - // This statement does several things: + // This statement does several things: // - If we're beginning a reply, and, // - Replying to zero or one users, places the cursor at the end of the textbox. // - Replying to more than one user, selects any usernames past the first; diff --git a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx index e54fa4d2..de8942d4 100644 --- a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx @@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
{options.map(item => -
+
{item.shortText} diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx index 936e003f..9ca1f5dc 100644 --- a/app/assets/javascripts/components/features/compose/components/search.jsx +++ b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -36,6 +36,10 @@ const Search = React.createClass({ } }, + noop () { + + }, + handleFocus () { this.props.onShow(); }, @@ -56,9 +60,9 @@ const Search = React.createClass({ onFocus={this.handleFocus} /> -
+
- +
); diff --git a/app/assets/javascripts/components/features/followers/index.jsx b/app/assets/javascripts/components/features/followers/index.jsx index bcadf601..af0a0a16 100644 --- a/app/assets/javascripts/components/features/followers/index.jsx +++ b/app/assets/javascripts/components/features/followers/index.jsx @@ -50,7 +50,7 @@ const Followers = React.createClass({ handleLoadMore (e) { e.preventDefault(); - this.props.dispatch(expandFollowing(Number(this.props.params.accountId))); + this.props.dispatch(expandFollowers(Number(this.props.params.accountId))); }, render () { diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 05bfcc22..6167d7cf 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -14,6 +14,7 @@ const messages = defineMessages({ sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' } }); @@ -37,6 +38,7 @@ const GettingStarted = ({ intl, me }) => { {followRequests} +
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx index 7fb41333..08d5f7f5 100644 --- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx +++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx @@ -13,6 +13,7 @@ import createStream from '../../stream'; const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0, + streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']), accessToken: state.getIn(['meta', 'access_token']) }); @@ -21,6 +22,7 @@ const HashtagTimeline = React.createClass({ propTypes: { params: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, + streamingAPIBaseURL: React.PropTypes.string.isRequired, accessToken: React.PropTypes.string.isRequired, hasUnread: React.PropTypes.bool }, @@ -28,9 +30,9 @@ const HashtagTimeline = React.createClass({ mixins: [PureRenderMixin], _subscribe (dispatch, id) { - const { accessToken } = this.props; + const { streamingAPIBaseURL, accessToken } = this.props; - this.subscription = createStream(accessToken, `hashtag&tag=${id}`, { + this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, { received (data) { switch(data.event) { diff --git a/app/assets/javascripts/components/features/mutes/index.jsx b/app/assets/javascripts/components/features/mutes/index.jsx new file mode 100644 index 00000000..698b5408 --- /dev/null +++ b/app/assets/javascripts/components/features/mutes/index.jsx @@ -0,0 +1,68 @@ +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 { ScrollContainer } from 'react-router-scroll'; +import Column from '../ui/components/column'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import AccountContainer from '../../containers/account_container'; +import { fetchMutes, expandMutes } from '../../actions/mutes'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + heading: { id: 'column.mutes', defaultMessage: 'Muted users' } +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'mutes', 'items']) +}); + +const Mutes = React.createClass({ + propTypes: { + params: React.PropTypes.object.isRequired, + dispatch: React.PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + intl: React.PropTypes.object.isRequired + }, + + mixins: [PureRenderMixin], + + componentWillMount () { + this.props.dispatch(fetchMutes()); + }, + + handleScroll (e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + + if (scrollTop === scrollHeight - clientHeight) { + this.props.dispatch(expandMutes()); + } + }, + + render () { + const { intl, accountIds } = this.props; + + if (!accountIds) { + return ( + + + + ); + } + + return ( + + + +
+ {accountIds.map(id => + + )} +
+
+
+ ); + } +}); + +export default connect(mapStateToProps)(injectIntl(Mutes)); diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx index debbfd01..63fe86af 100644 --- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({ const { intl } = this.props; return ( -
+
); 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 2edf9829..03bfaa65 100644 --- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({ propTypes: { settings: ImmutablePropTypes.map.isRequired, - intl: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired, onSave: React.PropTypes.func.isRequired, + intl: React.PropTypes.shape({ + formatMessage: React.PropTypes.func.isRequired + }).isRequired }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index fdebe4bb..2a9f2d07 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -71,7 +71,7 @@ const Notification = React.createClass({ ); }, - render () { + render () { // eslint-disable-line consistent-return const { notification } = this.props; const account = notification.get('account'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); diff --git a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx index eae3c2be..c4bfad5c 100644 --- a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx +++ b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx @@ -14,8 +14,8 @@ const labelSpanStyle = { marginLeft: '8px' }; -const SettingToggle = ({ settings, settingKey, label, onChange }) => ( -